99 version :
1010 description : Package version to build and validate without publishing
1111 required : true
12- default : " 0.1 .0"
12+ default : " 0.2 .0"
1313 type : string
1414
1515permissions :
1616 contents : read
1717
1818concurrency :
19- group : release-${{ github.ref }}
19+ group : codex-web-release
2020 cancel-in-progress : false
2121
2222jobs :
@@ -183,9 +183,15 @@ jobs:
183183 --expected-rust-version 1.95.0 `
184184 --output-dir .\dist\THIRD_PARTY_LICENSES
185185 if ($LASTEXITCODE -ne 0) { throw "license generation failed" }
186+ python -B .\scripts\generate-release-package-manifest.py `
187+ --version $env:RELEASE_VERSION `
188+ --target x86_64-pc-windows-msvc `
189+ --output-dir .\dist
190+ if ($LASTEXITCODE -ne 0) { throw "release marker generation failed" }
186191 $required = @(
187192 ".\dist\codex-web.exe",
188193 ".\dist\web\index.html",
194+ ".\dist\release-package.json",
189195 ".\dist\README.md",
190196 ".\dist\BUILDING.md",
191197 ".\dist\OPERATIONS.md",
@@ -220,6 +226,13 @@ jobs:
220226 --server .\dist\codex-web.exe `
221227 --port 8814
222228
229+ - name : Exercise packaged updater supervision and rollback
230+ shell : pwsh
231+ run : |
232+ python -B .\scripts\updater-supervisor-regression.py `
233+ --root-server .\dist\codex-web.exe `
234+ --timeout 20
235+
223236 - name : Create Windows archive
224237 id : archive
225238 shell : pwsh
@@ -315,9 +328,14 @@ jobs:
315328 --target x86_64-unknown-linux-gnu \
316329 --expected-rust-version 1.95.0 \
317330 --output-dir ./dist-linux/THIRD_PARTY_LICENSES
331+ python3 -B ./scripts/generate-release-package-manifest.py \
332+ --version "$RELEASE_VERSION" \
333+ --target x86_64-unknown-linux-gnu \
334+ --output-dir ./dist-linux
318335 for path in \
319336 ./dist-linux/codex-web \
320337 ./dist-linux/web/index.html \
338+ ./dist-linux/release-package.json \
321339 ./dist-linux/README.md \
322340 ./dist-linux/BUILDING.md \
323341 ./dist-linux/OPERATIONS.md \
@@ -347,6 +365,14 @@ jobs:
347365 --server ./dist-linux/codex-web \
348366 --port 8815
349367
368+ - name : Exercise packaged updater supervision and rollback
369+ shell : bash
370+ run : |
371+ set -euo pipefail
372+ python3 -B ./scripts/updater-supervisor-regression.py \
373+ --root-server ./dist-linux/codex-web \
374+ --timeout 20
375+
350376 - name : Create Linux archive
351377 id : archive
352378 shell : bash
@@ -455,6 +481,11 @@ jobs:
455481 RELEASE_VERSION : ${{ needs.validate-tag.outputs.version }}
456482 RELEASE_TAG : ${{ github.ref_name }}
457483 steps :
484+ - name : Check out release verification tooling
485+ uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
486+ with :
487+ persist-credentials : false
488+
458489 - name : Download validated archives
459490 uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
460491 with :
@@ -484,7 +515,59 @@ jobs:
484515 with :
485516 subject-path : release-assets/*
486517
487- - name : Create, verify, and publish release
518+ - name : Require repository release immutability
519+ env :
520+ GH_TOKEN : ${{ secrets.RELEASE_IMMUTABILITY_READ_TOKEN }}
521+ shell : bash
522+ run : |
523+ set -euo pipefail
524+ if [[ -z "${GH_TOKEN:-}" ]]; then
525+ printf '%s\n' \
526+ 'release environment secret RELEASE_IMMUTABILITY_READ_TOKEN is required' \
527+ >&2
528+ exit 1
529+ fi
530+ metadata="$RUNNER_TEMP/repository-immutable-releases.json"
531+ if ! gh api \
532+ --method GET \
533+ --header 'Accept: application/vnd.github+json' \
534+ --header 'X-GitHub-Api-Version: 2026-03-10' \
535+ "repos/$GITHUB_REPOSITORY/immutable-releases" \
536+ > "$metadata"; then
537+ printf '%s\n' \
538+ 'cannot confirm repository release immutability; refusing to publish' \
539+ >&2
540+ exit 1
541+ fi
542+ python3 -B ./scripts/verify-github-release.py policy \
543+ --metadata "$metadata"
544+
545+ - name : Recheck remote release identity before draft
546+ env :
547+ EXPECTED_SHA : ${{ github.sha }}
548+ shell : bash
549+ run : |
550+ set -euo pipefail
551+ remote_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
552+ tag_ref="refs/tags/${RELEASE_TAG}"
553+ tag_check_ref="refs/cwt-release-check/tag"
554+ main_check_ref="refs/cwt-release-check/main"
555+ git fetch --no-tags --force "$remote_url" \
556+ "+${tag_ref}:${tag_check_ref}" \
557+ "+refs/heads/main:${main_check_ref}"
558+ expected_commit=$(git rev-parse --verify "${EXPECTED_SHA}^{commit}")
559+ tag_commit=$(git rev-parse --verify "${tag_check_ref}^{commit}")
560+ main_commit=$(git rev-parse --verify "${main_check_ref}^{commit}")
561+ if [[ "$expected_commit" != "$tag_commit" ||
562+ "$expected_commit" != "$main_commit" ]]; then
563+ printf '%s\n' \
564+ 'release source, remote tag, and current main no longer identify one commit' \
565+ >&2
566+ exit 1
567+ fi
568+
569+ - name : Create draft release
570+ id : draft
488571 shell : bash
489572 run : |
490573 set -euo pipefail
@@ -500,18 +583,156 @@ jobs:
500583 --title "Codex Web Terminal $RELEASE_TAG" \
501584 release-assets/*
502585
503- mapfile -t published < < (
586+ release_id=$ (
504587 gh release view "$RELEASE_TAG" \
505588 --repo "$GITHUB_REPOSITORY" \
506- --json assets \
507- --jq '.assets[].name' |
508- sort
589+ --json databaseId \
590+ --jq '.databaseId'
509591 )
510- mapfile -t expected < <(find release-assets -maxdepth 1 -type f -printf '%f\n' | sort)
511- if [[ "${published[*]}" != "${expected[*]}" ]]; then
512- printf 'draft asset verification failed\n' >&2
592+ if [[ ! "$release_id" =~ ^[1-9][0-9]*$ ]]; then
593+ printf 'invalid draft release database ID: %q\n' "$release_id" >&2
513594 exit 1
514595 fi
596+ printf 'release_id=%s\n' "$release_id" >> "$GITHUB_OUTPUT"
597+
598+ - name : Verify draft assets and GitHub SHA-256 digests
599+ env :
600+ RELEASE_ID : ${{ steps.draft.outputs.release_id }}
601+ shell : bash
602+ run : |
603+ set -euo pipefail
604+ windows="codex-web-terminal-v${RELEASE_VERSION}-windows-x86_64.zip"
605+ linux="codex-web-terminal-v${RELEASE_VERSION}-linux-x86_64-glibc.tar.gz"
606+ metadata="$RUNNER_TEMP/draft-release.json"
607+ error_file="$RUNNER_TEMP/draft-release-error.txt"
608+ verified=false
609+ for attempt in $(seq 1 60); do
610+ if gh api \
611+ --method GET \
612+ --header 'Accept: application/vnd.github+json' \
613+ --header 'X-GitHub-Api-Version: 2026-03-10' \
614+ "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \
615+ > "$metadata" &&
616+ python3 -B ./scripts/verify-github-release.py release \
617+ --metadata "$metadata" \
618+ --tag "$RELEASE_TAG" \
619+ --phase draft \
620+ --asset-dir release-assets \
621+ --asset "$windows" \
622+ --asset "$linux" \
623+ --asset SHA256SUMS.txt \
624+ 2> "$error_file"; then
625+ verified=true
626+ break
627+ fi
628+ if (( attempt < 60 )); then
629+ sleep 2
630+ fi
631+ done
632+ if [[ "$verified" != true ]]; then
633+ printf '%s\n' \
634+ 'draft release metadata did not become complete within 120 seconds' \
635+ >&2
636+ if [[ -s "$error_file" ]]; then
637+ cat "$error_file" >&2
638+ fi
639+ exit 1
640+ fi
641+
642+ - name : Recheck immutability policy before publication
643+ env :
644+ GH_TOKEN : ${{ secrets.RELEASE_IMMUTABILITY_READ_TOKEN }}
645+ shell : bash
646+ run : |
647+ set -euo pipefail
648+ metadata="$RUNNER_TEMP/repository-immutable-releases-prepublish.json"
649+ if [[ -z "${GH_TOKEN:-}" ]] ||
650+ ! gh api \
651+ --method GET \
652+ --header 'Accept: application/vnd.github+json' \
653+ --header 'X-GitHub-Api-Version: 2026-03-10' \
654+ "repos/$GITHUB_REPOSITORY/immutable-releases" \
655+ > "$metadata"; then
656+ printf '%s\n' \
657+ 'cannot reconfirm repository release immutability; draft remains unpublished' \
658+ >&2
659+ exit 1
660+ fi
661+ python3 -B ./scripts/verify-github-release.py policy \
662+ --metadata "$metadata"
663+
664+ - name : Recheck remote release identity before publication
665+ env :
666+ EXPECTED_SHA : ${{ github.sha }}
667+ shell : bash
668+ run : |
669+ set -euo pipefail
670+ remote_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
671+ tag_ref="refs/tags/${RELEASE_TAG}"
672+ tag_check_ref="refs/cwt-release-check/tag"
673+ main_check_ref="refs/cwt-release-check/main"
674+ git fetch --no-tags --force "$remote_url" \
675+ "+${tag_ref}:${tag_check_ref}" \
676+ "+refs/heads/main:${main_check_ref}"
677+ expected_commit=$(git rev-parse --verify "${EXPECTED_SHA}^{commit}")
678+ tag_commit=$(git rev-parse --verify "${tag_check_ref}^{commit}")
679+ main_commit=$(git rev-parse --verify "${main_check_ref}^{commit}")
680+ if [[ "$expected_commit" != "$tag_commit" ||
681+ "$expected_commit" != "$main_commit" ]]; then
682+ printf '%s\n' \
683+ 'release source, remote tag, and current main no longer identify one commit' \
684+ >&2
685+ exit 1
686+ fi
687+
688+ - name : Publish verified draft
689+ shell : bash
690+ run : |
691+ set -euo pipefail
515692 gh release edit "$RELEASE_TAG" \
516693 --repo "$GITHUB_REPOSITORY" \
517694 --draft=false
695+
696+ - name : Require immutable published release and digested assets
697+ env :
698+ RELEASE_ID : ${{ steps.draft.outputs.release_id }}
699+ shell : bash
700+ run : |
701+ set -euo pipefail
702+ windows="codex-web-terminal-v${RELEASE_VERSION}-windows-x86_64.zip"
703+ linux="codex-web-terminal-v${RELEASE_VERSION}-linux-x86_64-glibc.tar.gz"
704+ metadata="$RUNNER_TEMP/published-release.json"
705+ error_file="$RUNNER_TEMP/published-release-error.txt"
706+ verified=false
707+ for attempt in $(seq 1 60); do
708+ if gh api \
709+ --method GET \
710+ --header 'Accept: application/vnd.github+json' \
711+ --header 'X-GitHub-Api-Version: 2026-03-10' \
712+ "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \
713+ > "$metadata" &&
714+ python3 -B ./scripts/verify-github-release.py release \
715+ --metadata "$metadata" \
716+ --tag "$RELEASE_TAG" \
717+ --phase published \
718+ --asset-dir release-assets \
719+ --asset "$windows" \
720+ --asset "$linux" \
721+ --asset SHA256SUMS.txt \
722+ 2> "$error_file"; then
723+ verified=true
724+ break
725+ fi
726+ if (( attempt < 60 )); then
727+ sleep 2
728+ fi
729+ done
730+ if [[ "$verified" != true ]]; then
731+ printf '%s\n' \
732+ 'published release did not become immutable and complete within 120 seconds' \
733+ >&2
734+ if [[ -s "$error_file" ]]; then
735+ cat "$error_file" >&2
736+ fi
737+ exit 1
738+ fi
0 commit comments