name: Discord notifications on: push: branches: - master permissions: contents: read pull-requests: read jobs: resolvePush: runs-on: ubuntu-latest outputs: is_pr_merge: ${{ steps.resolve.outputs.is_pr_merge }} is_external: ${{ steps.resolve.outputs.is_external }} pr_author: ${{ steps.resolve.outputs.pr_author }} pr_head_ref: ${{ steps.resolve.outputs.pr_head_ref }} pr_head_repo: ${{ steps.resolve.outputs.pr_head_repo }} pr_number: ${{ steps.resolve.outputs.pr_number }} pr_title: ${{ steps.resolve.outputs.pr_title }} pr_url: ${{ steps.resolve.outputs.pr_url }} steps: - name: Resolve pushed commit id: resolve uses: actions/github-script@v7 with: script: | const { owner, repo } = context.repo; const sha = context.sha; async function findAssociatedPRs() { // It seems that when we do this too promptly after a PR merge then the data isn't there, so we have a retry loop. for (let attempt = 1; attempt <= 3; attempt++) { core.info('Looking for associated PRs..'); const prs = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, { owner, repo, commit_sha: sha, }); if (prs.length > 0) { return prs; } core.info('No associated PRs found. Will check again.'); await new Promise((resolve) => setTimeout(resolve, 30000)); } return []; } const prs = await findAssociatedPRs(); core.info(`Associated PR info found:`); core.info(JSON.stringify(prs, null, 2)); if (prs.length === 0) { core.info('No associated PRs found. Treating this push as a direct/non-PR push.'); core.setOutput('is_pr_merge', 'false'); core.info('Output is_pr_merge=false'); return; } if (prs.length > 1) { core.info('Multiple associated PRs found. Failing because downstream notification expects exactly one PR.'); core.setFailed(`Expected exactly one PR associated with ${sha}, found ${prs.length}.`); return; } const pr = prs[0]; const headRepo = pr.head?.repo?.full_name || ''; core.setOutput('is_pr_merge', 'true'); core.setOutput('is_external', String(headRepo !== `${owner}/${repo}`)); core.setOutput('pr_author', pr.user?.login || ''); core.setOutput('pr_head_ref', pr.head?.ref || ''); core.setOutput('pr_head_repo', headRepo); core.setOutput('pr_number', String(pr.number)); core.setOutput('pr_title', pr.title || ''); core.setOutput('pr_url', pr.html_url || ''); discordNotification: needs: resolvePush runs-on: ubuntu-latest if: needs.resolvePush.outputs.is_pr_merge == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Set up GitHub CLI run: | curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /usr/share/keyrings/githubcli-archive-keyring.gpg > /dev/null sudo apt-get install -y apt-transport-https echo "deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list sudo apt-get update sudo apt-get install -y gh # TODO: Perhaps we should merge this into the public-pr-merge.yml workflow, now that that exists. - name: Send Discord notification env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} PR_AUTHOR: ${{ needs.resolvePush.outputs.pr_author }} PR_HEAD_REF: ${{ needs.resolvePush.outputs.pr_head_ref }} PR_HEAD_REPO: ${{ needs.resolvePush.outputs.pr_head_repo }} PR_IS_EXTERNAL: ${{ needs.resolvePush.outputs.is_external }} PR_TITLE: ${{ needs.resolvePush.outputs.pr_title }} PR_NUMBER: ${{ needs.resolvePush.outputs.pr_number }} PR_URL: ${{ needs.resolvePush.outputs.pr_url }} MENTION_ON_FAILURE: ${{ secrets.DEV_OPS_ROLE_ID }} DISCORD_USER_MAP: ${{ secrets.DISCORD_USER_MAP }} run: | message="PR merged: [(#${PR_NUMBER}) ${PR_TITLE}](<${PR_URL}>)" if [[ "${PR_IS_EXTERNAL}" == "true" ]]; then message+=$'\n' message+="External PR source: ${PR_HEAD_REPO}@${PR_HEAD_REF}" fi # Note that anything besides success is treated as a failure (e.g. if the check did not run at all, or if it is still pending). FAILED_CHECKS="$( gh pr checks "${PR_URL}" \ --json 'workflow,state,name' | jq '.[] | select(.workflow != "Discord notifications") | select(.state != "SUCCESS" and .state != "NEUTRAL" and .state != "SKIPPED") ' | jq -r '"\(.workflow) / \(.name): \(.state)"' )" # Lookup PR author's Discord ID from the provided JSON map (if any) author_discord_id="$( jq -r \ --arg u "${PR_AUTHOR}" \ '.[$u] // empty' \ <<<"${DISCORD_USER_MAP}" )" if [ -z "${author_discord_id}" ]; then echo "Warning: PR author not found not found in USER_LOOKUP_JSON" fi message+=$'\n' if [[ -z "${FAILED_CHECKS}" ]]; then message+='All checks passed.' else message+="${FAILED_CHECKS}" message+=$'\n' # This uses special Discord syntax for pinging a particular role. # Note the '&' - this is the difference between pinging a *role* and pinging a *person*. if [[ -n "${author_discord_id}" ]]; then message+="<@${author_discord_id}> please investigate these failures." fi message+=$'\n' message+="(cc <@&${MENTION_ON_FAILURE}> - Releases may be affected)" fi # Use `jq` to construct the json data blob in the format required by the webhook. data="$(jq --null-input --arg msg "$message" '.content=$msg')" curl -X POST -H 'Content-Type: application/json' -d "$data" "${DISCORD_WEBHOOK_URL}" notifyFailure: needs: - resolvePush - discordNotification runs-on: ubuntu-latest if: ${{ always() && (needs.resolvePush.result == 'failure' || needs.discordNotification.result == 'failure') }} steps: - name: Send failure notification env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: | message="Discord notification workflow failed." message+=$'\n' message+="Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" data="$(jq --null-input --arg msg "$message" '.content=$msg')" curl -X POST -H 'Content-Type: application/json' -d "$data" "${DISCORD_WEBHOOK_URL}" warnDirectPush: needs: resolvePush runs-on: ubuntu-latest if: needs.resolvePush.outputs.is_pr_merge != 'true' steps: - name: Warn about non-PR push env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} COMMIT_URL: ${{ github.event.head_commit.url }} PUSHER: ${{ github.event.pusher.name }} SHA: ${{ github.sha }} run: | short_sha="${SHA:0:7}" subject="$(printf '%s\n' "${COMMIT_MESSAGE}" | head -n 1)" message="Warning: push to master was not associated with a merged PR." message+=$'\n' message+="Commit: [${short_sha}](<${COMMIT_URL}>)" message+=$'\n' message+="Pusher: ${PUSHER}" message+=$'\n' message+="Subject: ${subject}" data="$(jq --null-input --arg msg "$message" '.content=$msg')" curl -X POST -H 'Content-Type: application/json' -d "$data" "${DISCORD_WEBHOOK_URL}" invokePrivate: needs: resolvePush runs-on: ubuntu-latest if: needs.resolvePush.outputs.is_pr_merge == 'true' permissions: contents: read steps: - name: Dispatch private merge workflow uses: actions/github-script@v7 with: github-token: ${{ secrets.SPACETIMEDB_PRIVATE_TOKEN }} script: | await github.rest.actions.createWorkflowDispatch({ owner: 'clockworklabs', repo: 'SpacetimeDBPrivate', workflow_id: 'public-pr-merge.yml', ref: 'master', inputs: { public_pr_number: '${{ needs.resolvePush.outputs.pr_number }}', } });