fix: simplify terminal conjunction condition #290
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Documentation | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| rollback_run_id: | |
| description: Redeploy the retained artifact from this documentation run | |
| required: false | |
| type: string | |
| rollback_run_attempt: | |
| description: Exact attempt of the retained documentation run | |
| required: false | |
| type: string | |
| expected_pointer_run_id: | |
| description: Automation guard for a queued recovery | |
| required: false | |
| type: string | |
| expected_pointer_run_attempt: | |
| description: Exact attempt guarded by queued recovery | |
| required: false | |
| type: string | |
| permissions: {} | |
| defaults: | |
| run: | |
| shell: bash | |
| concurrency: | |
| group: >- | |
| ${{ | |
| github.event_name == 'pull_request' && | |
| format( | |
| 'documentation-pr-{0}-{1}', | |
| github.event.pull_request.number, | |
| github.run_id | |
| ) || | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.rollback_run_id == '' && | |
| format('documentation-validation-{0}', github.ref) || | |
| 'pages' | |
| }} | |
| queue: max | |
| cancel-in-progress: false | |
| env: | |
| DOCS_RELEASE_TARGET: main | |
| DOTNET_CLI_TELEMETRY_OPTOUT: true | |
| DOTNET_NOLOGO: true | |
| GH_REPO: ${{ github.repository }} | |
| jobs: | |
| validate: | |
| if: inputs.rollback_run_id == '' | |
| permissions: | |
| actions: read | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| gate: | |
| - manifests | |
| - runtime | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 | |
| with: | |
| global-json-file: global.json | |
| - name: Cache immutable documentation packages | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: /tmp/humanizer-docs-nuget/**/*.nupkg | |
| key: >- | |
| docs-nuget-v2-${{ runner.os }}-${{ | |
| hashFiles( | |
| 'website/humanizer-versions.json', | |
| 'nuget.config', | |
| '.config/dotnet-tools.json' | |
| ) | |
| }}-${{ matrix.gate }} | |
| - name: Restore documentation tools | |
| run: dotnet tool restore | |
| - name: Validate manifests, snapshots, APIs, and examples | |
| if: matrix.gate == 'manifests' | |
| shell: pwsh | |
| run: ./tools/docs/build.ps1 -Mode Validate | |
| - name: Validate snapshot transactions and Native AOT | |
| if: matrix.gate == 'runtime' | |
| shell: pwsh | |
| run: ./tools/docs/test.ps1 -RequireNativeAot | |
| build: | |
| outputs: | |
| archive_run_attempt: ${{ steps.deployment-identity.outputs.archive_run_attempt }} | |
| archive_run_id: ${{ steps.deployment-identity.outputs.archive_run_id }} | |
| bootstrap_rollback: >- | |
| ${{ steps.rollback-source.outputs.bootstrap_rollback || 'false' }} | |
| recovery_stale: >- | |
| ${{ steps.rollback-source.outputs.recovery_stale || 'false' }} | |
| source_sha: ${{ steps.deployment-identity.outputs.source_sha }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Require main for rollback | |
| if: >- | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.rollback_run_id != '' && | |
| github.ref != 'refs/heads/main' | |
| run: | | |
| echo "Rollbacks must run from main." >&2 | |
| exit 1 | |
| - name: Setup .NET | |
| if: inputs.rollback_run_id == '' | |
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 | |
| with: | |
| global-json-file: global.json | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 | |
| with: | |
| package_json_file: website/package.json | |
| - name: Setup Node | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version-file: website/.node-version | |
| cache: pnpm | |
| cache-dependency-path: website/pnpm-lock.yaml | |
| - name: Restore site dependencies | |
| if: inputs.rollback_run_id == '' | |
| run: pnpm --dir website install --frozen-lockfile | |
| - name: Validate content | |
| if: inputs.rollback_run_id == '' | |
| run: pnpm --dir website run check:content | |
| - name: Build the complete site | |
| if: inputs.rollback_run_id == '' | |
| run: pnpm --dir website run build | |
| - name: Validate links, SEO, and search budgets | |
| if: inputs.rollback_run_id == '' | |
| run: | | |
| pnpm --dir website run check:links | |
| pnpm --dir website run check:search-budget | |
| - name: Validate site code | |
| if: inputs.rollback_run_id == '' | |
| run: | | |
| pnpm --dir website run test:unit | |
| pnpm --dir website run typecheck | |
| dotnet format Humanizer.slnx --verify-no-changes --verbosity diagnostic | |
| - name: Install browser | |
| if: inputs.rollback_run_id == '' | |
| run: pnpm --dir website exec playwright install --with-deps chromium | |
| - name: Validate browser behavior and accessibility | |
| if: inputs.rollback_run_id == '' | |
| run: pnpm --dir website run test:ci | |
| - name: Validate rollback source | |
| id: rollback-source | |
| if: inputs.rollback_run_id != '' | |
| env: | |
| EXPECTED_POINTER_RUN_ATTEMPT: >- | |
| ${{ inputs.expected_pointer_run_attempt }} | |
| EXPECTED_POINTER_RUN_ID: ${{ inputs.expected_pointer_run_id }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ROLLBACK_RUN_ATTEMPT: ${{ inputs.rollback_run_attempt }} | |
| ROLLBACK_RUN_ID: ${{ inputs.rollback_run_id }} | |
| run: | | |
| if [[ ! "$ROLLBACK_RUN_ID" =~ ^[0-9]+$ ]]; then | |
| echo "Rollback run ID must be numeric." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! "$ROLLBACK_RUN_ATTEMPT" =~ ^[0-9]+$ ]]; then | |
| echo "Rollback run attempt must be numeric." >&2 | |
| exit 1 | |
| fi | |
| if [[ -n "$EXPECTED_POINTER_RUN_ID" ]]; then | |
| if [[ "$EXPECTED_POINTER_RUN_ID" != none && | |
| ! "$EXPECTED_POINTER_RUN_ID" =~ ^[0-9]+$ ]]; then | |
| echo "Expected deployment pointer run ID is invalid." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$EXPECTED_POINTER_RUN_ID" == none ]]; then | |
| if [[ "$EXPECTED_POINTER_RUN_ATTEMPT" != none ]]; then | |
| echo "Expected deployment pointer attempt is invalid." >&2 | |
| exit 1 | |
| fi | |
| elif [[ ! "$EXPECTED_POINTER_RUN_ATTEMPT" =~ ^[0-9]+$ ]]; then | |
| echo "Expected deployment pointer attempt is invalid." >&2 | |
| exit 1 | |
| fi | |
| current_pointer_run_id=none | |
| current_pointer_run_attempt=none | |
| pointer="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r ' | |
| [ | |
| .[][] | |
| | select( | |
| .tag_name | |
| | startswith("docs-pages-deployment-v1-") | |
| ) | |
| | select(.draft == false and .immutable == true) | |
| ] | |
| | sort_by(.published_at) | |
| | (last // empty) | |
| | [ | |
| .tag_name, | |
| (.assets | length), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ] | length), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ][0].id // empty), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ][0].name // empty) | |
| ] | |
| | @tsv | |
| ' | |
| )" | |
| if [[ -n "$pointer" ]]; then | |
| IFS=$'\t' read -r pointer_tag asset_count pointer_count \ | |
| pointer_asset_id pointer_asset_name <<<"$pointer" | |
| pointer_key="${pointer_tag#docs-pages-deployment-v1-}" | |
| current_pointer_run_id="${pointer_key%-*}" | |
| current_pointer_run_attempt="${pointer_key##*-}" | |
| if [[ ! "$current_pointer_run_id" =~ ^[0-9]+$ || | |
| ! "$current_pointer_run_attempt" =~ ^[0-9]+$ || | |
| "$asset_count" != 3 || | |
| "$pointer_count" != 1 || | |
| "$pointer_asset_name" != "deployment-${current_pointer_run_id}-${current_pointer_run_attempt}.json" || | |
| ! "$pointer_asset_id" =~ ^[0-9]+$ ]]; then | |
| echo "Current deployment pointer release is invalid." >&2 | |
| exit 1 | |
| fi | |
| gh api \ | |
| -H "Accept: application/octet-stream" \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${pointer_asset_id}" \ | |
| > current-deployment.json | |
| if ! jq -e \ | |
| --argjson deploymentRunId "$current_pointer_run_id" \ | |
| --argjson publicationRunAttempt "$current_pointer_run_attempt" \ | |
| '.schemaVersion == 1 | |
| and .deploymentRunId == $deploymentRunId | |
| and .publicationRunAttempt == $publicationRunAttempt' \ | |
| current-deployment.json >/dev/null; then | |
| echo "Current deployment pointer is invalid." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ "$current_pointer_run_id" != "$EXPECTED_POINTER_RUN_ID" || | |
| "$current_pointer_run_attempt" != "$EXPECTED_POINTER_RUN_ATTEMPT" ]]; then | |
| echo "Recovery was superseded by a newer production result." | |
| echo "recovery_stale=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| source_run="$( | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${ROLLBACK_RUN_ID}/attempts/${ROLLBACK_RUN_ATTEMPT}" \ | |
| --jq '[.conclusion, .event, .head_branch, .path, .head_sha, .run_attempt] | @tsv' | |
| )" | |
| IFS=$'\t' read -r conclusion event branch path source_sha run_attempt \ | |
| <<<"$source_run" | |
| if [[ "$event" != push || | |
| "$branch" != main || | |
| "$run_attempt" != "$ROLLBACK_RUN_ATTEMPT" || | |
| ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "Rollback source must be a successful main push." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$path" == ".github/workflows/docs.yml" ]]; then | |
| if [[ "$conclusion" != success && | |
| "$conclusion" != failure ]]; then | |
| echo "Incomplete workflow attempts are not rollback sources." >&2 | |
| exit 1 | |
| fi | |
| recovery_jobs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${ROLLBACK_RUN_ID}/attempts/${ROLLBACK_RUN_ATTEMPT}/jobs?per_page=100" | | |
| jq -r ' | |
| [ | |
| .[].jobs[] | |
| | select( | |
| .name == "build" | |
| and .conclusion == "success" | |
| ) | |
| | .name | |
| ] | |
| | unique | |
| | sort | |
| | join(",") | |
| ' | |
| )" | |
| if [[ "$recovery_jobs" != "build" ]]; then | |
| echo "Workflow attempt lacks successful archive evidence." >&2 | |
| exit 1 | |
| fi | |
| elif [[ "$conclusion" != success ]]; then | |
| echo "Incomplete legacy workflow attempts are not rollback sources." >&2 | |
| exit 1 | |
| fi | |
| bootstrap_rollback=false | |
| if [[ "$path" == ".github/workflows/docs.yml" ]]; then | |
| : | |
| elif [[ "$path" == ".github/workflows/jekyll-gh-pages.yml" ]]; then | |
| runs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs?branch=main&event=push&status=success&per_page=100" | |
| )" | |
| latest_docs_run="$( | |
| jq -r ' | |
| [ | |
| .[].workflow_runs[] | |
| | select(.path == ".github/workflows/docs.yml") | |
| ] | |
| | sort_by(.created_at) | |
| | last | |
| | .id // empty | |
| ' <<<"$runs" | |
| )" | |
| latest_legacy_run="$( | |
| jq -r ' | |
| [ | |
| .[].workflow_runs[] | |
| | select(.path == ".github/workflows/jekyll-gh-pages.yml") | |
| ] | |
| | sort_by(.created_at) | |
| | last | |
| | .id // empty | |
| ' <<<"$runs" | |
| )" | |
| if [[ -n "$latest_docs_run" || | |
| "$ROLLBACK_RUN_ID" != "$latest_legacy_run" ]]; then | |
| echo "Legacy rollback is allowed only for the latest bootstrap deployment." >&2 | |
| exit 1 | |
| fi | |
| bootstrap_rollback=true | |
| else | |
| echo "Rollback source is not an approved documentation workflow." >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "bootstrap_rollback=${bootstrap_rollback}" | |
| echo "recovery_stale=false" | |
| echo "source_sha=${source_sha}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Restore retained Pages artifact | |
| if: >- | |
| inputs.rollback_run_id != '' && | |
| steps.rollback-source.outputs.recovery_stale != 'true' | |
| env: | |
| BOOTSTRAP_ROLLBACK: ${{ steps.rollback-source.outputs.bootstrap_rollback }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ROLLBACK_RUN_ATTEMPT: ${{ inputs.rollback_run_attempt }} | |
| ROLLBACK_RUN_ID: ${{ inputs.rollback_run_id }} | |
| ROLLBACK_SOURCE_SHA: ${{ steps.rollback-source.outputs.source_sha }} | |
| run: | | |
| mkdir -p rollback | |
| archive_name="github-pages-${ROLLBACK_RUN_ID}-${ROLLBACK_RUN_ATTEMPT}.tar" | |
| checksum_name="${archive_name}.sha256" | |
| release_tag="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r \ | |
| --arg archive "$archive_name" \ | |
| --arg checksum "$checksum_name" \ | |
| --arg releaseTarget "$DOCS_RELEASE_TARGET" \ | |
| --arg source "$ROLLBACK_SOURCE_SHA" ' | |
| [ | |
| .[][] | |
| | select( | |
| .draft == false | |
| and .immutable == true | |
| and ( | |
| .target_commitish == $releaseTarget | |
| or .target_commitish == $source | |
| ) | |
| ) | |
| | select( | |
| ([.assets[].name] | index($archive)) != null | |
| and ([.assets[].name] | index($checksum)) != null | |
| and (.assets | length) == 3 | |
| ) | |
| ] | |
| | sort_by(.published_at) | |
| | (last // empty) | |
| | .tag_name | |
| ' | |
| )" | |
| if [[ "$release_tag" != docs-pages-deployment-v1-* ]]; then | |
| echo "Exact immutable rollback release is unavailable." >&2 | |
| exit 1 | |
| fi | |
| deployment_key="${release_tag#docs-pages-deployment-v1-}" | |
| deployment_run_id="${deployment_key%-*}" | |
| deployment_run_attempt="${deployment_key##*-}" | |
| pointer_name="deployment-${deployment_run_id}-${deployment_run_attempt}.json" | |
| gh release download "$release_tag" \ | |
| --pattern "$archive_name" \ | |
| --pattern "$checksum_name" \ | |
| --pattern "$pointer_name" \ | |
| --dir rollback | |
| ( | |
| cd rollback | |
| sha256sum -c "$checksum_name" | |
| ) | |
| jq -e \ | |
| --argjson archiveRunId "$ROLLBACK_RUN_ID" \ | |
| --argjson archiveRunAttempt "$ROLLBACK_RUN_ATTEMPT" \ | |
| --argjson deploymentRunId "$deployment_run_id" \ | |
| --argjson publicationRunAttempt "$deployment_run_attempt" \ | |
| --arg sourceSha "$ROLLBACK_SOURCE_SHA" \ | |
| '.schemaVersion == 1 | |
| and .archiveRunId == $archiveRunId | |
| and .archiveRunAttempt == $archiveRunAttempt | |
| and .sourceSha == $sourceSha | |
| and .deploymentRunId == $deploymentRunId | |
| and .publicationRunAttempt == $publicationRunAttempt' \ | |
| "rollback/${pointer_name}" >/dev/null | |
| manifest_bootstrap="$( | |
| jq -r '.bootstrapSource // false' "rollback/${pointer_name}" | |
| )" | |
| if [[ "$manifest_bootstrap" == true ]]; then | |
| if [[ "$ROLLBACK_RUN_ATTEMPT" != 1 ]]; then | |
| echo "Rollback bootstrap releases must use attempt one." >&2 | |
| exit 1 | |
| fi | |
| bootstrap_publication="$( | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${deployment_run_id}/attempts/${deployment_run_attempt}" \ | |
| --jq '[.conclusion, .event, .head_branch, .path, .run_attempt] | @tsv' | |
| )" | |
| IFS=$'\t' read -r bootstrap_conclusion bootstrap_event \ | |
| bootstrap_branch bootstrap_path bootstrap_attempt \ | |
| <<<"$bootstrap_publication" | |
| if [[ "$bootstrap_conclusion" != success || | |
| "$bootstrap_event" != push || | |
| "$bootstrap_branch" != main || | |
| "$bootstrap_attempt" != "$deployment_run_attempt" || | |
| ( | |
| "$bootstrap_path" != ".github/workflows/docs.yml" && | |
| "$bootstrap_path" != ".github/workflows/jekyll-gh-pages.yml" | |
| ) ]]; then | |
| echo "Rollback bootstrap publication is invalid." >&2 | |
| exit 1 | |
| fi | |
| elif [[ "$BOOTSTRAP_ROLLBACK" != true ]]; then | |
| phase_attempts="$( | |
| jq -r ' | |
| [ | |
| .stageRunAttempt, | |
| .deployRunAttempt, | |
| .smokeRunAttempt, | |
| .publicationRunAttempt | |
| ] | |
| | @tsv | |
| ' "rollback/${pointer_name}" | |
| )" | |
| IFS=$'\t' read -r stage_run_attempt deploy_run_attempt \ | |
| smoke_run_attempt publication_run_attempt <<<"$phase_attempts" | |
| if [[ "$publication_run_attempt" != "$deployment_run_attempt" || | |
| ! "$stage_run_attempt" =~ ^[0-9]+$ || | |
| ! "$deploy_run_attempt" =~ ^[0-9]+$ || | |
| ! "$smoke_run_attempt" =~ ^[0-9]+$ ]]; then | |
| echo "Rollback publication provenance is invalid." >&2 | |
| exit 1 | |
| fi | |
| publication_run="$( | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${deployment_run_id}/attempts/${publication_run_attempt}" \ | |
| --jq '[.conclusion, .event, .head_branch, .path, .run_attempt] | @tsv' | |
| )" | |
| IFS=$'\t' read -r publication_conclusion publication_event \ | |
| publication_branch publication_path publication_attempt \ | |
| <<<"$publication_run" | |
| if [[ "$publication_branch" != main || | |
| "$publication_path" != ".github/workflows/docs.yml" || | |
| "$publication_attempt" != "$publication_run_attempt" || | |
| ( | |
| "$publication_event" != push && | |
| "$publication_event" != workflow_dispatch | |
| ) || | |
| ( | |
| "$publication_conclusion" != success && | |
| "$publication_conclusion" != failure | |
| ) ]]; then | |
| echo "Rollback publication attempt is invalid." >&2 | |
| exit 1 | |
| fi | |
| for phase in \ | |
| "stage-pages-release:${stage_run_attempt}" \ | |
| "deploy:${deploy_run_attempt}" \ | |
| "production-smoke:${smoke_run_attempt}"; do | |
| phase_name="${phase%%:*}" | |
| phase_attempt="${phase##*:}" | |
| phase_jobs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${deployment_run_id}/attempts/${phase_attempt}/jobs?per_page=100" | | |
| jq -r --arg name "$phase_name" ' | |
| [ | |
| .[].jobs[] | |
| | select( | |
| .name == $name | |
| and .conclusion == "success" | |
| ) | |
| | .name | |
| ] | |
| | unique | |
| | join(",") | |
| ' | |
| )" | |
| if [[ "$phase_jobs" != "$phase_name" ]]; then | |
| echo "Rollback release lacks exact successful ${phase_name} evidence." >&2 | |
| exit 1 | |
| fi | |
| done | |
| fi | |
| - name: Extract retained Pages artifact for verification | |
| if: >- | |
| inputs.rollback_run_id != '' && | |
| steps.rollback-source.outputs.recovery_stale != 'true' | |
| env: | |
| ROLLBACK_RUN_ATTEMPT: ${{ inputs.rollback_run_attempt }} | |
| ROLLBACK_RUN_ID: ${{ inputs.rollback_run_id }} | |
| run: | | |
| mkdir -p website/build | |
| archive_name="github-pages-${ROLLBACK_RUN_ID}-${ROLLBACK_RUN_ATTEMPT}.tar" | |
| tar -xf \ | |
| "rollback/${archive_name}" \ | |
| -C website/build | |
| - name: Write deployment identity | |
| id: deployment-identity | |
| if: steps.rollback-source.outputs.recovery_stale != 'true' | |
| env: | |
| ARCHIVE_RUN_ATTEMPT: >- | |
| ${{ | |
| inputs.rollback_run_id != '' && | |
| inputs.rollback_run_attempt || | |
| github.run_attempt | |
| }} | |
| ARCHIVE_RUN_ID: >- | |
| ${{ | |
| inputs.rollback_run_id != '' && | |
| inputs.rollback_run_id || | |
| github.run_id | |
| }} | |
| SOURCE_SHA: >- | |
| ${{ | |
| inputs.rollback_run_id != '' && | |
| steps.rollback-source.outputs.source_sha || | |
| github.sha | |
| }} | |
| run: | | |
| if [[ ! "$ARCHIVE_RUN_ID" =~ ^[0-9]+$ || | |
| ! "$ARCHIVE_RUN_ATTEMPT" =~ ^[0-9]+$ ]]; then | |
| echo "Deployment archive identity is invalid." >&2 | |
| exit 1 | |
| fi | |
| printf \ | |
| '{"sourceSha":"%s","archiveRunId":%s,"archiveRunAttempt":%s}\n' \ | |
| "$SOURCE_SHA" \ | |
| "$ARCHIVE_RUN_ID" \ | |
| "$ARCHIVE_RUN_ATTEMPT" \ | |
| > website/build/deployment.json | |
| echo "archive_run_id=${ARCHIVE_RUN_ID}" >> "$GITHUB_OUTPUT" | |
| echo "archive_run_attempt=${ARCHIVE_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" | |
| echo "source_sha=${SOURCE_SHA}" >> "$GITHUB_OUTPUT" | |
| - name: Verify complete Pages artifact | |
| if: >- | |
| steps.rollback-source.outputs.recovery_stale != 'true' && | |
| steps.rollback-source.outputs.bootstrap_rollback != 'true' | |
| run: pnpm --dir website run check:artifact | |
| - name: Verify bootstrap rollback artifact | |
| if: >- | |
| steps.rollback-source.outputs.recovery_stale != 'true' && | |
| steps.rollback-source.outputs.bootstrap_rollback == 'true' | |
| run: >- | |
| node website/scripts/verify-pages-artifact.mjs | |
| website/build | |
| --bootstrap-rollback | |
| - name: Upload GitHub Pages artifact | |
| if: >- | |
| steps.rollback-source.outputs.recovery_stale != 'true' && | |
| ( | |
| github.event_name == 'push' || | |
| inputs.rollback_run_id != '' | |
| ) | |
| uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 | |
| with: | |
| path: website/build | |
| include-hidden-files: true | |
| retention-days: 7 | |
| retain-pages-artifacts: | |
| if: github.event_name == 'push' | |
| outputs: | |
| prior_deployment_run_attempt: ${{ steps.prior.outputs.deployment_run_attempt }} | |
| prior_deployment_run_id: ${{ steps.prior.outputs.deployment_run_id }} | |
| prior_run_attempt: ${{ steps.prior.outputs.run_attempt }} | |
| prior_run_id: ${{ steps.prior.outputs.run_id }} | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build | |
| - validate | |
| permissions: | |
| actions: read | |
| contents: write | |
| steps: | |
| - name: Locate the currently deployable Pages run | |
| id: prior | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| prior= | |
| pointer="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r ' | |
| [ | |
| .[][] | |
| | select( | |
| .tag_name | |
| | startswith("docs-pages-deployment-v1-") | |
| ) | |
| | select(.draft == false and .immutable == true) | |
| ] | |
| | sort_by(.published_at) | |
| | (last // empty) | |
| | [ | |
| .tag_name, | |
| (.assets | length), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ] | length), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ][0].id // empty), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ][0].name // empty) | |
| ] | |
| | @tsv | |
| ' | |
| )" | |
| if [[ -n "$pointer" ]]; then | |
| IFS=$'\t' read -r pointer_tag asset_count pointer_count \ | |
| pointer_asset_id pointer_asset_name <<<"$pointer" | |
| pointer_key="${pointer_tag#docs-pages-deployment-v1-}" | |
| pointer_run_id="${pointer_key%-*}" | |
| pointer_run_attempt="${pointer_key##*-}" | |
| if [[ ! "$pointer_run_id" =~ ^[0-9]+$ || | |
| ! "$pointer_run_attempt" =~ ^[0-9]+$ || | |
| "$asset_count" != 3 || | |
| "$pointer_count" != 1 || | |
| "$pointer_asset_name" != "deployment-${pointer_run_id}-${pointer_run_attempt}.json" || | |
| ! "$pointer_asset_id" =~ ^[0-9]+$ ]]; then | |
| echo "Latest deployment pointer release is invalid." >&2 | |
| exit 1 | |
| fi | |
| gh api \ | |
| -H "Accept: application/octet-stream" \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${pointer_asset_id}" \ | |
| > prior-deployment.json | |
| prior="$( | |
| jq -r ' | |
| select(.schemaVersion == 1) | |
| | [ | |
| .archiveRunId, | |
| .archiveRunAttempt, | |
| .sourceSha, | |
| ".github/workflows/docs.yml", | |
| .deploymentRunId, | |
| (.stageRunAttempt // 0), | |
| (.deployRunAttempt // 0), | |
| (.smokeRunAttempt // 0), | |
| .publicationRunAttempt, | |
| (.bootstrapSource // false) | |
| ] | |
| | @tsv | |
| ' prior-deployment.json | |
| )" | |
| IFS=$'\t' read -r run_id run_attempt source_sha source_path \ | |
| deployment_run_id stage_run_attempt deploy_run_attempt \ | |
| smoke_run_attempt publication_run_attempt bootstrap_source \ | |
| <<<"$prior" | |
| deployment_run_attempt="$publication_run_attempt" | |
| if [[ ! "$run_id" =~ ^[0-9]+$ || | |
| ! "$run_attempt" =~ ^[0-9]+$ || | |
| ! "$source_sha" =~ ^[0-9a-f]{40}$ || | |
| "$deployment_run_id" != "$pointer_run_id" || | |
| "$publication_run_attempt" != "$pointer_run_attempt" || | |
| ( | |
| "$bootstrap_source" == true && | |
| "$run_attempt" != 1 | |
| ) || | |
| ( | |
| "$bootstrap_source" != true && | |
| ( | |
| ! "$stage_run_attempt" =~ ^[0-9]+$ || | |
| "$stage_run_attempt" == 0 || | |
| ! "$deploy_run_attempt" =~ ^[0-9]+$ || | |
| "$deploy_run_attempt" == 0 || | |
| ! "$smoke_run_attempt" =~ ^[0-9]+$ || | |
| "$smoke_run_attempt" == 0 | |
| ) | |
| ) ]]; then | |
| echo "Latest deployment pointer is invalid." >&2 | |
| exit 1 | |
| fi | |
| pointer_run="$( | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${deployment_run_id}/attempts/${deployment_run_attempt}" \ | |
| --jq '[.conclusion, .event, .head_branch, .path, .run_attempt] | @tsv' | |
| )" | |
| IFS=$'\t' read -r pointer_conclusion pointer_event \ | |
| pointer_branch pointer_path pointer_attempt <<<"$pointer_run" | |
| if [[ "$pointer_branch" != main || | |
| "$pointer_attempt" != "$deployment_run_attempt" || | |
| ( | |
| "$pointer_event" != push && | |
| "$pointer_event" != workflow_dispatch | |
| ) || | |
| ( | |
| "$pointer_path" != ".github/workflows/docs.yml" && | |
| "$pointer_path" != ".github/workflows/jekyll-gh-pages.yml" | |
| ) || | |
| ( | |
| "$pointer_path" == ".github/workflows/jekyll-gh-pages.yml" && | |
| "$pointer_event" != push | |
| ) ]]; then | |
| echo "Latest deployment pointer is not backed by a successful main documentation run." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$pointer_conclusion" != success && | |
| "$pointer_conclusion" != failure ]]; then | |
| echo "Incomplete workflow attempts are not deployment pointers." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$bootstrap_source" == true ]]; then | |
| if [[ "$pointer_conclusion" != success ]]; then | |
| echo "Bootstrap deployment pointers must be successful." >&2 | |
| exit 1 | |
| fi | |
| elif [[ "$pointer_path" == ".github/workflows/docs.yml" ]]; then | |
| for phase in \ | |
| "stage-pages-release:${stage_run_attempt}" \ | |
| "deploy:${deploy_run_attempt}" \ | |
| "production-smoke:${smoke_run_attempt}"; do | |
| phase_name="${phase%%:*}" | |
| phase_attempt="${phase##*:}" | |
| phase_jobs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${deployment_run_id}/attempts/${phase_attempt}/jobs?per_page=100" | | |
| jq -r --arg name "$phase_name" ' | |
| [ | |
| .[].jobs[] | |
| | select( | |
| .name == $name | |
| and .conclusion == "success" | |
| ) | |
| | .name | |
| ] | |
| | unique | |
| | join(",") | |
| ' | |
| )" | |
| if [[ "$phase_jobs" != "$phase_name" ]]; then | |
| echo "Deployment pointer lacks exact successful ${phase_name} evidence." >&2 | |
| exit 1 | |
| fi | |
| done | |
| elif [[ "$pointer_conclusion" != success ]]; then | |
| echo "Failed legacy workflow attempts are not deployment pointers." >&2 | |
| exit 1 | |
| fi | |
| archive_run="$( | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/attempts/${run_attempt}" \ | |
| --jq '[.conclusion, .event, .head_branch, .path, .head_sha, .run_attempt] | @tsv' | |
| )" | |
| IFS=$'\t' read -r archive_conclusion archive_event \ | |
| archive_branch archive_path archive_sha archive_attempt \ | |
| <<<"$archive_run" | |
| if [[ "$archive_event" != push || | |
| "$archive_branch" != main || | |
| "$archive_sha" != "$source_sha" || | |
| "$archive_attempt" != "$run_attempt" || | |
| ( | |
| "$archive_path" != ".github/workflows/docs.yml" && | |
| "$archive_path" != ".github/workflows/jekyll-gh-pages.yml" | |
| ) ]]; then | |
| echo "Latest deployment pointer names an invalid archive source." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$archive_path" == ".github/workflows/docs.yml" ]]; then | |
| if [[ "$archive_conclusion" != success && | |
| "$archive_conclusion" != failure ]]; then | |
| echo "Incomplete workflow attempts are not archive sources." >&2 | |
| exit 1 | |
| fi | |
| archive_jobs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/attempts/${run_attempt}/jobs?per_page=100" | | |
| jq -r ' | |
| [ | |
| .[].jobs[] | |
| | select( | |
| .name == "build" | |
| and .conclusion == "success" | |
| ) | |
| | .name | |
| ] | |
| | unique | |
| | sort | |
| | join(",") | |
| ' | |
| )" | |
| if [[ "$archive_jobs" != "build" ]]; then | |
| echo "Latest deployment pointer lacks successful archive evidence." >&2 | |
| exit 1 | |
| fi | |
| elif [[ "$archive_conclusion" != success ]]; then | |
| echo "Incomplete legacy workflow attempts are not archive sources." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$archive_path" == ".github/workflows/jekyll-gh-pages.yml" ]]; then | |
| bootstrap_runs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs?branch=main&event=push&status=success&per_page=100" | |
| )" | |
| latest_docs_run="$( | |
| jq -r ' | |
| [ | |
| .[].workflow_runs[] | |
| | select(.path == ".github/workflows/docs.yml") | |
| ] | |
| | sort_by(.created_at) | |
| | last | |
| | .id // empty | |
| ' <<<"$bootstrap_runs" | |
| )" | |
| latest_legacy_run="$( | |
| jq -r ' | |
| [ | |
| .[].workflow_runs[] | |
| | select(.path == ".github/workflows/jekyll-gh-pages.yml") | |
| ] | |
| | sort_by(.created_at) | |
| | last | |
| | .id // empty | |
| ' <<<"$bootstrap_runs" | |
| )" | |
| if [[ -n "$latest_docs_run" || | |
| "$run_id" != "$latest_legacy_run" ]]; then | |
| echo "Legacy deployment pointer is no longer a valid bootstrap source." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| fi | |
| if [[ -z "$prior" ]]; then | |
| runs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs?branch=main&event=push&status=success&per_page=100" | |
| )" | |
| prior="$( | |
| jq -r ' | |
| [ | |
| .[].workflow_runs[] | |
| | select( | |
| .path == ".github/workflows/docs.yml" | |
| and .run_attempt == 1 | |
| ) | |
| ] | |
| | sort_by(.created_at) | |
| | (last // empty) | |
| | [.id, .run_attempt, .head_sha, .path] | |
| | @tsv | |
| ' <<<"$runs" | |
| )" | |
| if [[ -z "$prior" ]]; then | |
| prior="$( | |
| jq -r ' | |
| . as $runs | |
| | if any( | |
| $runs[].workflow_runs[]; | |
| .path == ".github/workflows/docs.yml" | |
| ) then | |
| empty | |
| else | |
| [ | |
| $runs[].workflow_runs[] | |
| | select( | |
| .path == ".github/workflows/jekyll-gh-pages.yml" | |
| and .run_attempt == 1 | |
| ) | |
| ] | |
| | sort_by(.created_at) | |
| | (last // empty) | |
| | [.id, .run_attempt, .head_sha, .path] | |
| | @tsv | |
| end | |
| ' <<<"$runs" | |
| )" | |
| fi | |
| IFS=$'\t' read -r run_id run_attempt source_sha source_path \ | |
| <<<"$prior" | |
| deployment_run_id="$run_id" | |
| deployment_run_attempt="$run_attempt" | |
| fi | |
| if [[ ! "$run_id" =~ ^[0-9]+$ || | |
| ! "$run_attempt" =~ ^[0-9]+$ || | |
| ! "$deployment_run_id" =~ ^[0-9]+$ || | |
| ! "$deployment_run_attempt" =~ ^[0-9]+$ || | |
| ! "$source_sha" =~ ^[0-9a-f]{40}$ || | |
| ( | |
| "$source_path" != ".github/workflows/docs.yml" && | |
| "$source_path" != ".github/workflows/jekyll-gh-pages.yml" | |
| ) ]]; then | |
| echo "No successful main documentation deployment is available to retain." >&2 | |
| exit 1 | |
| fi | |
| echo "Retaining prior deployment ${run_id} from ${source_path}." | |
| { | |
| echo "deployment_run_attempt=${deployment_run_attempt}" | |
| echo "deployment_run_id=${deployment_run_id}" | |
| echo "run_attempt=${run_attempt}" | |
| echo "run_id=${run_id}" | |
| echo "source_sha=${source_sha}" | |
| echo "source_path=${source_path}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Inspect retained prior artifact | |
| id: prior-archive | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PRIOR_DEPLOYMENT_RUN_ATTEMPT: ${{ steps.prior.outputs.deployment_run_attempt }} | |
| PRIOR_DEPLOYMENT_RUN_ID: ${{ steps.prior.outputs.deployment_run_id }} | |
| PRIOR_RUN_ATTEMPT: ${{ steps.prior.outputs.run_attempt }} | |
| PRIOR_RUN_ID: ${{ steps.prior.outputs.run_id }} | |
| PRIOR_SOURCE_SHA: ${{ steps.prior.outputs.source_sha }} | |
| run: | | |
| archive_tag="docs-pages-deployment-v1-${PRIOR_DEPLOYMENT_RUN_ID}-${PRIOR_DEPLOYMENT_RUN_ATTEMPT}" | |
| archive_name="github-pages-${PRIOR_RUN_ID}-${PRIOR_RUN_ATTEMPT}.tar" | |
| checksum_name="${archive_name}.sha256" | |
| pointer_name="deployment-${PRIOR_DEPLOYMENT_RUN_ID}-${PRIOR_DEPLOYMENT_RUN_ATTEMPT}.json" | |
| release="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r --arg tag "$archive_tag" ' | |
| [ | |
| .[][] | |
| | select(.tag_name == $tag) | |
| ] | |
| | if length > 1 then | |
| error("duplicate prior rollback release") | |
| else | |
| (first // empty) | |
| end | |
| | [ | |
| .immutable, | |
| .target_commitish, | |
| (.assets | length), | |
| ([.assets[].name] | sort | join(",")) | |
| ] | |
| | @tsv | |
| ' | |
| )" | |
| if [[ -n "$release" ]]; then | |
| IFS=$'\t' read -r immutable target asset_count asset_names \ | |
| <<<"$release" | |
| if [[ "$immutable" != true || | |
| ( | |
| "$target" != "$DOCS_RELEASE_TARGET" && | |
| "$target" != "$PRIOR_SOURCE_SHA" | |
| ) || | |
| "$asset_count" != 3 || | |
| "$asset_names" != "${pointer_name},${archive_name},${checksum_name}" ]]; then | |
| echo "Prior rollback release is invalid." >&2 | |
| exit 1 | |
| fi | |
| mkdir -p prior-archive | |
| gh release download "$archive_tag" \ | |
| --pattern "$archive_name" \ | |
| --pattern "$checksum_name" \ | |
| --pattern "$pointer_name" \ | |
| --dir prior-archive | |
| ( | |
| cd prior-archive | |
| sha256sum -c "$checksum_name" | |
| ) | |
| jq -e \ | |
| --argjson archiveRunId "$PRIOR_RUN_ID" \ | |
| --argjson archiveRunAttempt "$PRIOR_RUN_ATTEMPT" \ | |
| --arg sourceSha "$PRIOR_SOURCE_SHA" \ | |
| --argjson deploymentRunId "$PRIOR_DEPLOYMENT_RUN_ID" \ | |
| --argjson publicationRunAttempt "$PRIOR_DEPLOYMENT_RUN_ATTEMPT" \ | |
| '.schemaVersion == 1 | |
| and .archiveRunId == $archiveRunId | |
| and .archiveRunAttempt == $archiveRunAttempt | |
| and .sourceSha == $sourceSha | |
| and .deploymentRunId == $deploymentRunId | |
| and .publicationRunAttempt == $publicationRunAttempt' \ | |
| "prior-archive/${pointer_name}" >/dev/null | |
| echo "download=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "download=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Download prior Pages artifact | |
| if: steps.prior-archive.outputs.download == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: github-pages | |
| path: prior-download | |
| repository: ${{ github.repository }} | |
| run-id: ${{ steps.prior.outputs.run_id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Retain and verify prior Pages artifact | |
| if: steps.prior-archive.outputs.download == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PRIOR_DEPLOYMENT_RUN_ATTEMPT: ${{ steps.prior.outputs.deployment_run_attempt }} | |
| PRIOR_DEPLOYMENT_RUN_ID: ${{ steps.prior.outputs.deployment_run_id }} | |
| PRIOR_RUN_ATTEMPT: ${{ steps.prior.outputs.run_attempt }} | |
| PRIOR_RUN_ID: ${{ steps.prior.outputs.run_id }} | |
| PRIOR_SOURCE_SHA: ${{ steps.prior.outputs.source_sha }} | |
| PRIOR_SOURCE_PATH: ${{ steps.prior.outputs.source_path }} | |
| run: | | |
| mkdir -p prior-archive | |
| archive_tag="docs-pages-deployment-v1-${PRIOR_DEPLOYMENT_RUN_ID}-${PRIOR_DEPLOYMENT_RUN_ATTEMPT}" | |
| archive_name="github-pages-${PRIOR_RUN_ID}-${PRIOR_RUN_ATTEMPT}.tar" | |
| checksum_name="${archive_name}.sha256" | |
| pointer_name="deployment-${PRIOR_DEPLOYMENT_RUN_ID}-${PRIOR_DEPLOYMENT_RUN_ATTEMPT}.json" | |
| if [[ "$PRIOR_SOURCE_PATH" == ".github/workflows/docs.yml" ]]; then | |
| if [[ "$PRIOR_RUN_ATTEMPT" != 1 ]]; then | |
| echo "A rerun pre-staging artifact cannot prove its exact attempt." >&2 | |
| exit 1 | |
| fi | |
| if ! tar -xOf prior-download/artifact.tar ./deployment.json \ | |
| > prior-download/deployment.json; then | |
| echo "Prior Pages artifact has no deployment identity." >&2 | |
| exit 1 | |
| fi | |
| if ! jq -e \ | |
| --arg sourceSha "$PRIOR_SOURCE_SHA" \ | |
| '.sourceSha == $sourceSha' \ | |
| prior-download/deployment.json >/dev/null; then | |
| echo "Prior Pages artifact does not match its selected workflow source." >&2 | |
| exit 1 | |
| fi | |
| elif [[ "$PRIOR_SOURCE_PATH" == ".github/workflows/jekyll-gh-pages.yml" ]]; then | |
| if [[ "$PRIOR_RUN_ATTEMPT" != 1 ]]; then | |
| echo "A rerun legacy artifact cannot prove its exact attempt." >&2 | |
| exit 1 | |
| fi | |
| else | |
| echo "Prior Pages artifact has an invalid workflow source." >&2 | |
| exit 1 | |
| fi | |
| mv prior-download/artifact.tar "prior-archive/${archive_name}" | |
| ( | |
| cd prior-archive | |
| sha256sum "$archive_name" > "$checksum_name" | |
| sha256sum -c "$checksum_name" | |
| ) | |
| printf \ | |
| '{"schemaVersion":1,"archiveRunId":%s,"archiveRunAttempt":%s,"sourceSha":"%s","deploymentRunId":%s,"publicationRunAttempt":%s,"bootstrapSource":true}\n' \ | |
| "$PRIOR_RUN_ID" \ | |
| "$PRIOR_RUN_ATTEMPT" \ | |
| "$PRIOR_SOURCE_SHA" \ | |
| "$PRIOR_DEPLOYMENT_RUN_ID" \ | |
| "$PRIOR_DEPLOYMENT_RUN_ATTEMPT" \ | |
| > "prior-archive/${pointer_name}" | |
| gh release create "$archive_tag" \ | |
| "prior-archive/${archive_name}" \ | |
| "prior-archive/${checksum_name}" \ | |
| "prior-archive/${pointer_name}" \ | |
| --latest=false \ | |
| --prerelease \ | |
| --target "$DOCS_RELEASE_TARGET" \ | |
| --title "Documentation deployment ${PRIOR_DEPLOYMENT_RUN_ID}/${PRIOR_DEPLOYMENT_RUN_ATTEMPT}" \ | |
| --notes "Automation-owned Pages artifact for exact rollback." | |
| mkdir prior-verify | |
| gh release download "$archive_tag" \ | |
| --pattern "$archive_name" \ | |
| --pattern "$checksum_name" \ | |
| --pattern "$pointer_name" \ | |
| --dir prior-verify | |
| ( | |
| cd prior-verify | |
| sha256sum -c "$checksum_name" | |
| ) | |
| stage-pages-release: | |
| if: >- | |
| always() && | |
| needs.build.result == 'success' && | |
| needs.build.outputs.recovery_stale != 'true' && | |
| ( | |
| inputs.rollback_run_id != '' || | |
| ( | |
| github.event_name == 'push' && | |
| needs.retain-pages-artifacts.result == 'success' | |
| ) | |
| ) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build | |
| - retain-pages-artifacts | |
| permissions: | |
| actions: read | |
| contents: write | |
| steps: | |
| - name: Download candidate Pages artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: github-pages | |
| path: candidate | |
| - name: Stage and verify mutable deployment release | |
| env: | |
| ARCHIVE_RUN_ATTEMPT: ${{ needs.build.outputs.archive_run_attempt }} | |
| ARCHIVE_RUN_ID: ${{ needs.build.outputs.archive_run_id }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SOURCE_SHA: ${{ needs.build.outputs.source_sha }} | |
| run: | | |
| if [[ ! "$ARCHIVE_RUN_ID" =~ ^[0-9]+$ || | |
| ! "$ARCHIVE_RUN_ATTEMPT" =~ ^[0-9]+$ || | |
| ! "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "Candidate deployment identity is invalid." >&2 | |
| exit 1 | |
| fi | |
| release_tag="docs-pages-candidate-v1-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| archive_name="github-pages-${ARCHIVE_RUN_ID}-${ARCHIVE_RUN_ATTEMPT}.tar" | |
| checksum_name="${archive_name}.sha256" | |
| pointer_name="deployment-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json" | |
| mv candidate/artifact.tar "candidate/${archive_name}" | |
| ( | |
| cd candidate | |
| sha256sum "$archive_name" > "$checksum_name" | |
| sha256sum -c "$checksum_name" | |
| ) | |
| printf \ | |
| '{"schemaVersion":1,"archiveRunId":%s,"archiveRunAttempt":%s,"sourceSha":"%s","deploymentRunId":%s,"stageRunAttempt":%s}\n' \ | |
| "$ARCHIVE_RUN_ID" \ | |
| "$ARCHIVE_RUN_ATTEMPT" \ | |
| "$SOURCE_SHA" \ | |
| "$GITHUB_RUN_ID" \ | |
| "$GITHUB_RUN_ATTEMPT" \ | |
| > "candidate/${pointer_name}" | |
| find_candidate_release() { | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r --arg tag "$release_tag" ' | |
| [ | |
| .[][] | |
| | select(.tag_name == $tag) | |
| ] | |
| | if length > 1 then | |
| error("duplicate candidate release") | |
| else | |
| (first // empty) | |
| end | |
| | [ | |
| .id, | |
| .draft, | |
| .immutable, | |
| .target_commitish, | |
| (.assets | length), | |
| ([.assets[].name] | sort | join(",")) | |
| ] | |
| | @tsv | |
| ' | |
| } | |
| release="$(find_candidate_release)" | |
| if [[ -z "$release" ]]; then | |
| gh release create "$release_tag" \ | |
| "candidate/${archive_name}" \ | |
| "candidate/${checksum_name}" \ | |
| "candidate/${pointer_name}" \ | |
| --draft \ | |
| --latest=false \ | |
| --prerelease \ | |
| --target "$DOCS_RELEASE_TARGET" \ | |
| --title "Documentation deployment ${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}" \ | |
| --notes "Automation-owned exact deployment candidate." | |
| for attempt in {1..10}; do | |
| release="$(find_candidate_release)" | |
| [[ -n "$release" ]] && break | |
| sleep 1 | |
| done | |
| fi | |
| if [[ -z "$release" ]]; then | |
| echo "Candidate release did not become visible." >&2 | |
| exit 1 | |
| fi | |
| IFS=$'\t' read -r release_id draft immutable target \ | |
| asset_count asset_names <<<"$release" | |
| if [[ ! "$release_id" =~ ^[0-9]+$ || | |
| "$target" != "$DOCS_RELEASE_TARGET" || | |
| "$asset_count" != 3 || | |
| "$asset_names" != "${pointer_name},${archive_name},${checksum_name}" || | |
| ! ( | |
| ( | |
| "$draft" == true && | |
| "$immutable" == false | |
| ) || | |
| ( | |
| "$draft" == false && | |
| "$immutable" == true | |
| ) | |
| ) ]]; then | |
| echo "Candidate deployment release is invalid." >&2 | |
| exit 1 | |
| fi | |
| mkdir candidate-verify | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \ | |
| --jq '.assets[] | [.name, .id] | @tsv' | | |
| while IFS=$'\t' read -r name asset_id; do | |
| gh api \ | |
| -H "Accept: application/octet-stream" \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" \ | |
| > "candidate-verify/${name}" | |
| done | |
| ( | |
| cd candidate-verify | |
| sha256sum -c "$checksum_name" | |
| ) | |
| cmp "candidate/${pointer_name}" "candidate-verify/${pointer_name}" | |
| mkdir candidate-tree verify-tree | |
| tar -xf "candidate/${archive_name}" -C candidate-tree | |
| tar -xf "candidate-verify/${archive_name}" -C verify-tree | |
| diff --no-dereference --recursive candidate-tree verify-tree | |
| deploy: | |
| if: >- | |
| always() && | |
| needs.build.result == 'success' && | |
| needs.stage-pages-release.result == 'success' && | |
| needs.build.outputs.recovery_stale != 'true' && | |
| ( | |
| inputs.rollback_run_id != '' || | |
| ( | |
| github.event_name == 'push' && | |
| needs.retain-pages-artifacts.result == 'success' | |
| ) | |
| ) | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build | |
| - retain-pages-artifacts | |
| - stage-pages-release | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pages: write | |
| steps: | |
| - name: Configure GitHub Pages | |
| uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 | |
| - name: Deploy complete artifact | |
| id: deployment | |
| uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 | |
| production-smoke: | |
| if: >- | |
| always() && | |
| needs.build.result == 'success' && | |
| needs.deploy.result == 'success' | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build | |
| - deploy | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout production smoke tests | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 | |
| with: | |
| package_json_file: website/package.json | |
| - name: Setup Node | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version-file: website/.node-version | |
| - name: Install production smoke dependencies | |
| run: | | |
| pnpm --dir website install --frozen-lockfile | |
| pnpm --dir website exec playwright install --with-deps chromium | |
| - name: Verify production custom domain | |
| env: | |
| HUMANIZER_DOCS_BASE_URL: https://humanizr.net | |
| HUMANIZER_DOCS_BOOTSTRAP_ROLLBACK: >- | |
| ${{ needs.build.outputs.bootstrap_rollback }} | |
| HUMANIZER_DOCS_EXPECTED_SHA: ${{ needs.build.outputs.source_sha }} | |
| run: pnpm --dir website run test:production | |
| record-production-deployment: | |
| if: >- | |
| always() && | |
| needs.build.result == 'success' && | |
| needs.deploy.result == 'success' && | |
| needs.production-smoke.result == 'success' | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build | |
| - deploy | |
| - production-smoke | |
| - stage-pages-release | |
| permissions: | |
| actions: read | |
| contents: write | |
| steps: | |
| - name: Record exact currently deployable archive | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SOURCE_SHA: ${{ needs.build.outputs.source_sha }} | |
| run: | | |
| if [[ ! "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "Deployment identity is invalid." >&2 | |
| exit 1 | |
| fi | |
| release_prefix="docs-pages-candidate-v1-${GITHUB_RUN_ID}-" | |
| release="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r --arg prefix "$release_prefix" ' | |
| [ | |
| .[][] | |
| | select(.tag_name | startswith($prefix)) | |
| | . + { | |
| deployment_attempt: ( | |
| .tag_name | |
| | ltrimstr($prefix) | |
| | select(test("^[0-9]+$")) | |
| | tonumber | |
| ) | |
| } | |
| ] | |
| | sort_by(.deployment_attempt) | |
| | (last // empty) | |
| | [ | |
| .tag_name, | |
| .deployment_attempt, | |
| .id, | |
| .draft, | |
| .immutable, | |
| .target_commitish, | |
| (.assets | length), | |
| ([.assets[].name] | sort | join(",")), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ] | length), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ][0].id // empty) | |
| ] | |
| | @tsv | |
| ' | |
| )" | |
| IFS=$'\t' read -r source_tag source_attempt release_id draft \ | |
| immutable target asset_count asset_names manifest_count \ | |
| manifest_asset_id <<<"$release" | |
| if [[ "$source_tag" != "${release_prefix}${source_attempt}" || | |
| ! "$source_attempt" =~ ^[0-9]+$ || | |
| ! "$release_id" =~ ^[0-9]+$ || | |
| "$target" != "$DOCS_RELEASE_TARGET" || | |
| "$asset_count" != 3 || | |
| "$manifest_count" != 1 || | |
| ! "$manifest_asset_id" =~ ^[0-9]+$ || | |
| ! ( | |
| ( | |
| "$draft" == true && | |
| "$immutable" == false | |
| ) || | |
| ( | |
| "$draft" == false && | |
| "$immutable" == true | |
| ) | |
| ) ]]; then | |
| echo "Deployment candidate release is invalid." >&2 | |
| exit 1 | |
| fi | |
| gh api \ | |
| -H "Accept: application/octet-stream" \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${manifest_asset_id}" \ | |
| > source-deployment.json | |
| source_manifest="$( | |
| jq -r ' | |
| select(.schemaVersion == 1) | |
| | [ | |
| .archiveRunId, | |
| .archiveRunAttempt, | |
| .sourceSha, | |
| .deploymentRunId, | |
| .stageRunAttempt | |
| ] | |
| | @tsv | |
| ' source-deployment.json | |
| )" | |
| IFS=$'\t' read -r archive_run_id archive_run_attempt source_sha \ | |
| deployment_run_id stage_run_attempt <<<"$source_manifest" | |
| archive_name="github-pages-${archive_run_id}-${archive_run_attempt}.tar" | |
| checksum_name="${archive_name}.sha256" | |
| source_pointer_name="deployment-${GITHUB_RUN_ID}-${source_attempt}.json" | |
| if [[ ! "$archive_run_id" =~ ^[0-9]+$ || | |
| ! "$archive_run_attempt" =~ ^[0-9]+$ || | |
| "$source_sha" != "$SOURCE_SHA" || | |
| "$deployment_run_id" != "$GITHUB_RUN_ID" || | |
| "$stage_run_attempt" != "$source_attempt" || | |
| "$asset_names" != "${source_pointer_name},${archive_name},${checksum_name}" ]]; then | |
| echo "Deployment candidate manifest is invalid." >&2 | |
| exit 1 | |
| fi | |
| stage_jobs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${stage_run_attempt}/jobs?per_page=100" | | |
| jq -r ' | |
| [ | |
| .[].jobs[] | |
| | select( | |
| .name == "stage-pages-release" | |
| and .conclusion == "success" | |
| ) | |
| | .name | |
| ] | |
| | unique | |
| | join(",") | |
| ' | |
| )" | |
| if [[ "$stage_jobs" != "stage-pages-release" ]]; then | |
| echo "Deployment candidate lacks successful staging evidence." >&2 | |
| exit 1 | |
| fi | |
| deploy_run_attempt= | |
| smoke_run_attempt= | |
| for ((attempt = 1; attempt <= GITHUB_RUN_ATTEMPT; attempt++)); do | |
| successful_jobs="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${attempt}/jobs?per_page=100" | | |
| jq -r ' | |
| [ | |
| .[].jobs[] | |
| | select( | |
| ( | |
| .name == "deploy" | |
| or .name == "production-smoke" | |
| ) | |
| and .conclusion == "success" | |
| ) | |
| | .name | |
| ] | |
| | unique | |
| | sort | |
| | join(",") | |
| ' | |
| )" | |
| if [[ "$successful_jobs" == "deploy" || | |
| "$successful_jobs" == "deploy,production-smoke" ]]; then | |
| deploy_run_attempt="$attempt" | |
| fi | |
| if [[ "$successful_jobs" == "deploy,production-smoke" || | |
| "$successful_jobs" == "production-smoke" ]]; then | |
| smoke_run_attempt="$attempt" | |
| fi | |
| done | |
| if [[ ! "$deploy_run_attempt" =~ ^[0-9]+$ || | |
| ! "$smoke_run_attempt" =~ ^[0-9]+$ ]]; then | |
| echo "Deployment candidate lacks successful production evidence." >&2 | |
| exit 1 | |
| fi | |
| curl \ | |
| --fail \ | |
| --header "Cache-Control: no-cache" \ | |
| --retry 4 \ | |
| --retry-all-errors \ | |
| --retry-delay 2 \ | |
| --show-error \ | |
| --silent \ | |
| "https://humanizr.net/deployment.json?run=${deployment_run_id}&attempt=${GITHUB_RUN_ATTEMPT}" \ | |
| > current-production.json | |
| if ! jq -e \ | |
| --arg sourceSha "$SOURCE_SHA" \ | |
| --argjson archiveRunId "$archive_run_id" \ | |
| --argjson archiveRunAttempt "$archive_run_attempt" \ | |
| '.sourceSha == $sourceSha | |
| and .archiveRunId == $archiveRunId | |
| and .archiveRunAttempt == $archiveRunAttempt' \ | |
| current-production.json >/dev/null; then | |
| echo "Production no longer matches the deployment candidate." >&2 | |
| exit 1 | |
| fi | |
| mkdir final | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \ | |
| --jq '.assets[] | [.name, .id] | @tsv' | | |
| while IFS=$'\t' read -r name asset_id; do | |
| if [[ "$name" == "$archive_name" || | |
| "$name" == "$checksum_name" ]]; then | |
| gh api \ | |
| -H "Accept: application/octet-stream" \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" \ | |
| > "final/${name}" | |
| fi | |
| done | |
| ( | |
| cd final | |
| sha256sum -c "$checksum_name" | |
| ) | |
| pointer_name="deployment-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json" | |
| printf \ | |
| '{"schemaVersion":1,"archiveRunId":%s,"archiveRunAttempt":%s,"sourceSha":"%s","deploymentRunId":%s,"stageRunAttempt":%s,"deployRunAttempt":%s,"smokeRunAttempt":%s,"publicationRunAttempt":%s}\n' \ | |
| "$archive_run_id" \ | |
| "$archive_run_attempt" \ | |
| "$SOURCE_SHA" \ | |
| "$GITHUB_RUN_ID" \ | |
| "$stage_run_attempt" \ | |
| "$deploy_run_attempt" \ | |
| "$smoke_run_attempt" \ | |
| "$GITHUB_RUN_ATTEMPT" \ | |
| > "final/${pointer_name}" | |
| pointer_tag="docs-pages-deployment-v1-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| find_final_release() { | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r --arg tag "$pointer_tag" ' | |
| [ | |
| .[][] | |
| | select(.tag_name == $tag) | |
| ] | |
| | if length > 1 then | |
| error("final release is duplicated") | |
| else | |
| (first // empty) | |
| end | |
| | [ | |
| .id, | |
| .draft, | |
| .immutable, | |
| .target_commitish, | |
| (.assets | length), | |
| ([.assets[].name] | sort | join(",")) | |
| ] | |
| | @tsv | |
| ' | |
| } | |
| release="$(find_final_release)" | |
| if [[ -z "$release" ]]; then | |
| gh release create "$pointer_tag" \ | |
| "final/${archive_name}" \ | |
| "final/${checksum_name}" \ | |
| "final/${pointer_name}" \ | |
| --draft \ | |
| --latest=false \ | |
| --prerelease \ | |
| --target "$DOCS_RELEASE_TARGET" \ | |
| --title "Documentation deployment ${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}" \ | |
| --notes "Automation-owned exact deployment release." | |
| for attempt in {1..10}; do | |
| release="$(find_final_release)" | |
| [[ -n "$release" ]] && break | |
| sleep 1 | |
| done | |
| fi | |
| if [[ -z "$release" ]]; then | |
| echo "Final release did not become visible." >&2 | |
| exit 1 | |
| fi | |
| IFS=$'\t' read -r release_id draft immutable target \ | |
| asset_count asset_names <<<"$release" | |
| if [[ ! "$release_id" =~ ^[0-9]+$ || | |
| "$target" != "$DOCS_RELEASE_TARGET" || | |
| "$asset_count" != 3 || | |
| "$asset_names" != "${pointer_name},${archive_name},${checksum_name}" || | |
| ! ( | |
| ( | |
| "$draft" == true && | |
| "$immutable" == false | |
| ) || | |
| ( | |
| "$draft" == false && | |
| "$immutable" == true | |
| ) | |
| ) ]]; then | |
| echo "Deployment publication candidate is invalid." >&2 | |
| exit 1 | |
| fi | |
| mkdir final-verify | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \ | |
| --jq '.assets[] | [.name, .id] | @tsv' | | |
| while IFS=$'\t' read -r name asset_id; do | |
| gh api \ | |
| -H "Accept: application/octet-stream" \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" \ | |
| > "final-verify/${name}" | |
| done | |
| ( | |
| cd final-verify | |
| sha256sum -c "$checksum_name" | |
| ) | |
| cmp "final/${pointer_name}" "final-verify/${pointer_name}" | |
| if [[ "$draft" == true ]]; then | |
| gh api \ | |
| --method PATCH \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \ | |
| -F draft=false >/dev/null | |
| fi | |
| published="$( | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \ | |
| --jq '[.draft, .immutable, .target_commitish, (.assets | length), ([.assets[].name] | sort | join(","))] | @tsv' | |
| )" | |
| IFS=$'\t' read -r draft immutable target asset_count asset_names \ | |
| <<<"$published" | |
| if [[ "$draft" != false || | |
| "$immutable" != true || | |
| "$target" != "$DOCS_RELEASE_TARGET" || | |
| "$asset_count" != 3 || | |
| "$asset_names" != "${pointer_name},${archive_name},${checksum_name}" ]]; then | |
| echo "Published deployment release is invalid." >&2 | |
| exit 1 | |
| fi | |
| recover-after-production-failure: | |
| if: >- | |
| always() && | |
| needs.deploy.result == 'success' && | |
| ( | |
| needs.production-smoke.result == 'failure' || | |
| ( | |
| needs.production-smoke.result == 'success' && | |
| needs.record-production-deployment.result == 'failure' | |
| ) | |
| ) && | |
| ( | |
| ( | |
| github.event_name == 'push' && | |
| needs.retain-pages-artifacts.result == 'success' | |
| ) || | |
| ( | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.rollback_run_id != '' | |
| ) | |
| ) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - retain-pages-artifacts | |
| - deploy | |
| - production-smoke | |
| - record-production-deployment | |
| permissions: | |
| actions: write | |
| contents: read | |
| steps: | |
| - name: Dispatch exact retained recovery | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PRIOR_DEPLOYMENT_RUN_ATTEMPT: >- | |
| ${{ needs.retain-pages-artifacts.outputs.prior_deployment_run_attempt }} | |
| PRIOR_DEPLOYMENT_RUN_ID: >- | |
| ${{ needs.retain-pages-artifacts.outputs.prior_deployment_run_id }} | |
| PRIOR_RUN_ATTEMPT: >- | |
| ${{ needs.retain-pages-artifacts.outputs.prior_run_attempt }} | |
| PRIOR_RUN_ID: ${{ needs.retain-pages-artifacts.outputs.prior_run_id }} | |
| RECORD_RESULT: ${{ needs.record-production-deployment.result }} | |
| REQUESTED_ROLLBACK_RUN_ATTEMPT: ${{ inputs.rollback_run_attempt }} | |
| REQUESTED_ROLLBACK_RUN_ID: ${{ inputs.rollback_run_id }} | |
| run: | | |
| recovery_run_id="$PRIOR_RUN_ID" | |
| recovery_run_attempt="$PRIOR_RUN_ATTEMPT" | |
| expected_pointer_run_id="$PRIOR_DEPLOYMENT_RUN_ID" | |
| expected_pointer_run_attempt="$PRIOR_DEPLOYMENT_RUN_ATTEMPT" | |
| if [[ -n "$REQUESTED_ROLLBACK_RUN_ID" || | |
| "$RECORD_RESULT" == failure ]]; then | |
| pointer="$( | |
| gh api \ | |
| --paginate \ | |
| --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/releases?per_page=100" | | |
| jq -r ' | |
| [ | |
| .[][] | |
| | select( | |
| .tag_name | |
| | startswith("docs-pages-deployment-v1-") | |
| ) | |
| | select(.draft == false and .immutable == true) | |
| ] | |
| | sort_by(.published_at) | |
| | (last // empty) | |
| | [ | |
| .tag_name, | |
| (.assets | length), | |
| ([.assets[].name] | sort | join(",")), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ] | length), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ][0].id // empty), | |
| ([ | |
| .assets[] | |
| | select( | |
| .name | |
| | ( | |
| startswith("deployment-") | |
| and endswith(".json") | |
| ) | |
| ) | |
| ][0].name // empty) | |
| ] | |
| | @tsv | |
| ' | |
| )" | |
| IFS=$'\t' read -r pointer_tag asset_count asset_names pointer_count \ | |
| pointer_asset_id pointer_asset_name <<<"$pointer" | |
| pointer_key="${pointer_tag#docs-pages-deployment-v1-}" | |
| pointer_run_id="${pointer_key%-*}" | |
| pointer_run_attempt="${pointer_key##*-}" | |
| if [[ ! "$pointer_run_id" =~ ^[0-9]+$ || | |
| ! "$pointer_run_attempt" =~ ^[0-9]+$ || | |
| "$asset_count" != 3 || | |
| "$pointer_count" != 1 || | |
| "$pointer_asset_name" != "deployment-${pointer_run_id}-${pointer_run_attempt}.json" || | |
| ! "$pointer_asset_id" =~ ^[0-9]+$ ]]; then | |
| echo "The last known-good deployment pointer is unavailable." >&2 | |
| exit 1 | |
| fi | |
| gh api \ | |
| -H "Accept: application/octet-stream" \ | |
| "repos/${GITHUB_REPOSITORY}/releases/assets/${pointer_asset_id}" \ | |
| > prior-deployment.json | |
| recovery_run_id="$( | |
| jq -r '.archiveRunId // empty' prior-deployment.json | |
| )" | |
| recovery_run_attempt="$( | |
| jq -r '.archiveRunAttempt // empty' prior-deployment.json | |
| )" | |
| expected_pointer_run_id="$( | |
| jq -r '.deploymentRunId // empty' prior-deployment.json | |
| )" | |
| expected_pointer_run_attempt="$( | |
| jq -r '.publicationRunAttempt // empty' prior-deployment.json | |
| )" | |
| archive_name="github-pages-${recovery_run_id}-${recovery_run_attempt}.tar" | |
| checksum_name="${archive_name}.sha256" | |
| if ! jq -e \ | |
| --argjson deploymentRunId "$pointer_run_id" \ | |
| --argjson publicationRunAttempt "$pointer_run_attempt" \ | |
| '.schemaVersion == 1 | |
| and .deploymentRunId == $deploymentRunId | |
| and .publicationRunAttempt == $publicationRunAttempt' \ | |
| prior-deployment.json >/dev/null; then | |
| echo "The last known-good deployment pointer is invalid." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$asset_names" != "${pointer_asset_name},${archive_name},${checksum_name}" ]]; then | |
| echo "The last known-good deployment assets are invalid." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$recovery_run_id" == "$REQUESTED_ROLLBACK_RUN_ID" && | |
| "$recovery_run_attempt" == "$REQUESTED_ROLLBACK_RUN_ATTEMPT" ]]; then | |
| echo "The requested recovery artifact is already the last known-good deployment; automatic recovery stopped." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ ! "$recovery_run_id" =~ ^[0-9]+$ || | |
| ! "$recovery_run_attempt" =~ ^[0-9]+$ ]]; then | |
| echo "Retained recovery run is unavailable." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! "$expected_pointer_run_id" =~ ^[0-9]+$ || | |
| ! "$expected_pointer_run_attempt" =~ ^[0-9]+$ ]]; then | |
| echo "Retained deployment pointer is unavailable." >&2 | |
| exit 1 | |
| fi | |
| gh workflow run docs.yml \ | |
| --ref main \ | |
| -f "rollback_run_id=${recovery_run_id}" \ | |
| -f "rollback_run_attempt=${recovery_run_attempt}" \ | |
| -f "expected_pointer_run_id=${expected_pointer_run_id}" \ | |
| -f "expected_pointer_run_attempt=${expected_pointer_run_attempt}" |