chore(raw): prepare public 0.1.0 release #1
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: codex-raw release | |
| on: | |
| push: | |
| tags: | |
| - codex-raw-v* | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: codex-raw-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| CARGO_INCREMENTAL: "0" | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| validate: | |
| name: Validate repository, tag, and crate version | |
| if: github.repository == 'bproject07/Codex-Source' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| outputs: | |
| commit: ${{ steps.source.outputs.commit }} | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Check out tagged source | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify tag commit is on main | |
| id: source | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags --prune origin \ | |
| 'refs/heads/main:refs/remotes/origin/main' | |
| git show-ref --verify --quiet refs/remotes/origin/main || { | |
| echo '::error::Unable to resolve origin/main' | |
| exit 1 | |
| } | |
| tagged_commit="$(git rev-parse --verify "${GITHUB_SHA}^{commit}")" || { | |
| echo '::error::Unable to resolve the tagged commit' | |
| exit 1 | |
| } | |
| main_commit="$(git rev-parse --verify 'refs/remotes/origin/main^{commit}')" || { | |
| echo '::error::Unable to resolve origin/main to a commit' | |
| exit 1 | |
| } | |
| checked_out_commit="$(git rev-parse --verify 'HEAD^{commit}')" || { | |
| echo '::error::Unable to resolve the checked-out commit' | |
| exit 1 | |
| } | |
| readonly tagged_commit main_commit checked_out_commit | |
| [[ "$checked_out_commit" == "$tagged_commit" ]] || { | |
| echo '::error::Checked-out commit does not match GITHUB_SHA' | |
| exit 1 | |
| } | |
| git merge-base --is-ancestor "$tagged_commit" "$main_commit" || { | |
| echo '::error::Release tag must point to a commit reachable from origin/main' | |
| exit 1 | |
| } | |
| echo "commit=$tagged_commit" >> "$GITHUB_OUTPUT" | |
| - name: Install Rust 1.95.0 | |
| uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 | |
| - name: Validate release identity | |
| id: version | |
| shell: bash | |
| working-directory: codex-rs | |
| run: | | |
| set -euo pipefail | |
| readonly expected_repository='bproject07/Codex-Source' | |
| readonly tag_prefix='codex-raw-v' | |
| readonly semver_pattern='[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' | |
| [[ "$GITHUB_REPOSITORY" == "$expected_repository" ]] || { | |
| echo "::error::Release is restricted to $expected_repository" | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_TYPE" == 'tag' ]] || { | |
| echo '::error::Release ref must be a tag' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_NAME" =~ ^${tag_prefix}(${semver_pattern})$ ]] || { | |
| echo "::error::Tag must match ${tag_prefix}<semver>" | |
| exit 1 | |
| } | |
| readonly tag_version="${BASH_REMATCH[1]}" | |
| readonly crate_version="$( | |
| cargo metadata --locked --no-deps --format-version 1 | | |
| jq -er '[.packages[] | select(.name == "codex-raw") | .version] | | |
| if length == 1 then .[0] else error("expected exactly one codex-raw package") end' | |
| )" | |
| [[ "$tag_version" == "$crate_version" ]] || { | |
| echo "::error::Tag version $tag_version does not match codex-raw version $crate_version" | |
| exit 1 | |
| } | |
| echo "version=$crate_version" >> "$GITHUB_OUTPUT" | |
| - name: Validate curated release notes | |
| shell: bash | |
| env: | |
| RELEASE_VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| readonly heading_prefix="## [${RELEASE_VERSION}] - " | |
| mapfile -t version_headings < <( | |
| grep -F "## [${RELEASE_VERSION}]" CHANGELOG.md || true | |
| ) | |
| [[ "${#version_headings[@]}" -eq 1 ]] || { | |
| echo "::error::CHANGELOG.md must contain exactly one ${RELEASE_VERSION} release heading" | |
| exit 1 | |
| } | |
| readonly heading="${version_headings[0]}" | |
| readonly release_date="${heading#"$heading_prefix"}" | |
| [[ "$heading" == "$heading_prefix"* ]] && | |
| [[ "$release_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { | |
| echo "::error::The ${RELEASE_VERSION} changelog heading must include an ISO release date" | |
| exit 1 | |
| } | |
| awk -v heading_prefix="$heading_prefix" ' | |
| index($0, heading_prefix) == 1 { | |
| capture = 1 | |
| next | |
| } | |
| capture && /^## \[/ { | |
| exit | |
| } | |
| capture { | |
| } | |
| ' CHANGELOG.md > "$RUNNER_TEMP/codex-raw-release-notes.md" | |
| grep -Eq '[^[:space:]]' "$RUNNER_TEMP/codex-raw-release-notes.md" || { | |
| echo "::error::The ${RELEASE_VERSION} changelog section is empty" | |
| exit 1 | |
| } | |
| build: | |
| name: Build ${{ matrix.target }} | |
| if: github.repository == 'bproject07/Codex-Source' | |
| needs: validate | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-24.04 | |
| target: x86_64-unknown-linux-gnu | |
| archive: tar.gz | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| archive: tar.gz | |
| - os: windows-2025 | |
| target: x86_64-pc-windows-msvc | |
| archive: zip | |
| - os: macos-15-intel | |
| target: x86_64-apple-darwin | |
| archive: tar.gz | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 90 | |
| env: | |
| RELEASE_VERSION: ${{ needs.validate.outputs.version }} | |
| RELEASE_TARGET: ${{ matrix.target }} | |
| steps: | |
| - name: Check out tagged source | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Verify native Linux ARM64 runner | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$RUNNER_OS" == 'Linux' && "$RUNNER_ARCH" == 'ARM64' && "$(uname -m)" == 'aarch64' ]] || { | |
| echo '::error::Linux ARM64 releases must be built on a native GitHub-hosted ARM64 runner' | |
| exit 1 | |
| } | |
| - name: Install Rust 1.95.0 | |
| uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Build release binary | |
| working-directory: codex-rs | |
| run: cargo build --locked --release --package codex-raw --target "${{ matrix.target }}" | |
| - name: Smoke-test Linux or macOS binary | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| readonly binary="codex-rs/target/${RELEASE_TARGET}/release/codex-raw" | |
| readonly actual_version="$("$binary" --version)" | |
| [[ "$actual_version" == "codex-raw ${RELEASE_VERSION}" ]] || { | |
| echo "::error::Expected codex-raw ${RELEASE_VERSION}, got ${actual_version}" | |
| exit 1 | |
| } | |
| "$binary" update --help > /dev/null | |
| "$binary" api-server --help > /dev/null | |
| - name: Smoke-test Windows binary | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $binary = "codex-rs\target\$env:RELEASE_TARGET\release\codex-raw.exe" | |
| $actualVersion = (& $binary --version | Out-String).Trim() | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "codex-raw --version exited with $LASTEXITCODE" | |
| } | |
| if ($actualVersion -ne "codex-raw $env:RELEASE_VERSION") { | |
| throw "Expected codex-raw $env:RELEASE_VERSION, got $actualVersion" | |
| } | |
| & $binary update --help | Out-Null | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "codex-raw update --help exited with $LASTEXITCODE" | |
| } | |
| & $binary api-server --help | Out-Null | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "codex-raw api-server --help exited with $LASTEXITCODE" | |
| } | |
| - name: Create Linux or macOS archive | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| readonly bundle="codex-raw-${RELEASE_VERSION}-${RELEASE_TARGET}" | |
| mkdir -p \ | |
| "dist/$bundle/docs" \ | |
| "dist/$bundle/codex-rs/raw-cli" | |
| cp "codex-rs/target/${RELEASE_TARGET}/release/codex-raw" "dist/$bundle/" | |
| cp \ | |
| API.md \ | |
| CHANGELOG.md \ | |
| CODE_OF_CONDUCT.md \ | |
| CONTRIBUTING.md \ | |
| INSTALL.md \ | |
| LICENSE \ | |
| NOTICE \ | |
| README.md \ | |
| SECURITY.md \ | |
| SUPPORT.md \ | |
| UPSTREAM.md \ | |
| "dist/$bundle/" | |
| cp docs/CLA.md docs/contributing.md "dist/$bundle/docs/" | |
| cp codex-rs/raw-cli/README.md "dist/$bundle/codex-rs/raw-cli/" | |
| tar -C dist -czf "dist/${bundle}.tar.gz" "$bundle" | |
| - name: Create Windows archive | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $bundle = "codex-raw-$env:RELEASE_VERSION-$env:RELEASE_TARGET" | |
| New-Item -ItemType Directory -Path ` | |
| "dist\$bundle\docs", ` | |
| "dist\$bundle\codex-rs\raw-cli" ` | |
| -Force | Out-Null | |
| Copy-Item "codex-rs\target\$env:RELEASE_TARGET\release\codex-raw.exe" "dist\$bundle\" | |
| $rootDocs = @( | |
| "API.md" | |
| "CHANGELOG.md" | |
| "CODE_OF_CONDUCT.md" | |
| "CONTRIBUTING.md" | |
| "INSTALL.md" | |
| "LICENSE" | |
| "NOTICE" | |
| "README.md" | |
| "SECURITY.md" | |
| "SUPPORT.md" | |
| "UPSTREAM.md" | |
| ) | |
| Copy-Item -LiteralPath $rootDocs -Destination "dist\$bundle\" | |
| Copy-Item -LiteralPath "docs\CLA.md", "docs\contributing.md" ` | |
| -Destination "dist\$bundle\docs\" | |
| Copy-Item -LiteralPath "codex-rs\raw-cli\README.md" ` | |
| -Destination "dist\$bundle\codex-rs\raw-cli\" | |
| Compress-Archive -Path "dist\$bundle" -DestinationPath "dist\$bundle.zip" | |
| - name: Upload release archive | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: codex-raw-${{ matrix.target }} | |
| path: dist/codex-raw-*.${{ matrix.archive }} | |
| if-no-files-found: error | |
| retention-days: 7 | |
| compression-level: 0 | |
| publish: | |
| name: Publish GitHub release | |
| if: github.repository == 'bproject07/Codex-Source' | |
| needs: | |
| - validate | |
| - build | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| env: | |
| ORIGINAL_GITHUB_SHA: ${{ github.sha }} | |
| RELEASE_VERSION: ${{ needs.validate.outputs.version }} | |
| VALIDATED_COMMIT: ${{ needs.validate.outputs.commit }} | |
| steps: | |
| - name: Verify immutable release gate | |
| shell: bash | |
| env: | |
| IMMUTABLE_RELEASES_GATE: ${{ vars.CODEX_RAW_IMMUTABLE_RELEASES }} | |
| run: | | |
| set -euo pipefail | |
| [[ "$GITHUB_REPOSITORY" == 'bproject07/Codex-Source' ]] || { | |
| echo '::error::Repository guard failed' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_TYPE" == 'tag' ]] || { | |
| echo '::error::Release ref must be a tag' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_NAME" == "codex-raw-v${RELEASE_VERSION}" ]] || { | |
| echo '::error::Validated version no longer matches the release tag' | |
| exit 1 | |
| } | |
| [[ "$IMMUTABLE_RELEASES_GATE" == 'enabled' ]] || { | |
| echo '::error::Enable release immutability, then set CODEX_RAW_IMMUTABLE_RELEASES=enabled' | |
| exit 1 | |
| } | |
| - name: Check out validated release notes | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ needs.validate.outputs.commit }} | |
| persist-credentials: false | |
| - name: Prepare exact release-tag verifier | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cat > "$RUNNER_TEMP/verify-codex-raw-release-tag.sh" <<'BASH' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| readonly phase="${1:?verification phase is required}" | |
| readonly expected_repository='bproject07/Codex-Source' | |
| readonly expected_ref="refs/tags/${GITHUB_REF_NAME}" | |
| readonly api_version='2026-03-10' | |
| readonly max_tag_depth=8 | |
| fail() { | |
| echo "::error::Release tag verification failed during ${phase}: $*" >&2 | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REPOSITORY" == "$expected_repository" ]] || | |
| fail "unexpected repository $GITHUB_REPOSITORY" | |
| [[ "$GITHUB_REF_TYPE" == 'tag' ]] || | |
| fail "the workflow ref is not a tag" | |
| [[ "$VALIDATED_COMMIT" =~ ^[0-9a-f]{40}$ ]] || | |
| fail 'the validated commit is not a full SHA-1' | |
| [[ "$ORIGINAL_GITHUB_SHA" =~ ^[0-9a-f]{40}$ ]] || | |
| fail 'the original GITHUB_SHA is not a full SHA-1' | |
| [[ "$VALIDATED_COMMIT" == "$ORIGINAL_GITHUB_SHA" ]] || | |
| fail "validated commit $VALIDATED_COMMIT differs from original GITHUB_SHA $ORIGINAL_GITHUB_SHA" | |
| read_exact_ref() { | |
| local ref_json | |
| if ! ref_json="$( | |
| gh api \ | |
| --method GET \ | |
| --header 'Accept: application/vnd.github+json' \ | |
| --header "X-GitHub-Api-Version: ${api_version}" \ | |
| "repos/${GITHUB_REPOSITORY}/git/ref/tags/${GITHUB_REF_NAME}" | |
| )"; then | |
| fail "cannot fetch the exact remote tag ${expected_ref}" | |
| fi | |
| jq -e \ | |
| --arg ref "$expected_ref" \ | |
| ' | |
| type == "object" and | |
| .ref == $ref and | |
| (.object | type == "object") and | |
| (.object.type == "commit" or .object.type == "tag") and | |
| (.object.sha | type == "string") and | |
| (.object.sha | test("^[0-9a-f]{40}$")) | |
| ' <<< "$ref_json" > /dev/null || | |
| fail "remote tag lookup was missing, ambiguous, or malformed for ${expected_ref}" | |
| printf '%s\n' "$ref_json" | |
| } | |
| initial_ref_json="$(read_exact_ref)" | |
| initial_type="$(jq -er '.object.type' <<< "$initial_ref_json")" | |
| initial_sha="$(jq -er '.object.sha' <<< "$initial_ref_json")" | |
| object_type="$initial_type" | |
| object_sha="$initial_sha" | |
| depth=0 | |
| while [[ "$object_type" == 'tag' ]]; do | |
| ((depth < max_tag_depth)) || | |
| fail "annotated tag chain exceeds ${max_tag_depth} objects" | |
| depth=$((depth + 1)) | |
| if ! tag_json="$( | |
| gh api \ | |
| --method GET \ | |
| --header 'Accept: application/vnd.github+json' \ | |
| --header "X-GitHub-Api-Version: ${api_version}" \ | |
| "repos/${GITHUB_REPOSITORY}/git/tags/${object_sha}" | |
| )"; then | |
| fail "cannot fetch annotated tag object ${object_sha}" | |
| fi | |
| jq -e \ | |
| --arg sha "$object_sha" \ | |
| ' | |
| type == "object" and | |
| .sha == $sha and | |
| (.object | type == "object") and | |
| (.object.type == "commit" or .object.type == "tag") and | |
| (.object.sha | type == "string") and | |
| (.object.sha | test("^[0-9a-f]{40}$")) | |
| ' <<< "$tag_json" > /dev/null || | |
| fail "annotated tag object ${object_sha} is malformed or does not lead to a commit" | |
| object_type="$(jq -er '.object.type' <<< "$tag_json")" | |
| object_sha="$(jq -er '.object.sha' <<< "$tag_json")" | |
| done | |
| [[ "$object_type" == 'commit' ]] || | |
| fail "remote tag resolves to unsupported object type ${object_type}" | |
| final_ref_json="$(read_exact_ref)" | |
| final_type="$(jq -er '.object.type' <<< "$final_ref_json")" | |
| final_sha="$(jq -er '.object.sha' <<< "$final_ref_json")" | |
| [[ "$final_type" == "$initial_type" && "$final_sha" == "$initial_sha" ]] || | |
| fail 'remote tag changed while it was being resolved' | |
| [[ "$object_sha" == "$VALIDATED_COMMIT" ]] || | |
| fail "remote tag resolves to $object_sha instead of validated commit $VALIDATED_COMMIT" | |
| [[ "$object_sha" == "$ORIGINAL_GITHUB_SHA" ]] || | |
| fail "remote tag resolves to $object_sha instead of original GITHUB_SHA $ORIGINAL_GITHUB_SHA" | |
| echo "Verified ${expected_ref} at ${object_sha} during ${phase}." | |
| BASH | |
| chmod 0700 "$RUNNER_TEMP/verify-codex-raw-release-tag.sh" | |
| - name: Download release archives | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: release-assets | |
| pattern: codex-raw-* | |
| merge-multiple: true | |
| - name: Verify archives and write SHA-256 manifest | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| readonly base="codex-raw-${RELEASE_VERSION}" | |
| readonly linux_x64="${base}-x86_64-unknown-linux-gnu.tar.gz" | |
| readonly linux_arm64="${base}-aarch64-unknown-linux-gnu.tar.gz" | |
| readonly windows="${base}-x86_64-pc-windows-msvc.zip" | |
| readonly macos="${base}-x86_64-apple-darwin.tar.gz" | |
| readonly expected=("$linux_x64" "$linux_arm64" "$windows" "$macos") | |
| for archive in "${expected[@]}"; do | |
| [[ -f "release-assets/$archive" ]] || { | |
| echo "::error::Missing release archive: $archive" | |
| exit 1 | |
| } | |
| done | |
| mapfile -t actual < <( | |
| find release-assets -maxdepth 1 -type f -printf '%f\n' | sort | |
| ) | |
| [[ "${#actual[@]}" -eq "${#expected[@]}" ]] || { | |
| echo "::error::Expected ${#expected[@]} release archives, found ${#actual[@]}" | |
| exit 1 | |
| } | |
| ( | |
| cd release-assets | |
| sha256sum "${expected[@]}" > SHA256SUMS | |
| sha256sum --check --strict SHA256SUMS | |
| ) | |
| - name: Create draft GitHub release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| [[ "$GITHUB_REPOSITORY" == 'bproject07/Codex-Source' ]] || { | |
| echo '::error::Repository guard failed' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_TYPE" == 'tag' ]] || { | |
| echo '::error::Release ref must be a tag' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_NAME" == "codex-raw-v${RELEASE_VERSION}" ]] || { | |
| echo '::error::Validated version no longer matches the release tag' | |
| exit 1 | |
| } | |
| "$RUNNER_TEMP/verify-codex-raw-release-tag.sh" 'before draft creation' | |
| readonly notes_file="$RUNNER_TEMP/codex-raw-release-notes.md" | |
| readonly heading_prefix="## [${RELEASE_VERSION}] - " | |
| awk -v heading_prefix="$heading_prefix" ' | |
| index($0, heading_prefix) == 1 { | |
| capture = 1 | |
| next | |
| } | |
| capture && /^## \[/ { | |
| exit | |
| } | |
| capture { | |
| } | |
| ' CHANGELOG.md > "$notes_file" | |
| grep -Eq '[^[:space:]]' "$notes_file" || { | |
| echo "::error::Curated release notes for ${RELEASE_VERSION} are missing" | |
| exit 1 | |
| } | |
| release_args=( | |
| "$GITHUB_REF_NAME" | |
| --repo "$GITHUB_REPOSITORY" | |
| --verify-tag | |
| --draft | |
| --title "codex-raw ${RELEASE_VERSION}" | |
| --notes-file "$notes_file" | |
| ) | |
| readonly release_precedence="${RELEASE_VERSION%%+*}" | |
| if [[ "$release_precedence" == *-* ]]; then | |
| release_args+=(--prerelease) | |
| fi | |
| gh release create "${release_args[@]}" | |
| - name: Upload exact draft release assets | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| [[ "$GITHUB_REPOSITORY" == 'bproject07/Codex-Source' ]] || { | |
| echo '::error::Repository guard failed' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_TYPE" == 'tag' ]] || { | |
| echo '::error::Release ref must be a tag' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_NAME" == "codex-raw-v${RELEASE_VERSION}" ]] || { | |
| echo '::error::Validated version no longer matches the release tag' | |
| exit 1 | |
| } | |
| readonly base="codex-raw-${RELEASE_VERSION}" | |
| readonly assets=( | |
| "release-assets/${base}-x86_64-unknown-linux-gnu.tar.gz" | |
| "release-assets/${base}-aarch64-unknown-linux-gnu.tar.gz" | |
| "release-assets/${base}-x86_64-pc-windows-msvc.zip" | |
| "release-assets/${base}-x86_64-apple-darwin.tar.gz" | |
| 'release-assets/SHA256SUMS' | |
| ) | |
| [[ "$( | |
| gh release view "$GITHUB_REF_NAME" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --json isDraft \ | |
| --jq '.isDraft' | |
| )" == 'true' ]] || { | |
| echo '::error::Release must remain a draft while assets are uploaded' | |
| exit 1 | |
| } | |
| for asset in "${assets[@]}"; do | |
| [[ -f "$asset" ]] || { | |
| echo "::error::Missing local release asset: $asset" | |
| exit 1 | |
| } | |
| done | |
| gh release upload "$GITHUB_REF_NAME" "${assets[@]}" \ | |
| --repo "$GITHUB_REPOSITORY" | |
| - name: Verify draft assets and publish immutable release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| [[ "$GITHUB_REPOSITORY" == 'bproject07/Codex-Source' ]] || { | |
| echo '::error::Repository guard failed' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_TYPE" == 'tag' ]] || { | |
| echo '::error::Release ref must be a tag' | |
| exit 1 | |
| } | |
| [[ "$GITHUB_REF_NAME" == "codex-raw-v${RELEASE_VERSION}" ]] || { | |
| echo '::error::Validated version no longer matches the release tag' | |
| exit 1 | |
| } | |
| readonly base="codex-raw-${RELEASE_VERSION}" | |
| readonly assets=( | |
| "release-assets/${base}-x86_64-unknown-linux-gnu.tar.gz" | |
| "release-assets/${base}-aarch64-unknown-linux-gnu.tar.gz" | |
| "release-assets/${base}-x86_64-pc-windows-msvc.zip" | |
| "release-assets/${base}-x86_64-apple-darwin.tar.gz" | |
| 'release-assets/SHA256SUMS' | |
| ) | |
| readonly release_precedence="${RELEASE_VERSION%%+*}" | |
| expected_prerelease='false' | |
| if [[ "$release_precedence" == *-* ]]; then | |
| expected_prerelease='true' | |
| fi | |
| readonly expected_prerelease | |
| declare -A expected_sizes | |
| declare -A expected_digests | |
| for asset in "${assets[@]}"; do | |
| asset_name="${asset##*/}" | |
| expected_sizes["$asset_name"]="$(stat --format='%s' "$asset")" | |
| [[ "${expected_sizes[$asset_name]}" -gt 0 ]] || { | |
| echo "::error::Local release asset is empty: $asset_name" | |
| exit 1 | |
| } | |
| expected_digests["$asset_name"]="sha256:$( | |
| sha256sum "$asset" | cut -d ' ' -f 1 | |
| )" | |
| done | |
| verify_draft_assets() { | |
| local release_json="$1" | |
| local remote_asset_count | |
| local asset | |
| local asset_name | |
| local asset_json | |
| local asset_state | |
| local remote_size | |
| local remote_digest | |
| jq -e \ | |
| --arg tag "$GITHUB_REF_NAME" \ | |
| --arg name "codex-raw ${RELEASE_VERSION}" \ | |
| --argjson prerelease "$expected_prerelease" \ | |
| ' | |
| .tag_name == $tag and | |
| .name == $name and | |
| .draft == true and | |
| .prerelease == $prerelease and | |
| .immutable == false | |
| ' <<< "$release_json" > /dev/null || return 1 | |
| remote_asset_count="$( | |
| jq -er '.assets | length' <<< "$release_json" | |
| )" || return 1 | |
| [[ "$remote_asset_count" -eq "${#assets[@]}" ]] || return 1 | |
| for asset in "${assets[@]}"; do | |
| asset_name="${asset##*/}" | |
| asset_json="$( | |
| jq -cer \ | |
| --arg name "$asset_name" \ | |
| ' | |
| [.assets[] | select(.name == $name)] | | |
| if length == 1 then .[0] | |
| else error("expected exactly one matching release asset") | |
| end | |
| ' <<< "$release_json" | |
| )" || return 1 | |
| asset_state="$(jq -er '.state' <<< "$asset_json")" || return 1 | |
| [[ "$asset_state" == 'uploaded' ]] || return 1 | |
| remote_size="$(jq -er '.size' <<< "$asset_json")" || return 1 | |
| [[ "$remote_size" == "${expected_sizes[$asset_name]}" ]] || | |
| return 1 | |
| remote_digest="$(jq -er '.digest' <<< "$asset_json")" || return 1 | |
| [[ "$remote_digest" == "${expected_digests[$asset_name]}" ]] || | |
| return 1 | |
| done | |
| } | |
| readonly max_attempts=10 | |
| verified='false' | |
| release_json='' | |
| for ((attempt = 1; attempt <= max_attempts; attempt++)); do | |
| release_json="$( | |
| gh api \ | |
| --method GET \ | |
| --header 'Accept: application/vnd.github+json' \ | |
| --header 'X-GitHub-Api-Version: 2026-03-10' \ | |
| "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" | |
| )" || true | |
| if [[ -n "$release_json" ]] && | |
| verify_draft_assets "$release_json"; then | |
| verified='true' | |
| break | |
| fi | |
| if [[ "$attempt" -lt "$max_attempts" ]]; then | |
| echo "::warning::Draft asset metadata is not ready (attempt $attempt/$max_attempts)" | |
| sleep 3 | |
| fi | |
| done | |
| [[ "$verified" == 'true' ]] || { | |
| echo '::error::Draft assets failed exact name, state, size, or digest verification' | |
| exit 1 | |
| } | |
| "$RUNNER_TEMP/verify-codex-raw-release-tag.sh" 'immediately before publication' | |
| gh release edit "$GITHUB_REF_NAME" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --verify-tag \ | |
| --draft=false | |
| published_json="$( | |
| gh api \ | |
| --method GET \ | |
| --header 'Accept: application/vnd.github+json' \ | |
| --header 'X-GitHub-Api-Version: 2026-03-10' \ | |
| "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" | |
| )" | |
| readonly published_json | |
| if jq -e \ | |
| --arg tag "$GITHUB_REF_NAME" \ | |
| --argjson asset_count "${#assets[@]}" \ | |
| ' | |
| .tag_name == $tag and | |
| .draft == false and | |
| .immutable == true and | |
| (.published_at | type == "string") and | |
| (.assets | length) == $asset_count | |
| ' <<< "$published_json" > /dev/null; then | |
| "$RUNNER_TEMP/verify-codex-raw-release-tag.sh" 'after immutable publication' | |
| exit 0 | |
| fi | |
| if jq -e \ | |
| --arg tag "$GITHUB_REF_NAME" \ | |
| ' | |
| .tag_name == $tag and | |
| .draft == false and | |
| .immutable == false | |
| ' <<< "$published_json" > /dev/null; then | |
| gh release delete "$GITHUB_REF_NAME" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --yes || { | |
| echo '::error::Mutable release detected but automatic deletion failed' | |
| exit 1 | |
| } | |
| echo '::error::Mutable release deleted; tag retained. Enable immutability and re-run all jobs' | |
| exit 1 | |
| fi | |
| echo '::error::Published release identity, assets, or immutability could not be verified' | |
| exit 1 |