diff --git a/.github/workflows/prod-build-deploy.yml b/.github/workflows/prod-build-deploy.yml index c1483d4ed7..e79204d34e 100644 --- a/.github/workflows/prod-build-deploy.yml +++ b/.github/workflows/prod-build-deploy.yml @@ -247,7 +247,7 @@ jobs: throw error } - - name: Send Slack notification if workflow fails + - name: Send Slack notification if workflow failed uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd if: ${{ failure() }} with: diff --git a/.github/workflows/staging-build-pr.yml b/.github/workflows/staging-build-pr.yml index 9263188b36..5d4f970743 100644 --- a/.github/workflows/staging-build-pr.yml +++ b/.github/workflows/staging-build-pr.yml @@ -14,6 +14,11 @@ on: permissions: contents: read +# This allows one Build workflow run to interrupt another +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label }}' + cancel-in-progress: true + jobs: debug: runs-on: ubuntu-latest @@ -27,8 +32,10 @@ jobs: if: ${{ github.repository == 'github/docs-internal' || github.repository == 'github/docs' }} runs-on: ubuntu-latest timeout-minutes: 5 + # This interrupts Build, Deploy, and pre-write Undeploy workflow runs in + # progress for this PR branch. concurrency: - group: staging_${{ github.head_ref }} + group: 'PR Staging @ ${{ github.event.pull_request.head.label }}' cancel-in-progress: true steps: - name: Check out repo diff --git a/.github/workflows/staging-deploy-pr.yml b/.github/workflows/staging-deploy-pr.yml index 7e59ff4c2b..db611d66e9 100644 --- a/.github/workflows/staging-deploy-pr.yml +++ b/.github/workflows/staging-deploy-pr.yml @@ -18,6 +18,15 @@ permissions: pull-requests: read statuses: write +# IMPORTANT: Intentionally OMIT a `concurrency` configuration from this workflow's +# top-level as we do not have any guarantee of identifying values being available +# within the `github.event` context for PRs from forked repos! +# +# The implication of this shortcoming is that we may have multiple workflow runs +# of this running at the same time for different commits within the same PR. +# However, once they reach the `concurrency` configurations deeper down within +# this workflow's jobs, then we can expect concurrent short-circuiting to begin. + env: CONTEXT_NAME: '${{ github.workflow }} / deploy (${{ github.event.workflow_run.event }})' ACTIONS_RUN_LOG: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -183,9 +192,12 @@ jobs: (github.repository == 'github/docs-internal' || github.repository == 'github/docs') }} runs-on: ubuntu-latest - timeout-minutes: 1 + # This timeout should match or exceed the value of the timeout for Undeploy + timeout-minutes: 5 + # This interrupts Build, Deploy, and pre-write Undeploy workflow runs in + # progress for this PR branch. concurrency: - group: 'staging_${{ needs.pr-metadata.outputs.head_ref }}' + group: 'PR Staging @ ${{ needs.pr-metadata.outputs.head_label }}' cancel-in-progress: true outputs: pull_request_state: ${{ steps.check-pr.outputs.state }} @@ -197,12 +209,33 @@ jobs: PR_NUMBER: ${{ needs.pr-metadata.outputs.number }} with: script: | + const sleep = require('await-sleep') // Does not require ESM + + const blockingLabel = 'automated-block-deploy' const { owner, repo } = context.repo - const { data: pullRequest } = await github.pulls.get({ - owner, - repo, - pull_number: process.env.PR_NUMBER - }) + const startTime = Date.now() + + let pullRequest = {} + let blocked = true + + // Keep polling the PR until the blocking label has been removed + while (blocked) { + const { data: pr } = await github.pulls.get({ + owner, + repo, + pull_number: process.env.PR_NUMBER + }) + + blocked = pr.labels.some(({ name }) => name === blockingLabel) + if (blocked) { + console.warn(`WARNING! PR currently has blocking label "${blockingLabel}" (after ${Date.now() - startTime} ms). Will check again soon...`) + await sleep(15000) // Wait 15 seconds and check again + } else { + console.log(`PR was unblocked (after ${Date.now() - startTime} ms)!`) + pullRequest = pr + } + } + core.setOutput('state', pullRequest.state) prepare-for-deploy: @@ -210,8 +243,10 @@ jobs: if: ${{ needs.check-pr-before-prepare.outputs.pull_request_state == 'open' }} runs-on: ubuntu-latest timeout-minutes: 5 + # This interrupts Build, Deploy, and pre-write Undeploy workflow runs in + # progress for this PR branch. concurrency: - group: 'staging_${{ needs.pr-metadata.outputs.head_ref }}' + group: 'PR Staging @ ${{ needs.pr-metadata.outputs.head_label }}' cancel-in-progress: true outputs: source_blob_url: ${{ steps.build-source.outputs.download_url }} @@ -361,7 +396,7 @@ jobs: target_url: ACTIONS_RUN_LOG }) - - name: Send Slack notification if workflow fails + - name: Send Slack notification if deployment preparation job failed uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd if: ${{ failure() }} with: @@ -374,8 +409,10 @@ jobs: needs: [pr-metadata, prepare-for-deploy] runs-on: ubuntu-latest timeout-minutes: 1 + # This interrupts Build, Deploy, and pre-write Undeploy workflow runs in + # progress for this PR branch. concurrency: - group: 'staging_${{ needs.pr-metadata.outputs.head_ref }}' + group: 'PR Staging @ ${{ needs.pr-metadata.outputs.head_label }}' cancel-in-progress: true outputs: pull_request_state: ${{ steps.check-pr.outputs.state }} @@ -400,8 +437,10 @@ jobs: if: ${{ needs.check-pr-before-deploy.outputs.pull_request_state == 'open' }} runs-on: ubuntu-latest timeout-minutes: 10 + # This interrupts Build, Deploy, and pre-write Undeploy workflow runs in + # progress for this PR branch. concurrency: - group: 'staging_${{ needs.pr-metadata.outputs.head_ref }}' + group: 'PR Staging @ ${{ needs.pr-metadata.outputs.head_label }}' cancel-in-progress: true steps: - name: Check out repo's default branch @@ -548,7 +587,7 @@ jobs: target_url: ACTIONS_RUN_LOG }) - - name: Send Slack notification if workflow fails + - name: Send Slack notification if deployment job failed uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd if: ${{ failure() }} with: diff --git a/.github/workflows/staging-undeploy-pr.yml b/.github/workflows/staging-undeploy-pr.yml index 39d6957628..e71e363f1f 100644 --- a/.github/workflows/staging-undeploy-pr.yml +++ b/.github/workflows/staging-undeploy-pr.yml @@ -13,6 +13,11 @@ permissions: contents: read deployments: write +# This prevents one Undeploy workflow run from interrupting another +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label }}' + cancel-in-progress: false + jobs: debug: runs-on: ubuntu-latest @@ -22,15 +27,33 @@ jobs: GITHUB_CONTEXT: ${{ toJSON(github) }} run: echo "$GITHUB_CONTEXT" - undeploy: + cancel-jobs-before-undeploy: if: ${{ github.repository == 'github/docs-internal' || github.repository == 'github/docs' }} - name: Undeploy runs-on: ubuntu-latest - timeout-minutes: 2 + # This interrupts Build and Deploy workflow runs in progress for this PR + # branch. However, it does so with an intentionally short, independent job + # so that the following `undeploy` job cannot be cancelled once started! concurrency: - group: staging_${{ github.head_ref }} + group: 'PR Staging @ ${{ github.event.pull_request.head.label }}' cancel-in-progress: true steps: + - name: Cancelling other deployments via concurrency configuration + run: | + echo 'Cancelling other deployment runs (if any)...' + + undeploy: + if: ${{ github.repository == 'github/docs-internal' || github.repository == 'github/docs' }} + runs-on: ubuntu-latest + timeout-minutes: 5 + # IMPORTANT: Intentionally OMIT a `concurrency` configuration from this job! + steps: + - name: Add a label to the PR to block deployment during undeployment + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + gh pr edit $PR_NUMBER --add-label "automated-block-deploy" + - name: Check out repo's default branch uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f with: @@ -91,3 +114,20 @@ jobs: console.error(error) throw error } + + - if: ${{ always() }} + name: Remove the label from the PR to unblock deployment + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + gh pr edit $PR_NUMBER --remove-label "automated-block-deploy" + + - name: Send Slack notification if workflow failed + uses: someimportantcompany/github-actions-slack-message@0b470c14b39da4260ed9e3f9a4f1298a74ccdefd + if: ${{ failure() }} + with: + channel: ${{ secrets.DOCS_STAGING_DEPLOYMENT_FAILURES_SLACK_CHANNEL_ID }} + bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + color: failure + text: Staging undeployment failed for PR ${{ github.event.pull_request.html_url }} at commit ${{ github.head_sha }}. See https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}.