Skip to content

Commit 735845d

Browse files
committed
Add secure supervised self-updates
1 parent a9f29f5 commit 735845d

45 files changed

Lines changed: 9152 additions & 405 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 231 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ on:
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

1515
permissions:
1616
contents: read
1717

1818
concurrency:
19-
group: release-${{ github.ref }}
19+
group: codex-web-release
2020
cancel-in-progress: false
2121

2222
jobs:
@@ -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

Comments
 (0)