Skip to content

refactor: build greedy ordinal map with TryAdd #293

refactor: build greedy ordinal map with TryAdd

refactor: build greedy ordinal map with TryAdd #293

Workflow file for this run

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}"