diff --git a/.env.example b/.env.example index c862321..d8cd644 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,14 @@ RELEASE_CANARY_HMAC_SECRET=replace-with-32-random-bytes # Missing/invalid values fail closed to no contract-3/query-plan-5 emission. RELEASE_DEPLOYMENT_PHASE=bridge RELEASE_EXPECTED_DATABASE_SCHEMA_VERSION=18 +# v2.5.4 direct editorial exposure uses this separate, mutually exclusive +# authority namespace. All four values are set on API and both worker lanes +# only after the signed 0->100 authority and durable rollback warrant verify. +# They must remain empty whenever RELEASE_PUBLIC_ROLLOUT_* authority is active. +RELEASE_V254_DIRECT_EXPOSURE_PRECONDITIONS_HASH= +RELEASE_V254_DIRECT_EXPOSURE_ROLLBACK_PLAN_HASH= +RELEASE_V254_DIRECT_EXPOSURE_STAGE= +RELEASE_V254_DIRECT_EXPOSURE_TARGET_CONFIGURATION_HASH= # Anonymous capability sessions CAPABILITY_PEPPER=replace-with-32-random-bytes @@ -129,11 +137,15 @@ GUIDANCE_CONTRACT_V2_OWNER_CANARY=true GUIDANCE_CONTRACT_V3_ENABLED=false GUIDANCE_CONTRACT_V3_OWNER_CANARY=false GUIDANCE_CONTRACT_V3_REGGAETON_ENABLED=false -# Dormant until the schema-21/protocol-13 v2.6 activation gates pass. +# Guidance V5.1 is schema-20/protocol-12 compatible for the v2.5.4 +# editorial-influence recovery. Keep it false in an unassigned local +# environment; the immutable production release pins it true on every lane. +# Schema-21/protocol-13 remains a separate v2.6 migration. GUIDANCE_V5_ENABLED=false PIPELINE_V3_OWNER_CANARY=false PIPELINE_V3_OWNER_CANARY_GROUPS=genre_scene PIPELINE_V3_OWNER_CANARY_MAX_TRACKS=50 +PIPELINE_V3_EDITORIAL_INFLUENCE_PERCENT=0 PIPELINE_V3_GENRE_SCENE_PERCENT=0 PIPELINE_V3_MOOD_ACTIVITY_PERCENT=0 PIPELINE_V3_SIMILARITY_PERCENT=0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3d3b6..800415e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,6 +182,64 @@ jobs: test-results/ if-no-files-found: ignore + stitched-system: + needs: [static] + runs-on: ubuntu-latest + timeout-minutes: 45 + services: + postgres: + image: postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193 + env: + POSTGRES_DB: needle + POSTGRES_USER: needle + POSTGRES_PASSWORD: needle + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U needle -d needle" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgresql://needle:needle@127.0.0.1:5432/needle + APP_ORIGIN: http://localhost:3000 + RAILWAY_API_BASE: http://127.0.0.1:8788 + GATEWAY_KEY_ID: ci-system-v1 + GATEWAY_HMAC_SECRET: 0123456789abcdef0123456789abcdef + IP_HASH_SECRET: abcdef0123456789abcdef0123456789 + CAPABILITY_PEPPER: fedcba9876543210fedcba9876543210 + CAPABILITY_PEPPER_VERSION: ci-system-capability-v1 + SESSION_SECRET: 00112233445566778899aabbccddeeff + OWNER_EMAIL: owner@example.com + OWNER_ALLOWLIST_VERSION: ci-system-owner-allowlist-v1 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm test:database:preflight + - run: pnpm db:migrate + - name: Install headless Chromium + run: pnpm exec playwright install --with-deps chromium-headless-shell + - name: Run mandatory stitched API-worker-database-browser suite + run: pnpm test:e2e:system + - name: Upload stitched-system failure artifacts + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: stitched-system + path: | + playwright-report/ + test-results/ + if-no-files-found: ignore + aggregate-exact-sha: name: aggregate exact-SHA gate if: always() @@ -191,6 +249,7 @@ jobs: - unit-and-database - production-database-compatibility - browser + - stitched-system runs-on: ubuntu-latest timeout-minutes: 5 permissions: diff --git a/.github/workflows/exact-sha-image.yml b/.github/workflows/exact-sha-image.yml index 3796edf..35ed66a 100644 --- a/.github/workflows/exact-sha-image.yml +++ b/.github/workflows/exact-sha-image.yml @@ -11,6 +11,10 @@ on: description: Stable semantic version embedded in the image required: true type: string + candidate_tag: + description: Annotated RC tag bound to the exact merged SHA + required: true + type: string concurrency: group: exact-sha-image-${{ inputs.source_revision }} @@ -37,6 +41,7 @@ jobs: env: SOURCE_REVISION: ${{ inputs.source_revision }} RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} RELEASE_VERIFICATION_KEY_SHA256: ${{ vars.RELEASE_VERIFICATION_KEY_SHA256 }} RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_AUTHORITY_POLICY_SHA256: ${{ vars.RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_AUTHORITY_POLICY_SHA256 }} run: | @@ -44,6 +49,8 @@ jobs: || { echo "source_revision must be a lowercase full SHA-1" >&2; exit 1; } [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] \ || { echo "release_version must be a stable semantic version" >&2; exit 1; } + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] \ + || { echo "candidate_tag must match the stable release version" >&2; exit 1; } [[ "$RELEASE_VERIFICATION_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] \ || { echo "RELEASE_VERIFICATION_KEY_SHA256 must pin the rollout verification key" >&2; exit 1; } [[ "$RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_AUTHORITY_POLICY_SHA256" =~ ^[0-9a-f]{64}$ ]] \ @@ -126,6 +133,7 @@ jobs: env: SOURCE_REVISION: ${{ inputs.source_revision }} RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" @@ -133,6 +141,10 @@ jobs: git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" test -f Dockerfile.ghcr-recovery + git fetch --force origin "+refs/tags/$CANDIDATE_TAG:refs/tags/$CANDIDATE_TAG" + TAG_OBJECT="$(git rev-parse "refs/tags/$CANDIDATE_TAG")" + test "$(git cat-file -t "$TAG_OBJECT")" = "tag" + test "$(git rev-parse "$TAG_OBJECT^{}")" = "$SOURCE_REVISION" - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io @@ -219,9 +231,20 @@ jobs: echo "- Version: \`$RELEASE_VERSION\`" echo "- Image: \`$IMAGE_REFERENCE\`" } >> "$GITHUB_STEP_SUMMARY" + - name: Produce native v2.5.4 candidate evidence from the exact-SHA receipt + env: + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + run: | + node --experimental-transform-types \ + scripts/native-v254-candidate-evidence.ts \ + --exact-sha-image-receipt exact-sha-image-receipt.json \ + --candidate-tag "$CANDIDATE_TAG" \ + --output native-v254-candidate-evidence.json - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: exact-sha-image-${{ inputs.source_revision }} - path: exact-sha-image-receipt.json + path: | + exact-sha-image-receipt.json + native-v254-candidate-evidence.json if-no-files-found: error retention-days: 30 diff --git a/.github/workflows/native-schema20-release-finalize.yml b/.github/workflows/native-schema20-release-finalize.yml new file mode 100644 index 0000000..617c39a --- /dev/null +++ b/.github/workflows/native-schema20-release-finalize.yml @@ -0,0 +1,513 @@ +name: native schema20 release finalization + +on: + workflow_dispatch: + inputs: + source_revision: + description: Full merged Git SHA already promoted + required: true + type: string + release_version: + description: Candidate version + required: true + type: string + candidate_tag: + description: Immutable RC tag + required: true + type: string + backend_promotion_run_id: + description: Successful native schema20 point-release run + required: true + type: string + direct_exposure_run_id: + description: Successful signed direct 0 to 100 exposure run + required: true + type: string + direct_exposure_artifact: + description: Artifact containing authority, warrant, activation, and runtime receipts + required: true + type: string + production_proof_run_id: + description: Run with post-exposure convergence and real public browser proof + required: true + type: string + production_proof_artifact: + description: Combined exact-SHA convergence and direct-browser artifact + required: true + type: string + apple_gate_run_id: + description: Run with signed owner fixed-three control plus Irish V5.1 execution, recovery, and ordered Apple-ID evidence + required: true + type: string + apple_gate_artifact: + description: Artifact containing mandatory production_fixed_three_track and production_affected_regression gates and attestations + required: true + type: string + burn_in_run_id: + description: Successful final 24-hour frozen-candidate burn-in segment + required: true + type: string + burn_in_artifact: + description: Artifact containing the completed burn-in receipt + required: true + type: string + +concurrency: + group: native-schema20-production-release-finalization + cancel-in-progress: false + +permissions: + actions: read + contents: write + +jobs: + finalize: + name: validate all proof and publish the exact stable release + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64: ${{ secrets.RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64 }} + RELEASE_GATE_PRODUCER_KEY_ID: ${{ vars.RELEASE_GATE_PRODUCER_KEY_ID }} + RELEASE_GATE_PRODUCER_KEY_SHA256: ${{ vars.RELEASE_GATE_PRODUCER_KEY_SHA256 }} + RELEASE_VERIFICATION_PUBLIC_KEY_BASE64: ${{ secrets.RELEASE_VERIFICATION_PUBLIC_KEY_BASE64 }} + RELEASE_VERIFICATION_KEY_ID: ${{ vars.RELEASE_VERIFICATION_KEY_ID }} + RELEASE_VERIFICATION_KEY_SHA256: ${{ vars.RELEASE_VERIFICATION_KEY_SHA256 }} + steps: + - name: Validate exact evidence selectors + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + DIRECT_RUN_ID: ${{ inputs.direct_exposure_run_id }} + DIRECT_ARTIFACT: ${{ inputs.direct_exposure_artifact }} + PROOF_RUN_ID: ${{ inputs.production_proof_run_id }} + PROOF_ARTIFACT: ${{ inputs.production_proof_artifact }} + APPLE_RUN_ID: ${{ inputs.apple_gate_run_id }} + APPLE_ARTIFACT: ${{ inputs.apple_gate_artifact }} + BURN_IN_RUN_ID: ${{ inputs.burn_in_run_id }} + BURN_IN_ARTIFACT: ${{ inputs.burn_in_artifact }} + run: | + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$BACKEND_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$DIRECT_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$PROOF_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$APPLE_RUN_ID" =~ ^[1-9][0-9]*$ ]] + test "$DIRECT_ARTIFACT" = "v254-editorial-direct-exposure-$SOURCE_REVISION" + test "$PROOF_ARTIFACT" = "v254-production-proof-$SOURCE_REVISION" + test "$APPLE_ARTIFACT" = "v254-owner-apple-gate-$SOURCE_REVISION" + [[ "$BURN_IN_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$BURN_IN_ARTIFACT" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]] + [[ "$RELEASE_GATE_PRODUCER_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:-]{2,79}$ ]] + [[ "$RELEASE_GATE_PRODUCER_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + [[ "$RELEASE_VERIFICATION_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:-]{2,79}$ ]] + [[ "$RELEASE_VERIFICATION_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + test -n "$RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64" + test -n "$RELEASE_VERIFICATION_PUBLIC_KEY_BASE64" + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require exact merged candidate + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + test "$GITHUB_SHA" = "$SOURCE_REVISION" + test "$GITHUB_WORKFLOW_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + git fetch --no-tags origin "$DEFAULT_BRANCH" + git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" + git fetch --force origin \ + "+refs/tags/$CANDIDATE_TAG:refs/tags/$CANDIDATE_TAG" + test "$(git cat-file -t "refs/tags/$CANDIDATE_TAG")" = "tag" + test "$(git rev-parse "refs/tags/$CANDIDATE_TAG^{}")" = "$SOURCE_REVISION" + + - name: Require successful evidence-producing runs for the exact SHA + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + DIRECT_RUN_ID: ${{ inputs.direct_exposure_run_id }} + PROOF_RUN_ID: ${{ inputs.production_proof_run_id }} + APPLE_RUN_ID: ${{ inputs.apple_gate_run_id }} + BURN_IN_RUN_ID: ${{ inputs.burn_in_run_id }} + run: | + for RUN_ID in "$BACKEND_RUN_ID" "$DIRECT_RUN_ID" "$PROOF_RUN_ID" "$APPLE_RUN_ID" "$BURN_IN_RUN_ID"; do + RUN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID")" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + done + BACKEND="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BACKEND_RUN_ID")" + test "$(jq -r '.path' <<<"$BACKEND")" = ".github/workflows/native-schema20-release.yml" + DIRECT="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$DIRECT_RUN_ID")" + test "$(jq -r '.path' <<<"$DIRECT")" = ".github/workflows/v254-editorial-direct-exposure.yml" + PROOF="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$PROOF_RUN_ID")" + test "$(jq -r '.path' <<<"$PROOF")" = ".github/workflows/v254-production-proof.yml" + APPLE="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$APPLE_RUN_ID")" + test "$(jq -r '.path' <<<"$APPLE")" = ".github/workflows/v254-owner-apple-gate.yml" + BURN_IN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BURN_IN_RUN_ID")" + test "$(jq -r '.path' <<<"$BURN_IN")" = ".github/workflows/v254-production-burn-in.yml" + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: native-schema20-promotion-${{ inputs.source_revision }} + path: ${{ runner.temp }}/backend + run-id: ${{ inputs.backend_promotion_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.direct_exposure_artifact }} + path: ${{ runner.temp }}/direct + run-id: ${{ inputs.direct_exposure_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.production_proof_artifact }} + path: ${{ runner.temp }}/proof + run-id: ${{ inputs.production_proof_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.apple_gate_artifact }} + path: ${{ runner.temp }}/apple + run-id: ${{ inputs.apple_gate_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.burn_in_artifact }} + path: ${{ runner.temp }}/burn-in + run-id: ${{ inputs.burn_in_run_id }} + github-token: ${{ github.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Install exact lockfile + run: pnpm install --frozen-lockfile + + - name: Materialize protected producer and direct-authority verification keys + run: | + umask 077 + printf '%s' "$RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64" \ + | base64 --decode \ + > "$RUNNER_TEMP/release-gate-producer-public-key.pem" + printf '%s' "$RELEASE_VERIFICATION_PUBLIC_KEY_BASE64" \ + | base64 --decode \ + > "$RUNNER_TEMP/release-verification-public-key.pem" + + - name: Fail closed unless all final production gates pass + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + run: | + pnpm release:native:finalize -- \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --backend-promotion "$RUNNER_TEMP/backend/native-schema20-promotion.json" \ + --direct-exposure-authority "$RUNNER_TEMP/direct/v254-direct-exposure-authority.json" \ + --direct-exposure-rollback-warrant "$RUNNER_TEMP/direct/v254-direct-exposure-rollback-warrant.json" \ + --direct-exposure-database-activate-receipt "$RUNNER_TEMP/direct/direct-exposure-database-activate.json" \ + --direct-exposure-runtime-receipt "$RUNNER_TEMP/direct/direct-exposure-runtime-apply.json" \ + --direct-exposure-verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --direct-exposure-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --direct-exposure-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --producer-verification-key "$RUNNER_TEMP/release-gate-producer-public-key.pem" \ + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" \ + --producer-key-sha256 "$RELEASE_GATE_PRODUCER_KEY_SHA256" \ + --release-convergence-artifact "$RUNNER_TEMP/proof/release-convergence.gate.json" \ + --release-convergence-attestation "$RUNNER_TEMP/proof/release-convergence.attestation.json" \ + --final-browser-artifact "$RUNNER_TEMP/proof/final-custom-domain-browser.gate.json" \ + --final-browser-attestation "$RUNNER_TEMP/proof/final-custom-domain-browser.attestation.json" \ + --fixed-three-artifact "$RUNNER_TEMP/apple/production-fixed-three-track.gate.json" \ + --fixed-three-attestation "$RUNNER_TEMP/apple/production-fixed-three-track.attestation.json" \ + --apple-artifact "$RUNNER_TEMP/apple/production-affected-regression.gate.json" \ + --apple-attestation "$RUNNER_TEMP/apple/production-affected-regression.attestation.json" \ + --burn-in-receipt "$RUNNER_TEMP/burn-in/v254-production-burn-in.json" \ + --output "$RUNNER_TEMP/native-schema20-finalization.json" + + - name: Prepare exact stable tag and immutable Release payload + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + run: | + pnpm release:native:prepare-stable -- \ + --confirm-native-stable-release \ + --finalization-receipt "$RUNNER_TEMP/native-schema20-finalization.json" \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --output "$RUNNER_TEMP/native-schema20-stable-release.json" \ + --notes-output "$RUNNER_TEMP/native-schema20-stable-release-notes.txt" + + - id: stable_writer + name: Mint the existing stable-tag writer when configured + if: vars.STABLE_RELEASE_WRITER_APP_ID != '' + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ vars.STABLE_RELEASE_WRITER_APP_ID }} + private-key: ${{ secrets.STABLE_RELEASE_WRITER_APP_PRIVATE_KEY }} + permission-contents: write + + - name: Create or verify the exact annotated stable tag + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + STABLE_PLAN_PATH: ${{ runner.temp }}/native-schema20-stable-release.json + STABLE_NOTES_PATH: ${{ runner.temp }}/native-schema20-stable-release-notes.txt + with: + github-token: >- + ${{ steps.stable_writer.outputs.token || github.token }} + script: | + const fs = require("node:fs"); + const plan = JSON.parse( + fs.readFileSync(process.env.STABLE_PLAN_PATH, "utf8"), + ); + const message = fs.readFileSync( + process.env.STABLE_NOTES_PATH, + "utf8", + ); + const tagName = plan.candidate.stableTag; + const sourceRevision = plan.candidate.sourceRevision; + const versionParts = (value) => { + const match = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/ + .exec(value); + return match + ? match.slice(1).map((part) => Number(part)) + : null; + }; + const compareVersions = (left, right) => { + for (let index = 0; index < 3; index += 1) { + if (left[index] !== right[index]) { + return left[index] > right[index] ? 1 : -1; + } + } + return 0; + }; + const candidateVersion = versionParts(tagName); + if (!candidateVersion) { + throw new Error("native stable tag version is invalid"); + } + const [refs, releases] = await Promise.all([ + github.paginate(github.rest.git.listMatchingRefs, { + owner: context.repo.owner, + repo: context.repo.repo, + ref: "tags/v", + per_page: 100, + }), + github.paginate(github.rest.repos.listReleases, { + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 100, + }), + ]); + const publishedVersions = [ + ...refs.map((ref) => ref.ref.replace(/^refs\/tags\//, "")), + ...releases.map((release) => release.tag_name), + ]; + if (publishedVersions.some((published) => { + const version = versionParts(published); + return version + && compareVersions(version, candidateVersion) > 0; + })) { + throw new Error( + "a newer stable tag or GitHub Release already exists", + ); + } + + async function verifyTag() { + const ref = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tagName}`, + }); + if (ref.data.object.type !== "tag") { + throw new Error("native stable ref is not an annotated tag"); + } + const tag = await github.rest.git.getTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_sha: ref.data.object.sha, + }); + if ( + tag.data.tag !== tagName + || tag.data.object.type !== "commit" + || tag.data.object.sha !== sourceRevision + || tag.data.message !== message + ) { + throw new Error( + "existing native stable tag has a different identity", + ); + } + return ref.data.object.sha; + } + + let tagObject; + try { + tagObject = await verifyTag(); + } catch (error) { + if (error.status !== 404) throw error; + const created = await github.rest.git.createTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: tagName, + message, + object: sourceRevision, + type: "commit", + }); + try { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/tags/${tagName}`, + sha: created.data.sha, + }); + } catch (createError) { + if (createError.status !== 422) throw createError; + } + tagObject = await verifyTag(); + } + core.info( + `Verified ${tagName} annotated object ${tagObject} at ` + + sourceRevision, + ); + + - name: Create, complete, or verify the exact GitHub Release + env: + GH_TOKEN: >- + ${{ steps.stable_writer.outputs.token || github.token }} + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + STABLE_TAG="v$RELEASE_VERSION" + NOTES="$RUNNER_TEMP/native-schema20-stable-release-notes.txt" + FINALIZATION="$RUNNER_TEMP/native-schema20-finalization.json" + STABLE_PLAN="$RUNNER_TEMP/native-schema20-stable-release.json" + RELEASE_JSON="$RUNNER_TEMP/native-schema20-stable-release-api.json" + if ! gh api \ + "repos/$GITHUB_REPOSITORY/releases/tags/$STABLE_TAG" \ + > "$RELEASE_JSON" 2>/dev/null; then + gh release create "$STABLE_TAG" \ + --draft \ + --verify-tag \ + --target "$SOURCE_REVISION" \ + --title "$STABLE_TAG" \ + --notes-file "$NOTES" + gh api \ + "repos/$GITHUB_REPOSITORY/releases/tags/$STABLE_TAG" \ + > "$RELEASE_JSON" + fi + jq -e \ + --arg tag "$STABLE_TAG" \ + --arg revision "$SOURCE_REVISION" \ + '.tag_name == $tag + and .name == $tag + and .target_commitish == $revision + and .prerelease == false' \ + "$RELEASE_JSON" >/dev/null + jq -r '.body // ""' "$RELEASE_JSON" \ + > "$RUNNER_TEMP/native-schema20-published-notes.txt" + cmp "$NOTES" "$RUNNER_TEMP/native-schema20-published-notes.txt" + RELEASE_DRAFT="$(jq -r '.draft' "$RELEASE_JSON")" + for ASSET_PATH in "$FINALIZATION" "$STABLE_PLAN"; do + ASSET="$(basename "$ASSET_PATH")" + ASSET_COUNT="$( + jq -r --arg asset "$ASSET" \ + '[.assets[] | select(.name == $asset)] | length' \ + "$RELEASE_JSON" + )" + if test "$ASSET_COUNT" = "0"; then + test "$RELEASE_DRAFT" = "true" + gh release upload "$STABLE_TAG" "$ASSET_PATH" + else + test "$ASSET_COUNT" = "1" + mkdir -p "$RUNNER_TEMP/published-assets" + gh release download "$STABLE_TAG" \ + --dir "$RUNNER_TEMP/published-assets" \ + --pattern "$ASSET" + cmp "$ASSET_PATH" "$RUNNER_TEMP/published-assets/$ASSET" + fi + done + gh api \ + "repos/$GITHUB_REPOSITORY/releases/tags/$STABLE_TAG" \ + > "$RELEASE_JSON" + test "$( + jq -r \ + '[.assets[].name] | sort + == ["native-schema20-finalization.json", + "native-schema20-stable-release.json"]' \ + "$RELEASE_JSON" + )" = "true" + if test "$(jq -r '.draft' "$RELEASE_JSON")" = "true"; then + gh release edit "$STABLE_TAG" --draft=false + fi + + - name: Verify stable source, tag, Release, notes, and assets read-only + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + STABLE_TAG="v$RELEASE_VERSION" + git fetch --force origin \ + "+refs/tags/$STABLE_TAG:refs/tags/$STABLE_TAG" + test "$(git cat-file -t "refs/tags/$STABLE_TAG")" = "tag" + test "$(git rev-parse "refs/tags/$STABLE_TAG^{}")" = "$SOURCE_REVISION" + pnpm release:check:native-stable -- "$STABLE_TAG" + RELEASE_JSON="$RUNNER_TEMP/native-schema20-published-release.json" + gh api \ + "repos/$GITHUB_REPOSITORY/releases/tags/$STABLE_TAG" \ + > "$RELEASE_JSON" + test "$(jq -r '.draft' "$RELEASE_JSON")" = "false" + test "$(jq -r '.prerelease' "$RELEASE_JSON")" = "false" + test "$(jq -r '.target_commitish' "$RELEASE_JSON")" = "$SOURCE_REVISION" + jq -r '.body // ""' "$RELEASE_JSON" \ + > "$RUNNER_TEMP/native-schema20-verified-notes.txt" + cmp \ + "$RUNNER_TEMP/native-schema20-stable-release-notes.txt" \ + "$RUNNER_TEMP/native-schema20-verified-notes.txt" + mkdir "$RUNNER_TEMP/verified-assets" + gh release download "$STABLE_TAG" \ + --dir "$RUNNER_TEMP/verified-assets" \ + --pattern native-schema20-finalization.json \ + --pattern native-schema20-stable-release.json + cmp \ + "$RUNNER_TEMP/native-schema20-finalization.json" \ + "$RUNNER_TEMP/verified-assets/native-schema20-finalization.json" + cmp \ + "$RUNNER_TEMP/native-schema20-stable-release.json" \ + "$RUNNER_TEMP/verified-assets/native-schema20-stable-release.json" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: native-schema20-finalization-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/native-schema20-finalization.json + ${{ runner.temp }}/native-schema20-stable-release.json + ${{ runner.temp }}/native-schema20-stable-release-notes.txt + ${{ runner.temp }}/native-schema20-published-release.json + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/native-schema20-release.yml b/.github/workflows/native-schema20-release.yml new file mode 100644 index 0000000..54f0cb0 --- /dev/null +++ b/.github/workflows/native-schema20-release.yml @@ -0,0 +1,427 @@ +name: native schema20 point release + +on: + workflow_dispatch: + inputs: + source_revision: + description: Full merged Git SHA already built by exact SHA image + required: true + type: string + release_version: + description: Stable release version embedded in the image + required: true + type: string + candidate_tag: + description: RC tag bound by the exact-SHA candidate receipt + required: true + type: string + image_digest: + description: Immutable exact-SHA GHCR image digest + required: true + type: string + exact_sha_image_run_id: + description: Successful exact SHA image workflow run ID + required: true + type: string + prior_sites_revision: + description: Full revision currently served by Sites + required: true + type: string + prior_sites_version: + description: Version currently served by Sites + required: true + type: string + prior_sites_version_id: + description: Saved Sites version ID currently serving production + required: true + type: string + prior_sites_deployment_id: + description: Sites deployment ID currently serving production + required: true + type: string + prior_sites_control_plane_evidence_hash: + description: SHA-256 of the independently verified prior Sites control-plane evidence + required: true + type: string + +concurrency: + group: native-schema20-production-release + cancel-in-progress: false + +permissions: + actions: read + contents: read + +jobs: + promote: + name: workers first, API last + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + V254_IRISH_INFLUENCE_INCIDENT_BINDING_BASE64: ${{ secrets.V254_IRISH_INFLUENCE_INCIDENT_BINDING_BASE64 }} + RELEASE_VERIFICATION_KEY_SHA256: ${{ vars.RELEASE_VERIFICATION_KEY_SHA256 }} + RELEASE_VERIFICATION_KEY_ID: ${{ vars.RELEASE_VERIFICATION_KEY_ID }} + RELEASE_SECRET_VERSIONS_HASH: ${{ vars.RELEASE_SECRET_VERSIONS_HASH }} + # Code-owned production targets. The orchestrator also verifies each ID's + # existing Railway name and refuses stopped, renamed, or missing targets. + RAILWAY_PROJECT_ID: 7e7a44ee-3e54-453c-9120-3cb1f33db76e + RAILWAY_INTERACTIVE_SERVICE_ID: 3affc435-074d-4f6f-806f-1c2b552a8b17 + RAILWAY_DEEP_SERVICE_ID: a3469457-f56e-4871-8a10-5ea6c052640c + RAILWAY_API_SERVICE_ID: 11ba75aa-2495-4f28-bbde-64c43efbe731 + steps: + - name: Validate immutable release selectors + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + EXACT_SHA_IMAGE_RUN_ID: ${{ inputs.exact_sha_image_run_id }} + PRIOR_SITES_REVISION: ${{ inputs.prior_sites_revision }} + PRIOR_SITES_VERSION: ${{ inputs.prior_sites_version }} + PRIOR_SITES_VERSION_ID: ${{ inputs.prior_sites_version_id }} + PRIOR_SITES_DEPLOYMENT_ID: ${{ inputs.prior_sites_deployment_id }} + PRIOR_SITES_CONTROL_PLANE_EVIDENCE_HASH: ${{ inputs.prior_sites_control_plane_evidence_hash }} + run: | + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] + [[ "$EXACT_SHA_IMAGE_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$PRIOR_SITES_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$PRIOR_SITES_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] + [[ "$PRIOR_SITES_VERSION_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:+/-]{0,255}$ ]] + [[ "$PRIOR_SITES_DEPLOYMENT_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:+/-]{0,255}$ ]] + [[ "$PRIOR_SITES_CONTROL_PLANE_EVIDENCE_HASH" =~ ^[0-9a-f]{64}$ ]] + [[ "$RELEASE_VERIFICATION_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:+/-]{0,159}$ ]] + [[ "$RELEASE_VERIFICATION_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + [[ "$RELEASE_SECRET_VERSIONS_HASH" =~ ^[0-9a-f]{64}$ ]] + [[ "$RAILWAY_PROJECT_ID" =~ ^[0-9a-f-]{36}$ ]] + [[ "$RAILWAY_INTERACTIVE_SERVICE_ID" =~ ^[0-9a-f-]{36}$ ]] + [[ "$RAILWAY_DEEP_SERVICE_ID" =~ ^[0-9a-f-]{36}$ ]] + [[ "$RAILWAY_API_SERVICE_ID" =~ ^[0-9a-f-]{36}$ ]] + test -n "$RAILWAY_TOKEN" + test -n "$V254_IRISH_INFLUENCE_INCIDENT_BINDING_BASE64" + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require exact merged source + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + git fetch --no-tags origin "$DEFAULT_BRANCH" + git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" + + - name: Verify exact SHA image workflow provenance + env: + EXACT_SHA_IMAGE_RUN_ID: ${{ inputs.exact_sha_image_run_id }} + SOURCE_REVISION: ${{ inputs.source_revision }} + run: | + RUN="$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$EXACT_SHA_IMAGE_RUN_ID" + )" + test "$(jq -r '.path' <<<"$RUN")" = ".github/workflows/exact-sha-image.yml" + test "$(jq -r '.event' <<<"$RUN")" = "workflow_dispatch" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: exact-sha-image-${{ inputs.source_revision }} + path: ${{ runner.temp }}/exact-sha-image + run-id: ${{ inputs.exact_sha_image_run_id }} + github-token: ${{ github.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Install exact lockfile + run: pnpm install --frozen-lockfile + + - name: Install pinned Railway CLI + run: | + npm install --global @railway/cli@4.65.0 + test "$(railway --version)" = "railway 4.65.0" + + - name: Apply and receipt-bind the schema-20 Guidance V5.1 migration + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + run: | + set -euo pipefail + railway run \ + --service Postgres \ + --project "$RAILWAY_PROJECT_ID" \ + --environment production \ + --no-local -- \ + /bin/sh -eu -c \ + 'test -n "${DATABASE_PUBLIC_URL:-}"; DATABASE_URL="$DATABASE_PUBLIC_URL" PGSSLMODE=no-verify pnpm db:migrate' + railway run \ + --service Postgres \ + --project "$RAILWAY_PROJECT_ID" \ + --environment production \ + --no-local -- \ + pnpm release:v254:guidance-migration-receipt -- \ + --source-revision "$SOURCE_REVISION" \ + --migration-file postgres-migrations/0024_guidance_v5_execution_decision.sql \ + --output "$RUNNER_TEMP/v254-guidance-migration-receipt.json" + jq -e \ + --arg revision "$SOURCE_REVISION" \ + '.schemaVersion == "genio-v254-guidance-migration-receipt/v1" + and .sourceRevision == $revision + and .migrationTag == "0024_guidance_v5_execution_decision" + and .databaseSchemaVersion == "20" + and .guidanceCheckpointVersion == "5.1" + and .constraintValidated == true + and .executionDecisionAccepted == true + and .migrationFileSha256 == .drizzleMigrationHash + and (.receiptHash | test("^[0-9a-f]{64}$"))' \ + "$RUNNER_TEMP/v254-guidance-migration-receipt.json" + + - name: Contain affected editorial-influence work + run: | + railway run \ + --service Postgres \ + --project "$RAILWAY_PROJECT_ID" \ + --environment production \ + --no-local -- \ + pnpm release:v254:contain -- pause \ + > "$RUNNER_TEMP/v254-containment-pause.json" + railway run \ + --service Postgres \ + --project "$RAILWAY_PROJECT_ID" \ + --environment production \ + --no-local -- \ + pnpm release:v254:contain -- contain-dry-run \ + > "$RUNNER_TEMP/v254-containment-dry-run.json" + CONTAINMENT_HASH="$( + jq -er '.receiptHash | select(test(\"^[0-9a-f]{64}$\"))' \ + "$RUNNER_TEMP/v254-containment-dry-run.json" + )" + OWNER_REVIEW_PROOF_HASH="$( + jq -er \ + '.ownerReviewPromotionProofHash + | select(test("^[0-9a-f]{64}$"))' \ + "$RUNNER_TEMP/v254-containment-dry-run.json" + )" + jq -e \ + '.mode == "contain-dry-run" + and .safeToApply == true + and (has("receipt") | not) + and (has("observed") | not) + and ([ + .. | objects | keys[] | select( + . == "run_id" + or . == "runId" + or . == "access_id" + or . == "accessId" + or . == "contract_revision_id" + or . == "contractRevisionId" + or . == "query_plan_revision_id" + or . == "queryPlanRevisionId" + or . == "execution_attempt_id" + or . == "executionAttemptId" + or . == "blocker_id" + or . == "blockerId" + ) + ] | length == 0) + and (.ownerReviewCandidateCount + | type == "number" and . >= 0 and floor == .) + and (.ownerReviewDispositionCount + | type == "number" and . >= 0 and floor == .) + and (.ownerReviewUndispositionedCount + | type == "number" and . >= 0 and floor == .) + and (.ownerReviewUnresolvedExecutableWorkCount + | type == "number" and . >= 0 and floor == .) + and (.ownerReviewUnresolvedPublicationWorkCount + | type == "number" and . >= 0 and floor == .) + and (.ownerReviewPromotionSafe | type == "boolean") + and .ownerReviewCandidateCount + == .ownerReviewPromotionProof.candidateCount + and .ownerReviewDispositionCount + == .ownerReviewPromotionProof.dispositionCount + and .ownerReviewUndispositionedCount + == .ownerReviewPromotionProof.undispositionedCount + and (.expectedMutationCountsHash + | test("^[0-9a-f]{64}$")) + and (.actualMutationCountsHash + | test("^[0-9a-f]{64}$")) + and (.actualMutationCounts + | [.[]] | all(. == 0))' \ + "$RUNNER_TEMP/v254-containment-dry-run.json" + railway run \ + --service Postgres \ + --project "$RAILWAY_PROJECT_ID" \ + --environment production \ + --no-local -- \ + pnpm release:v254:contain -- contain-apply "$CONTAINMENT_HASH" \ + > "$RUNNER_TEMP/v254-containment.json" + jq -e \ + --arg receipt_hash "$CONTAINMENT_HASH" \ + --arg owner_review_proof_hash "$OWNER_REVIEW_PROOF_HASH" \ + '.mode == "contain-apply" + and (.applied == true or .applied == false) + and .receiptHash == $receipt_hash + and .ownerReviewPromotionProofHash + == $owner_review_proof_hash + and .ownerReviewPromotionSafe == true + and ( + .ownerReviewCandidateCount == 0 + or ( + .ownerReviewDispositionCount + == .ownerReviewCandidateCount + and .ownerReviewUndispositionedCount == 0 + and .ownerReviewUnresolvedExecutableWorkCount == 0 + and .ownerReviewUnresolvedPublicationWorkCount == 0 + ) + ) + and .affectedRunCount >= 1 + and .hardSwitchRemainsEngaged == true + and .intentPublicPauseRemainsEngaged == true + and .expectedMutationCounts == .actualMutationCounts + and .actualMutationCounts == .mutationCounts + and (.expectedMutationCountsHash + | test("^[0-9a-f]{64}$")) + and (.actualMutationCountsHash + | test("^[0-9a-f]{64}$")) + and (.mutationCountsHash + | test("^[0-9a-f]{64}$")) + and .safeRoutesResumed == true' \ + "$RUNNER_TEMP/v254-containment.json" + + - name: Promote one immutable image through existing services + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + PRIOR_SITES_REVISION: ${{ inputs.prior_sites_revision }} + PRIOR_SITES_VERSION: ${{ inputs.prior_sites_version }} + run: | + pnpm release:native:promote -- \ + --exact-sha-image-receipt "$RUNNER_TEMP/exact-sha-image/exact-sha-image-receipt.json" \ + --candidate-evidence "$RUNNER_TEMP/exact-sha-image/native-v254-candidate-evidence.json" \ + --containment-receipt "$RUNNER_TEMP/v254-containment.json" \ + --guidance-migration-receipt "$RUNNER_TEMP/v254-guidance-migration-receipt.json" \ + --release-verification-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --secret-versions-hash "$RELEASE_SECRET_VERSIONS_HASH" \ + --expected-image-repository ghcr.io/hooterjackson/genio \ + --origin https://9enio.com \ + --prior-sites-revision "$PRIOR_SITES_REVISION" \ + --prior-sites-version "$PRIOR_SITES_VERSION" \ + --project-id "$RAILWAY_PROJECT_ID" \ + --environment production \ + --interactive-service "$RAILWAY_INTERACTIVE_SERVICE_ID" \ + --deep-service "$RAILWAY_DEEP_SERVICE_ID" \ + --api-service "$RAILWAY_API_SERVICE_ID" \ + --deployment-timeout-seconds 600 \ + --poll-interval-seconds 10 \ + --output "$RUNNER_TEMP/native-schema20-promotion.json" + + - name: Sign the native promotion authority with the protected release key + env: + RELEASE_PUBLIC_ROLLOUT_EVIDENCE_SIGNING_KEY_BASE64: ${{ secrets.RELEASE_PUBLIC_ROLLOUT_EVIDENCE_SIGNING_KEY_BASE64 }} + SOURCE_REVISION: ${{ inputs.source_revision }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + PRIOR_SITES_REVISION: ${{ inputs.prior_sites_revision }} + PRIOR_SITES_VERSION: ${{ inputs.prior_sites_version }} + PRIOR_SITES_VERSION_ID: ${{ inputs.prior_sites_version_id }} + PRIOR_SITES_DEPLOYMENT_ID: ${{ inputs.prior_sites_deployment_id }} + PRIOR_SITES_CONTROL_PLANE_EVIDENCE_HASH: ${{ inputs.prior_sites_control_plane_evidence_hash }} + run: | + set -euo pipefail + umask 077 + test -n "$RELEASE_PUBLIC_ROLLOUT_EVIDENCE_SIGNING_KEY_BASE64" + printf '%s' "$RELEASE_PUBLIC_ROLLOUT_EVIDENCE_SIGNING_KEY_BASE64" \ + | base64 --decode > "$RUNNER_TEMP/release-signing-key.pem" + pnpm release:native:promotion-authority -- \ + --promotion-receipt "$RUNNER_TEMP/native-schema20-promotion.json" \ + --candidate-tag "$CANDIDATE_TAG" \ + --expected-source-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-image-digest "$IMAGE_DIGEST" \ + --prior-sites-project-id appgprj_6a5565cf7d6c8191ab9f2084e8eda856 \ + --prior-sites-version-id "$PRIOR_SITES_VERSION_ID" \ + --prior-sites-deployment-id "$PRIOR_SITES_DEPLOYMENT_ID" \ + --prior-sites-version "$PRIOR_SITES_VERSION" \ + --prior-sites-revision "$PRIOR_SITES_REVISION" \ + --prior-sites-control-plane-evidence-hash "$PRIOR_SITES_CONTROL_PLANE_EVIDENCE_HASH" \ + --signing-key "$RUNNER_TEMP/release-signing-key.pem" \ + --key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --output "$RUNNER_TEMP/native-schema20-rollout-promotion-authority.json" \ + > "$RUNNER_TEMP/native-schema20-rollout-promotion-authority-result.json" + jq -e \ + --arg revision "$SOURCE_REVISION" \ + '.payload.candidate.sourceRevision == $revision + and .payload.sites.candidateMatched == false + and (.payloadHash | test("^[0-9a-f]{64}$"))' \ + "$RUNNER_TEMP/native-schema20-rollout-promotion-authority.json" + rm "$RUNNER_TEMP/release-signing-key.pem" + + - name: Fail closed on sensitive promotion artifacts + run: | + set -euo pipefail + node - <<'NODE' + const fs = require("node:fs"); + const paths = [ + process.env.RUNNER_TEMP + "/native-schema20-promotion.json", + process.env.RUNNER_TEMP + "/native-schema20-rollout-promotion-authority.json", + process.env.RUNNER_TEMP + "/v254-guidance-migration-receipt.json", + process.env.RUNNER_TEMP + "/v254-containment-pause.json", + process.env.RUNNER_TEMP + "/v254-containment-dry-run.json", + process.env.RUNNER_TEMP + "/v254-containment.json", + ]; + const forbiddenKey = /(?:prompt|rawPrompt|runId|run_id|accessId|access_id|contractRevisionId|contract_revision_id|queryPlanRevisionId|query_plan_revision_id|executionAttemptId|execution_attempt_id|blockerId|blocker_id|credential|secret|token|authorization)/i; + const forbiddenValue = /(?:Bearer\s+|gh[opusr]_|sk-[A-Za-z0-9]|-----BEGIN (?:OPENSSH |RSA |EC |)PRIVATE KEY-----)/; + const visit = (value) => { + if (typeof value === "string" && forbiddenValue.test(value)) throw new Error("sensitive promotion artifact value"); + if (Array.isArray(value)) return value.forEach(visit); + if (!value || typeof value !== "object") return; + for (const [key, child] of Object.entries(value)) { + if (forbiddenKey.test(key)) throw new Error("sensitive promotion artifact key"); + visit(child); + } + }; + paths.forEach((path) => visit(JSON.parse(fs.readFileSync(path, "utf8")))); + NODE + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: native-schema20-promotion-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/native-schema20-promotion.json + ${{ runner.temp }}/v254-containment-pause.json + ${{ runner.temp }}/v254-containment-dry-run.json + ${{ runner.temp }}/v254-containment.json + ${{ runner.temp }}/v254-guidance-migration-receipt.json + if-no-files-found: error + retention-days: 30 + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: native-schema20-rollout-promotion-authority-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/native-schema20-rollout-promotion-authority.json + ${{ runner.temp }}/native-schema20-rollout-promotion-authority-result.json + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/v254-editorial-direct-exposure.yml b/.github/workflows/v254-editorial-direct-exposure.yml new file mode 100644 index 0000000..4a70934 --- /dev/null +++ b/.github/workflows/v254-editorial-direct-exposure.yml @@ -0,0 +1,463 @@ +name: v254 editorial direct exposure + +on: + workflow_dispatch: + inputs: + source_revision: + description: Exact merged v2.5.4 source revision + required: true + type: string + release_version: + description: Exact candidate version + required: true + type: string + candidate_tag: + description: Immutable annotated RC tag + required: true + type: string + image_digest: + description: Immutable promoted backend image digest + required: true + type: string + backend_promotion_run_id: + description: Successful native schema20 promotion and signed authority run + required: true + type: string + sites_handoff_run_id: + description: Successful exact-SHA Sites connector handoff run + required: true + type: string + sites_handoff_artifact: + description: Exact Sites connector handoff artifact + required: true + type: string + owner_gate_run_id: + description: Successful owner Apple acceptance run + required: true + type: string + owner_gate_artifact: + description: Exact owner Apple acceptance artifact + required: true + type: string + route_authority_run_id: + description: Successful paused pre-exposure database route run + required: true + type: string + route_authority_artifact: + description: Exact paused pre-exposure database route artifact + required: true + type: string + clean_gate_run_id: + description: Successful zero-write clean non-owner browser run + required: true + type: string + clean_gate_artifact: + description: Exact zero-write clean non-owner browser artifact + required: true + type: string + +concurrency: + group: v254-editorial-direct-exposure + cancel-in-progress: false + +permissions: + actions: read + contents: read + +jobs: + expose: + name: signed editorial_influence 0 to 100 + runs-on: ubuntu-latest + timeout-minutes: 60 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + RAILWAY_PROJECT_ID: 7e7a44ee-3e54-453c-9120-3cb1f33db76e + RAILWAY_INTERACTIVE_SERVICE_ID: 3affc435-074d-4f6f-806f-1c2b552a8b17 + RAILWAY_DEEP_SERVICE_ID: a3469457-f56e-4871-8a10-5ea6c052640c + RAILWAY_API_SERVICE_ID: 11ba75aa-2495-4f28-bbde-64c43efbe731 + RELEASE_VERIFICATION_KEY_ID: ${{ vars.RELEASE_VERIFICATION_KEY_ID }} + RELEASE_VERIFICATION_KEY_SHA256: ${{ vars.RELEASE_VERIFICATION_KEY_SHA256 }} + RELEASE_GATE_PRODUCER_KEY_ID: ${{ vars.RELEASE_GATE_PRODUCER_KEY_ID }} + RELEASE_GATE_PRODUCER_KEY_SHA256: ${{ vars.RELEASE_GATE_PRODUCER_KEY_SHA256 }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require the exact immutable candidate and upstream graph + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + SITES_RUN_ID: ${{ inputs.sites_handoff_run_id }} + SITES_ARTIFACT: ${{ inputs.sites_handoff_artifact }} + OWNER_RUN_ID: ${{ inputs.owner_gate_run_id }} + OWNER_ARTIFACT: ${{ inputs.owner_gate_artifact }} + ROUTE_RUN_ID: ${{ inputs.route_authority_run_id }} + ROUTE_ARTIFACT: ${{ inputs.route_authority_artifact }} + CLEAN_RUN_ID: ${{ inputs.clean_gate_run_id }} + CLEAN_ARTIFACT: ${{ inputs.clean_gate_artifact }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + test "$GITHUB_WORKFLOW_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] + test "$SITES_ARTIFACT" = "v254-sites-connector-handoff-$SOURCE_REVISION" + test "$OWNER_ARTIFACT" = "v254-owner-apple-gate-$SOURCE_REVISION" + test "$ROUTE_ARTIFACT" = "v254-pre-exposure-route-pause-public_canary-$SOURCE_REVISION" + test "$CLEAN_ARTIFACT" = "v254-pre-exposure-clean-nonowner-$SOURCE_REVISION" + git fetch --no-tags origin "$DEFAULT_BRANCH" + git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" + git fetch --force origin "+refs/tags/$CANDIDATE_TAG:refs/tags/$CANDIDATE_TAG" + test "$(git cat-file -t "refs/tags/$CANDIDATE_TAG")" = "tag" + test "$(git rev-parse "refs/tags/$CANDIDATE_TAG^{}")" = "$SOURCE_REVISION" + for RUN_ID in "$BACKEND_RUN_ID" "$SITES_RUN_ID" "$OWNER_RUN_ID" "$ROUTE_RUN_ID" "$CLEAN_RUN_ID"; do + [[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]] + RUN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID")" + test "$(jq -r '.event' <<<"$RUN")" = "workflow_dispatch" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + done + BACKEND="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BACKEND_RUN_ID")" + SITES="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SITES_RUN_ID")" + OWNER="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$OWNER_RUN_ID")" + ROUTE="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$ROUTE_RUN_ID")" + CLEAN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$CLEAN_RUN_ID")" + test "$(jq -r '.path' <<<"$BACKEND")" = ".github/workflows/native-schema20-release.yml" + test "$(jq -r '.path' <<<"$SITES")" = ".github/workflows/v254-sites-connector-handoff.yml" + test "$(jq -r '.path' <<<"$OWNER")" = ".github/workflows/v254-owner-apple-gate.yml" + test "$(jq -r '.path' <<<"$ROUTE")" = ".github/workflows/v254-pre-exposure-route-pause.yml" + test "$(jq -r '.path' <<<"$CLEAN")" = ".github/workflows/v254-pre-exposure-clean-nonowner.yml" + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: native-schema20-promotion-${{ inputs.source_revision }} + path: ${{ runner.temp }}/backend + run-id: ${{ inputs.backend_promotion_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: native-schema20-rollout-promotion-authority-${{ inputs.source_revision }} + path: ${{ runner.temp }}/native-authority + run-id: ${{ inputs.backend_promotion_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.sites_handoff_artifact }} + path: ${{ runner.temp }}/sites + run-id: ${{ inputs.sites_handoff_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.owner_gate_artifact }} + path: ${{ runner.temp }}/owner + run-id: ${{ inputs.owner_gate_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.route_authority_artifact }} + path: ${{ runner.temp }}/route + run-id: ${{ inputs.route_authority_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.clean_gate_artifact }} + path: ${{ runner.temp }}/clean + run-id: ${{ inputs.clean_gate_run_id }} + github-token: ${{ github.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Install exact lockfile and pinned Railway CLI + run: | + set -euo pipefail + pnpm install --frozen-lockfile + npm install --global @railway/cli@4.65.0 + test "$(railway --version)" = "railway 4.65.0" + + - name: Materialize and pin protected signing and verification keys + env: + RELEASE_VERIFICATION_PUBLIC_KEY_BASE64: ${{ secrets.RELEASE_VERIFICATION_PUBLIC_KEY_BASE64 }} + RELEASE_PUBLIC_ROLLOUT_EVIDENCE_SIGNING_KEY_BASE64: ${{ secrets.RELEASE_PUBLIC_ROLLOUT_EVIDENCE_SIGNING_KEY_BASE64 }} + RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64: ${{ secrets.RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64 }} + run: | + set -euo pipefail + umask 077 + printf '%s' "$RELEASE_VERIFICATION_PUBLIC_KEY_BASE64" | base64 --decode > "$RUNNER_TEMP/release-verification-public-key.pem" + printf '%s' "$RELEASE_PUBLIC_ROLLOUT_EVIDENCE_SIGNING_KEY_BASE64" | base64 --decode > "$RUNNER_TEMP/direct-exposure-signing-key.pem" + printf '%s' "$RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64" | base64 --decode > "$RUNNER_TEMP/release-gate-producer-public-key.pem" + node -e 'const c=require("node:crypto"),f=require("node:fs"); const pub=c.createPublicKey(f.readFileSync(process.argv[1])); const priv=c.createPrivateKey(f.readFileSync(process.argv[2])); const gate=c.createPublicKey(f.readFileSync(process.argv[3])); const fp=k=>c.createHash("sha256").update(k.export({format:"der",type:"spki"})).digest("hex"); if(fp(pub)!==process.env.RELEASE_VERIFICATION_KEY_SHA256||fp(c.createPublicKey(priv))!==process.env.RELEASE_VERIFICATION_KEY_SHA256||fp(gate)!==process.env.RELEASE_GATE_PRODUCER_KEY_SHA256) throw new Error("protected release keys do not match their configured fingerprints");' "$RUNNER_TEMP/release-verification-public-key.pem" "$RUNNER_TEMP/direct-exposure-signing-key.pem" "$RUNNER_TEMP/release-gate-producer-public-key.pem" + + - id: prepare + name: Independently derive one shared post-exposure semantic hash + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + pnpm release:v254:direct-exposure:runtime -- prepare \ + --promotion-receipt "$RUNNER_TEMP/backend/native-schema20-promotion.json" \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --image-digest "$IMAGE_DIGEST" \ + --project-id "$RAILWAY_PROJECT_ID" \ + --environment production \ + --interactive-service "$RAILWAY_INTERACTIVE_SERVICE_ID" \ + --deep-service "$RAILWAY_DEEP_SERVICE_ID" \ + --api-service "$RAILWAY_API_SERVICE_ID" \ + --origin https://9enio.com \ + --deployment-timeout-seconds 600 \ + --poll-interval-seconds 10 \ + --output "$RUNNER_TEMP/direct-exposure-runtime-plan.json" + POST_HASH="$(jq -er '.postSemanticConfigurationHash | select(test("^[0-9a-f]{64}$"))' "$RUNNER_TEMP/direct-exposure-runtime-plan.json")" + echo "post_hash=$POST_HASH" >> "$GITHUB_OUTPUT" + + - name: Mint and verify the single-use 0 to 100 authority and rollback warrant + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + POST_HASH: ${{ steps.prepare.outputs.post_hash }} + run: | + set -euo pipefail + pnpm release:v254:direct-exposure:authorize -- \ + --promotion-receipt "$RUNNER_TEMP/backend/native-schema20-promotion.json" \ + --native-authority "$RUNNER_TEMP/native-authority/native-schema20-rollout-promotion-authority.json" \ + --native-verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --native-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --native-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --sites-control-plane "$RUNNER_TEMP/sites/v254-sites-connector-handoff.json" \ + --owner-gate "$RUNNER_TEMP/owner/production-affected-regression.gate.json" \ + --owner-attestation "$RUNNER_TEMP/owner/production-affected-regression.attestation.json" \ + --clean-gate "$RUNNER_TEMP/clean/pre-exposure-clean-nonowner.gate.json" \ + --clean-attestation "$RUNNER_TEMP/clean/pre-exposure-clean-nonowner.attestation.json" \ + --gate-verification-key "$RUNNER_TEMP/release-gate-producer-public-key.pem" \ + --gate-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" \ + --gate-key-sha256 "$RELEASE_GATE_PRODUCER_KEY_SHA256" \ + --database-route-receipt "$RUNNER_TEMP/route/v254-editorial-route-authority.json" \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --image-digest "$IMAGE_DIGEST" \ + --post-semantic-configuration-hash "$POST_HASH" \ + --signing-key "$RUNNER_TEMP/direct-exposure-signing-key.pem" \ + --signing-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --signing-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --authority-output "$RUNNER_TEMP/v254-direct-exposure-authority.json" \ + --rollback-warrant-output "$RUNNER_TEMP/v254-direct-exposure-rollback-warrant.json" + + - id: plan + name: Verify the exact database plan before mutation + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + node --experimental-transform-types scripts/apply-v254-direct-exposure.ts plan \ + --authority "$RUNNER_TEMP/v254-direct-exposure-authority.json" \ + --rollback-warrant "$RUNNER_TEMP/v254-direct-exposure-rollback-warrant.json" \ + --verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --expected-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --expected-source-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-image-digest "$IMAGE_DIGEST" \ + --output "$RUNNER_TEMP/direct-exposure-database-plan.json" + + - id: arm + name: Arm the durable authority while route and publication stay paused + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + railway run --service Postgres --project "$RAILWAY_PROJECT_ID" --environment production --no-local -- \ + sh -lc 'export DATABASE_URL="$DATABASE_PUBLIC_URL"; export PGSSLMODE=no-verify; exec node --experimental-transform-types scripts/apply-v254-direct-exposure.ts "$@"' sh arm \ + --authority "$RUNNER_TEMP/v254-direct-exposure-authority.json" \ + --rollback-warrant "$RUNNER_TEMP/v254-direct-exposure-rollback-warrant.json" \ + --verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --expected-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --expected-source-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-image-digest "$IMAGE_DIGEST" \ + --output "$RUNNER_TEMP/direct-exposure-database-arm.json" + jq -e '.operation == "arm" and .state == "armed" and .terminal == false' "$RUNNER_TEMP/direct-exposure-database-arm.json" + echo "armed=true" >> "$GITHUB_OUTPUT" + + - id: runtime + name: Redeploy the existing workers first and API last from the same image + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + pnpm release:v254:direct-exposure:runtime -- apply \ + --promotion-receipt "$RUNNER_TEMP/backend/native-schema20-promotion.json" \ + --authority "$RUNNER_TEMP/v254-direct-exposure-authority.json" \ + --rollback-warrant "$RUNNER_TEMP/v254-direct-exposure-rollback-warrant.json" \ + --verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --expected-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --image-digest "$IMAGE_DIGEST" \ + --project-id "$RAILWAY_PROJECT_ID" \ + --environment production \ + --interactive-service "$RAILWAY_INTERACTIVE_SERVICE_ID" \ + --deep-service "$RAILWAY_DEEP_SERVICE_ID" \ + --api-service "$RAILWAY_API_SERVICE_ID" \ + --origin https://9enio.com \ + --deployment-timeout-seconds 600 \ + --poll-interval-seconds 10 \ + --output "$RUNNER_TEMP/direct-exposure-runtime-apply.json" + echo "applied=true" >> "$GITHUB_OUTPUT" + + - id: activate + name: Activate the database authority last + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + railway run --service Postgres --project "$RAILWAY_PROJECT_ID" --environment production --no-local -- \ + sh -lc 'export DATABASE_URL="$DATABASE_PUBLIC_URL"; export PGSSLMODE=no-verify; exec node --experimental-transform-types scripts/apply-v254-direct-exposure.ts "$@"' sh activate \ + --authority "$RUNNER_TEMP/v254-direct-exposure-authority.json" \ + --rollback-warrant "$RUNNER_TEMP/v254-direct-exposure-rollback-warrant.json" \ + --verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --expected-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --expected-source-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-image-digest "$IMAGE_DIGEST" \ + --output "$RUNNER_TEMP/direct-exposure-database-activate.json" + jq -e '.operation == "activate" and .state == "active" and .terminal == false' "$RUNNER_TEMP/direct-exposure-database-activate.json" + echo "activated=true" >> "$GITHUB_OUTPUT" + + - name: Prove the active exact runtime and absence of staged authority + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + for PATHNAME in live ready system; do + curl --fail --silent --show-error --location --max-time 20 \ + -H 'cache-control: no-cache' \ + "https://9enio.com/health/$PATHNAME?v254-direct=$RANDOM" \ + > "$RUNNER_TEMP/health-$PATHNAME.json" + done + jq -e --arg version "$RELEASE_VERSION" --arg revision "$SOURCE_REVISION" \ + '(.build // .).version == $version and (.build // .).revision == $revision' \ + "$RUNNER_TEMP/health-live.json" + jq -e \ + '.ok == true + and .activationReady == true + and (.schemaVersion | tostring) == "20" + and .workerProtocol.actual == "playlist-pipeline-v12" + and .executorFencing.uncoveredJobs == 0 + and .publicRollout.active == false + and .directExposure.active == true + and .directExposure.state == "active" + and .directExposure.exposureClass == "fully_exposed_unproven" + and .directExposure.organicReliabilityProven == false + and (.directExposure.authorityPayloadHash | test("^[0-9a-f]{64}$")) + and (.directExposure.rollbackWarrantPayloadHash | test("^[0-9a-f]{64}$"))' \ + "$RUNNER_TEMP/health-system.json" + + - name: Roll back database first, then exact image and configuration + if: failure() && steps.arm.outputs.armed == 'true' + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set +e + railway run --service Postgres --project "$RAILWAY_PROJECT_ID" --environment production --no-local -- \ + sh -lc 'export DATABASE_URL="$DATABASE_PUBLIC_URL"; export PGSSLMODE=no-verify; exec node --experimental-transform-types scripts/apply-v254-direct-exposure.ts "$@"' sh rollback \ + --authority "$RUNNER_TEMP/v254-direct-exposure-authority.json" \ + --rollback-warrant "$RUNNER_TEMP/v254-direct-exposure-rollback-warrant.json" \ + --verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --expected-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --expected-source-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-image-digest "$IMAGE_DIGEST" \ + --output "$RUNNER_TEMP/direct-exposure-database-rollback.json" + DB_STATUS=$? + pnpm release:v254:direct-exposure:runtime -- rollback \ + --promotion-receipt "$RUNNER_TEMP/backend/native-schema20-promotion.json" \ + --authority "$RUNNER_TEMP/v254-direct-exposure-authority.json" \ + --rollback-warrant "$RUNNER_TEMP/v254-direct-exposure-rollback-warrant.json" \ + --verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --expected-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --image-digest "$IMAGE_DIGEST" \ + --project-id "$RAILWAY_PROJECT_ID" \ + --environment production \ + --interactive-service "$RAILWAY_INTERACTIVE_SERVICE_ID" \ + --deep-service "$RAILWAY_DEEP_SERVICE_ID" \ + --api-service "$RAILWAY_API_SERVICE_ID" \ + --origin https://9enio.com \ + --deployment-timeout-seconds 600 \ + --poll-interval-seconds 10 \ + --output "$RUNNER_TEMP/direct-exposure-runtime-rollback.json" + RUNTIME_STATUS=$? + test "$DB_STATUS" -eq 0 + test "$RUNTIME_STATUS" -eq 0 + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: v254-editorial-direct-exposure-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/direct-exposure-runtime-plan.json + ${{ runner.temp }}/v254-direct-exposure-authority.json + ${{ runner.temp }}/v254-direct-exposure-rollback-warrant.json + ${{ runner.temp }}/direct-exposure-database-plan.json + ${{ runner.temp }}/direct-exposure-database-arm.json + ${{ runner.temp }}/direct-exposure-runtime-apply.json + ${{ runner.temp }}/direct-exposure-database-activate.json + ${{ runner.temp }}/direct-exposure-database-rollback.json + ${{ runner.temp }}/direct-exposure-runtime-rollback.json + ${{ runner.temp }}/health-live.json + ${{ runner.temp }}/health-ready.json + ${{ runner.temp }}/health-system.json + if-no-files-found: warn + retention-days: 30 diff --git a/.github/workflows/v254-owner-apple-gate.yml b/.github/workflows/v254-owner-apple-gate.yml new file mode 100644 index 0000000..b1277f9 --- /dev/null +++ b/.github/workflows/v254-owner-apple-gate.yml @@ -0,0 +1,385 @@ +name: v254 owner Irish Apple gate + +on: + workflow_dispatch: + inputs: + source_revision: + description: Exact merged v2.5.4 source revision + required: true + type: string + release_version: + description: Exact candidate version + required: true + type: string + candidate_tag: + description: Immutable RC tag + required: true + type: string + image_digest: + description: Immutable promoted backend image digest + required: true + type: string + sites_handoff_run_id: + description: Successful connector evidence handoff run + required: true + type: string + sites_handoff_artifact: + description: Exact connector evidence handoff artifact + required: true + type: string + backend_promotion_run_id: + description: Successful native schema20 promotion run + required: true + type: string + route_authority_run_id: + description: Successful owner_gate route authority run + required: true + type: string + route_authority_artifact: + description: Exact owner_gate authority artifact + required: true + type: string + +concurrency: + group: v254-owner-apple-gate-${{ inputs.source_revision }} + cancel-in-progress: false + +permissions: + actions: read + contents: read + +jobs: + owner-gate: + name: fixed-three and owner Irish publications with independent Apple readback + runs-on: ubuntu-latest + timeout-minutes: 60 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RELEASE_PRODUCTION_ORIGIN: https://9enio.com + RELEASE_CANARY_HMAC_SECRET: ${{ secrets.RELEASE_CANARY_HMAC_SECRET }} + RELEASE_IRISH_RECOVERY_ACCESS_ID: ${{ secrets.RELEASE_IRISH_RECOVERY_ACCESS_ID }} + RELEASE_IRISH_RECOVERY_COOKIE: ${{ secrets.RELEASE_IRISH_RECOVERY_COOKIE }} + RELEASE_OWNER_BROWSER_COOKIE: ${{ secrets.RELEASE_OWNER_BROWSER_COOKIE }} + RELEASE_PRODUCTION_DATABASE_URL: ${{ secrets.RELEASE_PRODUCTION_DATABASE_URL }} + APPLE_QA_VERIFIER_DEVELOPER_TOKEN: ${{ secrets.APPLE_QA_VERIFIER_DEVELOPER_TOKEN }} + APPLE_QA_VERIFIER_CREDENTIAL_VERSION: ${{ vars.APPLE_QA_VERIFIER_CREDENTIAL_VERSION }} + RELEASE_GATE_PRODUCER_KEY_ID: ${{ vars.RELEASE_GATE_PRODUCER_KEY_ID }} + RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_ID: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_ID }} + RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_SHA256: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_SHA256 }} + SITES_PROJECT_ID: appgprj_6a5565cf7d6c8191ab9f2084e8eda856 + RELEASE_BROWSER_ARTIFACT_DIR: ${{ runner.temp }}/owner-browser + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require exact candidate and proof lineage + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + SITES_RUN_ID: ${{ inputs.sites_handoff_run_id }} + SITES_ARTIFACT: ${{ inputs.sites_handoff_artifact }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + ROUTE_RUN_ID: ${{ inputs.route_authority_run_id }} + ROUTE_ARTIFACT: ${{ inputs.route_authority_artifact }} + run: | + set -euo pipefail + test "$GITHUB_WORKFLOW_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] + test "$SITES_ARTIFACT" = "v254-sites-connector-handoff-$SOURCE_REVISION" + test "$ROUTE_ARTIFACT" = "v254-pre-exposure-route-pause-owner_gate-$SOURCE_REVISION" + for RUN_ID in "$SITES_RUN_ID" "$BACKEND_RUN_ID" "$ROUTE_RUN_ID"; do + RUN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID")" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + done + SITES="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SITES_RUN_ID")" + test "$(jq -r '.path' <<<"$SITES")" = ".github/workflows/v254-sites-connector-handoff.yml" + BACKEND="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BACKEND_RUN_ID")" + test "$(jq -r '.path' <<<"$BACKEND")" = ".github/workflows/native-schema20-release.yml" + ROUTE="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$ROUTE_RUN_ID")" + test "$(jq -r '.path' <<<"$ROUTE")" = ".github/workflows/v254-pre-exposure-route-pause.yml" + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.sites_handoff_artifact }} + path: ${{ runner.temp }}/sites + run-id: ${{ inputs.sites_handoff_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: native-schema20-promotion-${{ inputs.source_revision }} + path: ${{ runner.temp }}/backend + run-id: ${{ inputs.backend_promotion_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.route_authority_artifact }} + path: ${{ runner.temp }}/route + run-id: ${{ inputs.route_authority_run_id }} + github-token: ${{ github.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Install exact dependencies and browser + run: | + pnpm install --frozen-lockfile + pnpm exec playwright install --with-deps chromium + + - name: Bind backend promotion and owner route receipts + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + BACKEND="$RUNNER_TEMP/backend/native-schema20-promotion.json" + ROUTE="$RUNNER_TEMP/route/v254-editorial-route-authority.json" + jq -e \ + --arg sha "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + --arg digest "$IMAGE_DIGEST" \ + '.schemaVersion == "genio-native-schema20-promotion/v1" + and .sourceRevision == $sha + and .version == $version + and .imageDigest == $digest + and .environment == "production" + and (.semanticBehaviorManifestHash | test("^[0-9a-f]{64}$")) + and (.semanticExecutionConfigurationHash | test("^[0-9a-f]{64}$")) + and (.receiptHash | test("^[0-9a-f]{64}$"))' \ + "$BACKEND" + BACKEND_RECEIPT_HASH="$(jq -er '.receiptHash' "$BACKEND")" + BACKEND_SEMANTIC_HASH="$( + jq -er '.semanticExecutionConfigurationHash' "$BACKEND" + )" + jq -e \ + --arg sha "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + --arg digest "$IMAGE_DIGEST" \ + --arg promotion "$BACKEND_RECEIPT_HASH" \ + --arg semantic "$BACKEND_SEMANTIC_HASH" \ + '.schemaVersion == "genio-v254-editorial-route-authority/v1" + and .phase == "owner_gate" + and .sourceRevision == $sha + and .version == $version + and .imageDigest == $digest + and .promotionReceiptHash == $promotion + and .semanticExecutionConfigurationHash == $semantic + and .applied == true + and .database.hardSwitchDisabled == false + and .database.globalPublicPause == true + and .database.intentPublicPause == true + and (.behaviorManifestHash | test("^[0-9a-f]{64}$")) + and (.ownerGateReceiptHash | test("^[0-9a-f]{64}$")) + and .publicCanaryReceiptHash == null + and (.receiptHash | test("^[0-9a-f]{64}$"))' \ + "$ROUTE" + + - name: Reverify exact Sites connector handoff + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + node --experimental-transform-types \ + scripts/v254-sites-connector-handoff.ts \ + --candidate-tag "$CANDIDATE_TAG" \ + --version "$RELEASE_VERSION" \ + --source-revision "$SOURCE_REVISION" \ + --image-digest "$IMAGE_DIGEST" \ + --project-id "$SITES_PROJECT_ID" \ + --sites-control-plane-evidence "$RUNNER_TEMP/sites/sites-control-plane.json" \ + --sites-control-plane-attestation "$RUNNER_TEMP/sites/sites-control-plane.attestation.json" \ + --sites-control-plane-public-key "$RUNNER_TEMP/sites/sites-control-plane-public-key.pem" \ + --output "$RUNNER_TEMP/owner-sites-connector-handoff.json" + jq -e \ + --arg source "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + --arg tag "$CANDIDATE_TAG" \ + --arg image "$IMAGE_DIGEST" \ + --arg project "$SITES_PROJECT_ID" \ + --arg key_id "$RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_ID" \ + --arg key_sha "$RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_SHA256" \ + ' + .schemaVersion == "genio-v254-sites-connector-handoff/v1" + and .candidate.tag == $tag + and .candidate.version == $version + and .candidate.sourceRevision == $source + and .candidate.sitesSourceRevision == $source + and .candidate.imageDigest == $image + and .projectId == $project + and .connectorProduced == true + and .importedVia == "bounded_workflow_dispatch_input" + and .trustedKeyId == $key_id + and .trustedKeySha256 == $key_sha + and (.sitesControlPlaneEvidenceHash | test("^[0-9a-f]{64}$")) + and (.sitesControlPlaneAttestationPayloadHash | test("^[0-9a-f]{64}$")) + and (.sitesControlPlaneAttestationHash | test("^[0-9a-f]{64}$")) + and (.archiveSha256 | test("^[0-9a-f]{64}$")) + and (.handoffHash | test("^[0-9a-f]{64}$")) + and (.versionId | type == "string" and length > 0) + and (.deploymentId | type == "string" and length > 0) + ' "$RUNNER_TEMP/owner-sites-connector-handoff.json" + jq -e -s ' + .[0] as $upstream + | .[1] as $reverified + | ( + $upstream + | del(.verifiedAt, .handoffHash) + ) == ( + $reverified + | del(.verifiedAt, .handoffHash) + ) + ' \ + "$RUNNER_TEMP/sites/v254-sites-connector-handoff.json" \ + "$RUNNER_TEMP/owner-sites-connector-handoff.json" + + - name: Materialize protected proof inputs + env: + RELEASE_SECRET_VERSIONS_BASE64: ${{ secrets.RELEASE_SECRET_VERSIONS_BASE64 }} + RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64: ${{ secrets.RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64 }} + run: | + set -euo pipefail + umask 077 + printf '%s' "$RELEASE_SECRET_VERSIONS_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/release-secret-versions.json" + printf '%s' "$RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/release-gate-producer-private-key.pem" + + - name: Capture exact full runtime and Sites snapshot + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + node --experimental-transform-types scripts/release-runtime-snapshot.ts \ + --origin https://9enio.com \ + --environment production \ + --scope full \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --secret-versions "$RUNNER_TEMP/release-secret-versions.json" \ + --output "$RUNNER_TEMP/full-runtime-snapshot.json" + jq -e \ + --arg source "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + ' + .schemaVersion == "genio-release-runtime-snapshot/v3" + and .environment == "production" + and .scope == "full" + and .candidate.sourceRevision == $source + and .candidate.version == $version + and .sitesObservation.sourceRevision == $source + and .sitesObservation.version == $version + and .sitesObservation.candidateMatched == true + and .executorFencing.ready == true + and .executorFencing.incompleteJobs == 0 + and .executorFencing.mismatchedActiveAttempts == 0 + and .executorFencing.uncoveredJobs == 0 + and .runtime.databaseSchemaVersion == "20" + and .runtime.workerProtocol == "playlist-pipeline-v12" + and (.snapshotHash | test("^[0-9a-f]{64}$")) + ' "$RUNNER_TEMP/full-runtime-snapshot.json" + + - name: Publish the code-owned fixed-three control once and reread Apple order + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + RELEASE_BROWSER_ARTIFACT_DIR: ${{ runner.temp }}/owner-browser/fixed-three + # The fixed control is a signed release canary inside the protected + # owner gate, but it must not receive any Irish-only selectors. + RELEASE_IRISH_RECOVERY_ACCESS_ID: "" + RELEASE_IRISH_RECOVERY_COOKIE: "" + RELEASE_OWNER_BROWSER_COOKIE: "" + RELEASE_PRODUCTION_DATABASE_URL: "" + run: | + set -euo pipefail + unset RELEASE_IRISH_RECOVERY_ACCESS_ID + unset RELEASE_IRISH_RECOVERY_COOKIE + unset RELEASE_OWNER_BROWSER_COOKIE + unset RELEASE_PRODUCTION_DATABASE_URL + node --experimental-transform-types scripts/hosted-publication-smoke.ts \ + --confirm-live-write \ + --origin https://9enio.com \ + --fixture-id fixed-three-track-control-v1 \ + --candidate-tag "$CANDIDATE_TAG" \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --image-digest "$IMAGE_DIGEST" \ + --environment production \ + --cache-mode reuse_disabled \ + --runtime-snapshot "$RUNNER_TEMP/full-runtime-snapshot.json" \ + --source-output "$RUNNER_TEMP/production-fixed-three-track.source.json" \ + --output "$RUNNER_TEMP/production-fixed-three-track.gate.json" \ + --attestation-output "$RUNNER_TEMP/production-fixed-three-track.attestation.json" \ + --producer-signing-key "$RUNNER_TEMP/release-gate-producer-private-key.pem" \ + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" + + - name: Publish the owner Irish playlist once and reread Apple order + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + RELEASE_BROWSER_ARTIFACT_DIR: ${{ runner.temp }}/owner-browser/irish + run: | + set -euo pipefail + node --experimental-transform-types scripts/hosted-publication-smoke.ts \ + --confirm-live-write \ + --origin https://9enio.com \ + --fixture-id irish-influence-recovery-25-v1 \ + --candidate-tag "$CANDIDATE_TAG" \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --image-digest "$IMAGE_DIGEST" \ + --environment production \ + --cache-mode reuse_disabled \ + --runtime-snapshot "$RUNNER_TEMP/full-runtime-snapshot.json" \ + --source-output "$RUNNER_TEMP/production-affected-regression.source.json" \ + --output "$RUNNER_TEMP/production-affected-regression.gate.json" \ + --attestation-output "$RUNNER_TEMP/production-affected-regression.attestation.json" \ + --producer-signing-key "$RUNNER_TEMP/release-gate-producer-private-key.pem" \ + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: v254-owner-apple-gate-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/production-fixed-three-track.source.json + ${{ runner.temp }}/production-fixed-three-track.gate.json + ${{ runner.temp }}/production-fixed-three-track.attestation.json + ${{ runner.temp }}/production-affected-regression.source.json + ${{ runner.temp }}/production-affected-regression.gate.json + ${{ runner.temp }}/production-affected-regression.attestation.json + ${{ runner.temp }}/full-runtime-snapshot.json + ${{ runner.temp }}/owner-sites-connector-handoff.json + ${{ runner.temp }}/sites/v254-sites-connector-handoff.json + ${{ runner.temp }}/sites/sites-control-plane.json + ${{ runner.temp }}/sites/sites-control-plane.attestation.json + ${{ runner.temp }}/sites/sites-control-plane-public-key.pem + ${{ runner.temp }}/owner-browser + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/v254-pre-exposure-clean-nonowner.yml b/.github/workflows/v254-pre-exposure-clean-nonowner.yml new file mode 100644 index 0000000..b1784fb --- /dev/null +++ b/.github/workflows/v254-pre-exposure-clean-nonowner.yml @@ -0,0 +1,460 @@ +name: v254 pre-exposure clean non-owner proof + +on: + workflow_dispatch: + inputs: + source_revision: + description: Exact merged v2.5.4 source revision + required: true + type: string + release_version: + description: Exact candidate version + required: true + type: string + candidate_tag: + description: Immutable RC tag + required: true + type: string + image_digest: + description: Immutable promoted backend image digest + required: true + type: string + backend_promotion_run_id: + description: Successful exact-image native schema20 promotion run + required: true + type: string + sites_handoff_run_id: + description: Successful exact-SHA Sites connector handoff run + required: true + type: string + sites_handoff_artifact: + description: Exact Sites connector handoff artifact + required: true + type: string + route_authority_run_id: + description: Successful paused public_canary route authority run + required: true + type: string + route_authority_artifact: + description: Exact paused public_canary route authority artifact + required: true + type: string + +concurrency: + group: v254-pre-exposure-clean-nonowner-${{ inputs.source_revision }} + cancel-in-progress: false + +permissions: + actions: read + contents: read + +jobs: + prove: + name: signed zero-write typo and corrected UI probes + runs-on: ubuntu-latest + timeout-minutes: 45 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RELEASE_PRODUCTION_ORIGIN: https://9enio.com + RELEASE_CANARY_HMAC_SECRET: ${{ secrets.RELEASE_CANARY_HMAC_SECRET }} + RELEASE_GATE_PRODUCER_KEY_ID: ${{ vars.RELEASE_GATE_PRODUCER_KEY_ID }} + RELEASE_GATE_PRODUCER_KEY_SHA256: ${{ vars.RELEASE_GATE_PRODUCER_KEY_SHA256 }} + RELEASE_SITES_CONTROL_PLANE_KEY_ID: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_ID }} + RELEASE_SITES_CONTROL_PLANE_KEY_SHA256: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_SHA256 }} + RELEASE_BROWSER_ARTIFACT_DIR: ${{ runner.temp }}/pre-exposure-browser + SITES_PROJECT_ID: appgprj_6a5565cf7d6c8191ab9f2084e8eda856 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require exact candidate and successful upstream graph + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + SITES_RUN_ID: ${{ inputs.sites_handoff_run_id }} + SITES_ARTIFACT: ${{ inputs.sites_handoff_artifact }} + ROUTE_RUN_ID: ${{ inputs.route_authority_run_id }} + ROUTE_ARTIFACT: ${{ inputs.route_authority_artifact }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + test "$GITHUB_WORKFLOW_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] + [[ "$BACKEND_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$SITES_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$ROUTE_RUN_ID" =~ ^[1-9][0-9]*$ ]] + test "$SITES_ARTIFACT" = "v254-sites-connector-handoff-$SOURCE_REVISION" + test "$ROUTE_ARTIFACT" = "v254-pre-exposure-route-pause-public_canary-$SOURCE_REVISION" + git fetch --no-tags origin "$DEFAULT_BRANCH" + git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" + git fetch --force origin "+refs/tags/$CANDIDATE_TAG:refs/tags/$CANDIDATE_TAG" + TAG_OBJECT="$(git rev-parse "refs/tags/$CANDIDATE_TAG")" + test "$(git cat-file -t "$TAG_OBJECT")" = "tag" + test "$(git rev-parse "$TAG_OBJECT^{}")" = "$SOURCE_REVISION" + for RUN_ID in "$BACKEND_RUN_ID" "$SITES_RUN_ID" "$ROUTE_RUN_ID"; do + RUN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID")" + test "$(jq -r '.event' <<<"$RUN")" = "workflow_dispatch" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + done + BACKEND="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BACKEND_RUN_ID")" + test "$(jq -r '.path' <<<"$BACKEND")" = ".github/workflows/native-schema20-release.yml" + SITES="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SITES_RUN_ID")" + test "$(jq -r '.path' <<<"$SITES")" = ".github/workflows/v254-sites-connector-handoff.yml" + ROUTE="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$ROUTE_RUN_ID")" + test "$(jq -r '.path' <<<"$ROUTE")" = ".github/workflows/v254-pre-exposure-route-pause.yml" + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: native-schema20-promotion-${{ inputs.source_revision }} + path: ${{ runner.temp }}/backend + run-id: ${{ inputs.backend_promotion_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.sites_handoff_artifact }} + path: ${{ runner.temp }}/sites + run-id: ${{ inputs.sites_handoff_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.route_authority_artifact }} + path: ${{ runner.temp }}/route + run-id: ${{ inputs.route_authority_run_id }} + github-token: ${{ github.token }} + + - name: Bind exact promotion, Sites, and paused route receipts + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + BACKEND="$RUNNER_TEMP/backend/native-schema20-promotion.json" + SITES="$RUNNER_TEMP/sites/v254-sites-connector-handoff.json" + ROUTE="$RUNNER_TEMP/route/v254-editorial-route-authority.json" + jq -e \ + --arg sha "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + --arg digest "$IMAGE_DIGEST" \ + '.schemaVersion == "genio-native-schema20-promotion/v1" + and .sourceRevision == $sha + and .version == $version + and .imageDigest == $digest + and .environment == "production" + and (.semanticBehaviorManifestHash | test("^[0-9a-f]{64}$")) + and (.semanticExecutionConfigurationHash | test("^[0-9a-f]{64}$")) + and (.backendConvergenceEvidenceHash | test("^[0-9a-f]{64}$")) + and (.receiptHash | test("^[0-9a-f]{64}$")) + and (.completedAt | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T"))' \ + "$BACKEND" + BACKEND_RECEIPT_HASH="$(jq -er '.receiptHash' "$BACKEND")" + BACKEND_SEMANTIC_HASH="$(jq -er '.semanticExecutionConfigurationHash' "$BACKEND")" + BACKEND_COMPLETED_AT="$(jq -er '.completedAt' "$BACKEND")" + jq -e \ + --arg sha "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + --arg tag "$CANDIDATE_TAG" \ + --arg digest "$IMAGE_DIGEST" \ + --arg project "$SITES_PROJECT_ID" \ + '.schemaVersion == "genio-v254-sites-connector-handoff/v1" + and .candidate.sourceRevision == $sha + and .candidate.sitesSourceRevision == $sha + and .candidate.version == $version + and .candidate.tag == $tag + and .candidate.imageDigest == $digest + and .projectId == $project + and .connectorProduced == true + and .importedVia == "bounded_workflow_dispatch_input" + and (.sitesControlPlaneEvidenceHash | test("^[0-9a-f]{64}$")) + and (.sitesControlPlaneAttestationPayloadHash | test("^[0-9a-f]{64}$")) + and (.handoffHash | test("^[0-9a-f]{64}$")) + and (.versionId | type == "string" and length > 0) + and (.deploymentId | type == "string" and length > 0) + and (.verifiedAt | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T"))' \ + "$SITES" + SITES_VERIFIED_AT="$(jq -er '.verifiedAt' "$SITES")" + jq -e \ + --arg sha "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + --arg digest "$IMAGE_DIGEST" \ + --arg promotion "$BACKEND_RECEIPT_HASH" \ + --arg semantic "$BACKEND_SEMANTIC_HASH" \ + '.schemaVersion == "genio-v254-editorial-route-authority/v1" + and .phase == "public_canary" + and .rollbackTarget == null + and .sourceRevision == $sha + and .version == $version + and .imageDigest == $digest + and .promotionReceiptHash == $promotion + and .semanticExecutionConfigurationHash == $semantic + and .applied == true + and .database.hardSwitchDisabled == false + and .database.globalPublicPause == true + and .database.intentPublicPause == true + and (.ownerGateReceiptHash | test("^[0-9a-f]{64}$")) + and (.publicCanaryReceiptHash | test("^[0-9a-f]{64}$")) + and (.proofEvidenceHash | test("^[0-9a-f]{64}$")) + and (.rollingDeployGuardHash | test("^[0-9a-f]{64}$")) + and (.receiptHash | test("^[0-9a-f]{64}$")) + and (.completedAt | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T"))' \ + "$ROUTE" + ROUTE_COMPLETED_AT="$(jq -er '.completedAt' "$ROUTE")" + test "$(date -u -d "$BACKEND_COMPLETED_AT" +%s)" \ + -le "$(date -u -d "$SITES_VERIFIED_AT" +%s)" + test "$(date -u -d "$SITES_VERIFIED_AT" +%s)" \ + -le "$(date -u -d "$ROUTE_COMPLETED_AT" +%s)" + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Install exact dependencies and browser + run: | + set -euo pipefail + pnpm install --frozen-lockfile + pnpm exec playwright install --with-deps chromium + + - name: Require the exact typo and corrected clean-browser fixtures + run: | + set -euo pipefail + node --experimental-transform-types --input-type=module -e ' + import { FINAL_PUBLIC_ASSIGNMENT_PROBE_FIXTURES_V1 as fixtures } from "./scripts/release-fixtures.ts"; + const expected = [ + ["final-public-assignment-editorial-influence-typo-v1", "Infuential irish music", 25], + ["final-public-assignment-editorial-influence-corrected-v1", "Influential Irish music", 25], + ]; + const actual = fixtures.map(({ fixtureId, prompt, targetTrackCount }) => [fixtureId, prompt, targetTrackCount]); + if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error("pre-exposure browser fixture contract changed"); + ' + + - name: Materialize and pin protected proof inputs + env: + RELEASE_SECRET_VERSIONS_BASE64: ${{ secrets.RELEASE_SECRET_VERSIONS_BASE64 }} + RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64: ${{ secrets.RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64 }} + run: | + set -euo pipefail + test -n "$RELEASE_SECRET_VERSIONS_BASE64" + test -n "$RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64" + test -n "$RELEASE_CANARY_HMAC_SECRET" + test "$(printf '%s' "$RELEASE_CANARY_HMAC_SECRET" | wc -c)" -ge 32 + [[ "$RELEASE_GATE_PRODUCER_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:-]{2,79}$ ]] + [[ "$RELEASE_GATE_PRODUCER_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + [[ "$RELEASE_SITES_CONTROL_PLANE_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:+/-]{2,159}$ ]] + [[ "$RELEASE_SITES_CONTROL_PLANE_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + umask 077 + printf '%s' "$RELEASE_SECRET_VERSIONS_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/release-secret-versions.json" + printf '%s' "$RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/release-gate-producer-private-key.pem" + jq -e 'type == "object" and length > 0' \ + "$RUNNER_TEMP/release-secret-versions.json" + OBSERVED_KEY_SHA256="$( + node -e 'const c=require("node:crypto"),f=require("node:fs"); const k=c.createPrivateKey(f.readFileSync(process.argv[1])); if(k.asymmetricKeyType!=="ed25519") throw new Error("release gate key must be Ed25519"); process.stdout.write(c.createHash("sha256").update(c.createPublicKey(k).export({format:"der",type:"spki"})).digest("hex"));' \ + "$RUNNER_TEMP/release-gate-producer-private-key.pem" + )" + test "$OBSERVED_KEY_SHA256" = "$RELEASE_GATE_PRODUCER_KEY_SHA256" + + - name: Capture the exact pre-exposure runtime snapshot + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + node --experimental-transform-types scripts/release-runtime-snapshot.ts \ + --origin https://9enio.com \ + --environment production \ + --scope full \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --secret-versions "$RUNNER_TEMP/release-secret-versions.json" \ + --output "$RUNNER_TEMP/pre-exposure-runtime-snapshot.json" + jq -e \ + --arg source "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + '.schemaVersion == "genio-release-runtime-snapshot/v3" + and .environment == "production" + and .scope == "full" + and .candidate.sourceRevision == $source + and .candidate.version == $version + and .sitesObservation.sourceRevision == $source + and .sitesObservation.version == $version + and .sitesObservation.candidateMatched == true + and .executorFencing.ready == true + and .executorFencing.incompleteJobs == 0 + and .executorFencing.mismatchedActiveAttempts == 0 + and .executorFencing.uncoveredJobs == 0 + and .runtime.databaseSchemaVersion == "20" + and .runtime.workerProtocol == "playlist-pipeline-v12" + and (.snapshotHash | test("^[0-9a-f]{64}$"))' \ + "$RUNNER_TEMP/pre-exposure-runtime-snapshot.json" + ROUTE_SEMANTIC_HASH="$( + jq -er '.semanticExecutionConfigurationHash' \ + "$RUNNER_TEMP/route/v254-editorial-route-authority.json" + )" + BACKEND_API_HASH="$( + jq -er '.promotedRuntimeConfigurationHashes.api' \ + "$RUNNER_TEMP/backend/native-schema20-promotion.json" + )" + BACKEND_INTERACTIVE_HASH="$( + jq -er '.promotedRuntimeConfigurationHashes.interactive' \ + "$RUNNER_TEMP/backend/native-schema20-promotion.json" + )" + BACKEND_DEEP_HASH="$( + jq -er '.promotedRuntimeConfigurationHashes.deep' \ + "$RUNNER_TEMP/backend/native-schema20-promotion.json" + )" + jq -e \ + --arg semantic "$ROUTE_SEMANTIC_HASH" \ + --arg api "$BACKEND_API_HASH" \ + --arg interactive "$BACKEND_INTERACTIVE_HASH" \ + --arg deep "$BACKEND_DEEP_HASH" \ + '.runtime.semanticExecutionConfigurationHash == $semantic + and .configuration.apiHash == $api + and .configuration.interactiveWorkerHash == $interactive + and .configuration.deepWorkerHash == $deep' \ + "$RUNNER_TEMP/pre-exposure-runtime-snapshot.json" + + - name: Produce signed clean non-owner zero-write browser proof + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + node --experimental-transform-types scripts/final-custom-domain-browser-producer.ts \ + --confirm-production-browser \ + --pre-exposure-clean-nonowner \ + --origin https://9enio.com \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --candidate-tag "$CANDIDATE_TAG" \ + --image-digest "$IMAGE_DIGEST" \ + --runtime-snapshot "$RUNNER_TEMP/pre-exposure-runtime-snapshot.json" \ + --sites-control-plane-evidence "$RUNNER_TEMP/sites/sites-control-plane.json" \ + --sites-control-plane-attestation "$RUNNER_TEMP/sites/sites-control-plane.attestation.json" \ + --sites-control-plane-verification-key "$RUNNER_TEMP/sites/sites-control-plane-public-key.pem" \ + --browser-artifact-dir "$RELEASE_BROWSER_ARTIFACT_DIR" \ + --source-output "$RUNNER_TEMP/pre-exposure-clean-nonowner.source.json" \ + --output "$RUNNER_TEMP/pre-exposure-clean-nonowner.gate.json" \ + --attestation-output "$RUNNER_TEMP/pre-exposure-clean-nonowner.attestation.json" \ + --producer-signing-key "$RUNNER_TEMP/release-gate-producer-private-key.pem" \ + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" + + - name: Assert exact signed-canary, fresh-run, and Apple-write fences + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + GATE="$RUNNER_TEMP/pre-exposure-clean-nonowner.gate.json" + jq -e \ + --arg source "$SOURCE_REVISION" \ + --arg version "$RELEASE_VERSION" \ + --arg tag "$CANDIDATE_TAG" \ + --arg digest "$IMAGE_DIGEST" \ + '.gate == "final_custom_domain_browser" + and .environment == "production" + and .candidate.sourceRevision == $source + and .candidate.sitesSourceRevision == $source + and .candidate.version == $version + and .candidate.tag == $tag + and .candidate.imageDigest == $digest + and .sources.browser.schemaVersion == "genio-final-custom-domain-browser/v7" + and .sources.browser.assignmentMode == "pre_exposure_release_canary" + and .sources.browser.noDirectRailwayRequests == true + and (.sources.browser.publicAssignmentProbes | length == 2) + and ( + .sources.browser.publicAssignmentProbes + | map(.fixtureId) + | sort + ) == [ + "final-public-assignment-editorial-influence-corrected-v1", + "final-public-assignment-editorial-influence-typo-v1" + ] + and ( + .sources.browser.publicAssignmentProbes + | all( + .intentGroup == "editorial_influence" + and .targetTrackCount == 25 + and .rolloutEvidenceHash == null + and .rolloutStage == null + and .assignmentHash == null + and .assignmentAuthority == "signed_release_canary" + and (.assignmentReceiptHash | test("^[0-9a-f]{64}$")) + and .publicPercentageBypass == true + and .organicAssignment == false + and .contractVersion == 3 + and .guidancePolicyVersion == "adaptive_guidance_v5" + and .axis == "influence_scope" + and .selectedOptionId == "balanced_influence" + and .answerAccepted == true + and (.successorContractHash | test("^[0-9a-f]{64}$")) + and (.queryPlanHash | test("^[0-9a-f]{64}$")) + and (.executionRouteReceiptHash | test("^[0-9a-f]{64}$")) + and (.runAccessIdHash | test("^[0-9a-f]{64}$")) + and (.workerConsumptionReceiptHash | test("^[0-9a-f]{64}$")) + and (.workerExecutionEffectHash | test("^[0-9a-f]{64}$")) + and (.workerResultEffectHash | test("^[0-9a-f]{64}$")) + and (.manifestCanaryEvidenceHash | test("^[0-9a-f]{64}$")) + and (.qualifiedManifestHash | test("^[0-9a-f]{64}$")) + and .selectedTrackCount == 25 + and .reserveTrackCount >= 1 + and .attemptCount >= 1 + and .manifestOnly == true + and .appleWriteAccess == "forbidden" + and .autoPublish == false + and .manifestRows == 0 + and .matchingJobs == 0 + and .publicationJobs == 0 + and .publicationVolumeRows == 0 + and .orphanPlaylistRows == 0 + and .noPublishedUi == true + and .runReused == false + and .realUiPath == true + ) + ) + and (.evidenceHash | test("^[0-9a-f]{64}$"))' \ + "$GATE" + + - name: Remove protected private inputs before upload + if: always() + run: | + rm -f \ + "$RUNNER_TEMP/release-gate-producer-private-key.pem" \ + "$RUNNER_TEMP/release-secret-versions.json" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: v254-pre-exposure-clean-nonowner-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/pre-exposure-clean-nonowner.source.json + ${{ runner.temp }}/pre-exposure-clean-nonowner.gate.json + ${{ runner.temp }}/pre-exposure-clean-nonowner.attestation.json + ${{ runner.temp }}/pre-exposure-runtime-snapshot.json + ${{ runner.temp }}/pre-exposure-browser + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/v254-pre-exposure-route-pause.yml b/.github/workflows/v254-pre-exposure-route-pause.yml new file mode 100644 index 0000000..b1f6017 --- /dev/null +++ b/.github/workflows/v254-pre-exposure-route-pause.yml @@ -0,0 +1,273 @@ +name: v254 pre-exposure route pause + +on: + workflow_dispatch: + inputs: + source_revision: + description: Exact merged v2.5.4 SHA already promoted + required: true + type: string + release_version: + description: Exact v2.5.4 image version + required: true + type: string + phase: + description: Audited paused pre-exposure route authority phase + required: true + type: choice + options: + - owner_gate + - public_canary + backend_promotion_run_id: + description: Successful exact-image native schema20 promotion run + required: true + type: string + prior_authority_run_id: + description: Prior successful pause run, or 0 for owner_gate + required: true + default: "0" + type: string + prior_authority_artifact: + description: Prior pause artifact name, or none for owner_gate + required: true + default: none + type: string + proof_run_id: + description: Exact-SHA owner Apple proof run, or 0 for owner_gate + required: true + default: "0" + type: string + proof_artifact: + description: Owner Apple gate artifact, or none for owner_gate + required: true + default: none + type: string + proof_file: + description: Owner Apple gate JSON filename, or none for owner_gate + required: true + default: none + type: string + +concurrency: + group: v254-pre-exposure-route-pause + cancel-in-progress: false + +permissions: + actions: read + contents: read + +jobs: + pause: + name: exact-artifact paused ${{ inputs.phase }} + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + RAILWAY_PROJECT_ID: 7e7a44ee-3e54-453c-9120-3cb1f33db76e + RAILWAY_INTERACTIVE_SERVICE_ID: 3affc435-074d-4f6f-806f-1c2b552a8b17 + RAILWAY_DEEP_SERVICE_ID: a3469457-f56e-4871-8a10-5ea6c052640c + RAILWAY_API_SERVICE_ID: 11ba75aa-2495-4f28-bbde-64c43efbe731 + RELEASE_GATE_PRODUCER_KEY_ID: ${{ vars.RELEASE_GATE_PRODUCER_KEY_ID }} + RELEASE_GATE_PRODUCER_KEY_SHA256: ${{ vars.RELEASE_GATE_PRODUCER_KEY_SHA256 }} + steps: + - name: Validate exact paused authority selectors + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + PHASE: ${{ inputs.phase }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + PRIOR_RUN_ID: ${{ inputs.prior_authority_run_id }} + PROOF_RUN_ID: ${{ inputs.proof_run_id }} + PRIOR_ARTIFACT: ${{ inputs.prior_authority_artifact }} + PROOF_ARTIFACT: ${{ inputs.proof_artifact }} + PROOF_FILE: ${{ inputs.proof_file }} + run: | + set -euo pipefail + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] + [[ "$BACKEND_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$PRIOR_RUN_ID" == "0" || "$PRIOR_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$PROOF_RUN_ID" == "0" || "$PROOF_RUN_ID" =~ ^[1-9][0-9]*$ ]] + test -n "$RAILWAY_TOKEN" + if [[ "$PHASE" == "owner_gate" ]]; then + test "$PRIOR_RUN_ID" = "0" + test "$PRIOR_ARTIFACT" = "none" + test "$PROOF_RUN_ID" = "0" + test "$PROOF_ARTIFACT" = "none" + test "$PROOF_FILE" = "none" + else + test "$PHASE" = "public_canary" + test "$PRIOR_RUN_ID" != "0" + test "$PRIOR_ARTIFACT" = "v254-pre-exposure-route-pause-owner_gate-$SOURCE_REVISION" + test "$PROOF_RUN_ID" != "0" + test "$PROOF_ARTIFACT" = "v254-owner-apple-gate-$SOURCE_REVISION" + test "$PROOF_FILE" = "production-affected-regression.gate.json" + [[ "$RELEASE_GATE_PRODUCER_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:+/-]{0,159}$ ]] + [[ "$RELEASE_GATE_PRODUCER_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + fi + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require exact merged source + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + test "$GITHUB_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + git fetch --no-tags origin "$DEFAULT_BRANCH" + git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" + + - name: Require proof-producing runs for the exact SHA + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + PRIOR_RUN_ID: ${{ inputs.prior_authority_run_id }} + PROOF_RUN_ID: ${{ inputs.proof_run_id }} + PHASE: ${{ inputs.phase }} + run: | + set -euo pipefail + for RUN_ID in "$BACKEND_RUN_ID" "$PRIOR_RUN_ID" "$PROOF_RUN_ID"; do + if [[ "$RUN_ID" == "0" ]]; then continue; fi + RUN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID")" + test "$(jq -r '.event' <<<"$RUN")" = "workflow_dispatch" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + done + BACKEND="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BACKEND_RUN_ID")" + test "$(jq -r '.path' <<<"$BACKEND")" = ".github/workflows/native-schema20-release.yml" + if [[ "$PRIOR_RUN_ID" != "0" ]]; then + PRIOR="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$PRIOR_RUN_ID")" + test "$(jq -r '.path' <<<"$PRIOR")" = ".github/workflows/v254-pre-exposure-route-pause.yml" + fi + if [[ "$PROOF_RUN_ID" != "0" ]]; then + test "$PHASE" = "public_canary" + PROOF="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$PROOF_RUN_ID")" + test "$(jq -r '.path' <<<"$PROOF")" = ".github/workflows/v254-owner-apple-gate.yml" + fi + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: native-schema20-promotion-${{ inputs.source_revision }} + path: ${{ runner.temp }}/backend + run-id: ${{ inputs.backend_promotion_run_id }} + github-token: ${{ github.token }} + + - name: Download prior paused authority receipt + if: inputs.prior_authority_run_id != '0' + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.prior_authority_artifact }} + path: ${{ runner.temp }}/prior + run-id: ${{ inputs.prior_authority_run_id }} + github-token: ${{ github.token }} + + - name: Download exact owner Apple proof + if: inputs.proof_run_id != '0' + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.proof_artifact }} + path: ${{ runner.temp }}/proof + run-id: ${{ inputs.proof_run_id }} + github-token: ${{ github.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Materialize and pin the proof producer verification key + if: inputs.proof_run_id != '0' + env: + RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64: ${{ secrets.RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64 }} + run: | + set -euo pipefail + test -n "$RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64" + umask 077 + printf '%s' "$RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/release-gate-producer-public-key.pem" + OBSERVED_KEY_SHA256="$(node -e 'const c=require("node:crypto"),f=require("node:fs"); const k=c.createPublicKey(f.readFileSync(process.argv[1])); if(k.asymmetricKeyType!=="ed25519") throw new Error("proof producer key must be Ed25519"); process.stdout.write(c.createHash("sha256").update(k.export({format:"der",type:"spki"})).digest("hex"));' "$RUNNER_TEMP/release-gate-producer-public-key.pem")" + test "$OBSERVED_KEY_SHA256" = "$RELEASE_GATE_PRODUCER_KEY_SHA256" + + - name: Install exact lockfile and pinned Railway CLI + run: | + pnpm install --frozen-lockfile + npm install --global @railway/cli@4.65.0 + test "$(railway --version)" = "railway 4.65.0" + + - name: Dry-run then apply the identical paused authority plan + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + PHASE: ${{ inputs.phase }} + PRIOR_RUN_ID: ${{ inputs.prior_authority_run_id }} + PROOF_RUN_ID: ${{ inputs.proof_run_id }} + PROOF_FILE: ${{ inputs.proof_file }} + run: | + set -euo pipefail + COMMON=( + --phase "$PHASE" + --rollback-target owner_gate + --promotion-receipt "$RUNNER_TEMP/backend/native-schema20-promotion.json" + --source-revision "$SOURCE_REVISION" + --version "$RELEASE_VERSION" + --project-id "$RAILWAY_PROJECT_ID" + --environment production + --interactive-service "$RAILWAY_INTERACTIVE_SERVICE_ID" + --deep-service "$RAILWAY_DEEP_SERVICE_ID" + --api-service "$RAILWAY_API_SERVICE_ID" + --origin https://9enio.com + --deployment-timeout-seconds 600 + --poll-interval-seconds 10 + ) + if [[ "$PRIOR_RUN_ID" != "0" ]]; then + COMMON+=(--prior-authority-receipt "$RUNNER_TEMP/prior/v254-editorial-route-authority.json") + fi + if [[ "$PROOF_RUN_ID" != "0" ]]; then + test "$PHASE" = "public_canary" + COMMON+=(--proof-artifact "$RUNNER_TEMP/proof/$PROOF_FILE") + COMMON+=( + --proof-attestation "$RUNNER_TEMP/proof/production-affected-regression.attestation.json" + --producer-verification-key "$RUNNER_TEMP/release-gate-producer-public-key.pem" + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" + --producer-key-sha256 "$RELEASE_GATE_PRODUCER_KEY_SHA256" + ) + fi + pnpm release:v254:route-control -- dry-run "${COMMON[@]}" \ + --output "$RUNNER_TEMP/v254-pre-exposure-route-pause-plan.json" + APPROVAL_HASH="$(jq -er '.approvalHash | select(test("^[0-9a-f]{64}$"))' \ + "$RUNNER_TEMP/v254-pre-exposure-route-pause-plan.json")" + pnpm release:v254:route-control -- apply "${COMMON[@]}" \ + --approval-hash "$APPROVAL_HASH" \ + --output "$RUNNER_TEMP/v254-editorial-route-authority.json" + jq -e --arg phase "$PHASE" --arg approval "$APPROVAL_HASH" \ + '.schemaVersion == "genio-v254-editorial-route-authority/v1" + and .phase == $phase and .approvalHash == $approval + and .database.hardSwitchDisabled == false + and .database.globalPublicPause == true + and .database.intentPublicPause == true + and (.imageDigest | test("^sha256:[0-9a-f]{64}$"))' \ + "$RUNNER_TEMP/v254-editorial-route-authority.json" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: v254-pre-exposure-route-pause-${{ inputs.phase }}-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/v254-pre-exposure-route-pause-plan.json + ${{ runner.temp }}/v254-editorial-route-authority.json + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/v254-production-burn-in.yml b/.github/workflows/v254-production-burn-in.yml new file mode 100644 index 0000000..a53ee9b --- /dev/null +++ b/.github/workflows/v254-production-burn-in.yml @@ -0,0 +1,276 @@ +name: v254 production burn-in + +on: + workflow_dispatch: + inputs: + source_revision: + description: Full merged v2.5.4 Git SHA already live + required: true + type: string + release_version: + description: Exact live release version + required: true + type: string + candidate_tag: + description: Immutable RC tag for the live candidate + required: true + type: string + backend_promotion_run_id: + description: Successful native schema20 promotion run + required: true + type: string + direct_exposure_run_id: + description: Successful signed direct-exposure transition run + required: true + type: string + direct_exposure_artifact: + description: Authority, warrant, runtime and DB activation artifact + required: true + type: string + production_proof_run_id: + description: Successful post-exposure v8 custom-domain proof run + required: true + type: string + production_proof_artifact: + description: Post-exposure v8 browser gate and attestation artifact + required: true + type: string + prior_burn_in_run_id: + description: Prior successful segment run, or 0 to start a new chain + required: true + default: "0" + type: string + +concurrency: + group: v254-production-burn-in-${{ inputs.candidate_tag }} + cancel-in-progress: false + +permissions: + actions: write + contents: read + +jobs: + monitor: + name: bounded exact-candidate production monitor + runs-on: ubuntu-latest + timeout-minutes: 345 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64: ${{ secrets.RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64 }} + RELEASE_GATE_PRODUCER_KEY_ID: ${{ vars.RELEASE_GATE_PRODUCER_KEY_ID }} + RELEASE_GATE_PRODUCER_KEY_SHA256: ${{ vars.RELEASE_GATE_PRODUCER_KEY_SHA256 }} + RELEASE_DIRECT_EXPOSURE_VERIFICATION_KEY_BASE64: ${{ secrets.RELEASE_VERIFICATION_PUBLIC_KEY_BASE64 }} + RELEASE_DIRECT_EXPOSURE_KEY_ID: ${{ vars.RELEASE_VERIFICATION_KEY_ID }} + RELEASE_DIRECT_EXPOSURE_KEY_SHA256: ${{ vars.RELEASE_VERIFICATION_KEY_SHA256 }} + RAILWAY_PROJECT_ID: 7e7a44ee-3e54-453c-9120-3cb1f33db76e + steps: + - name: Validate exact immutable selectors + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + DIRECT_RUN_ID: ${{ inputs.direct_exposure_run_id }} + DIRECT_ARTIFACT: ${{ inputs.direct_exposure_artifact }} + PROOF_RUN_ID: ${{ inputs.production_proof_run_id }} + PROOF_ARTIFACT: ${{ inputs.production_proof_artifact }} + PRIOR_RUN_ID: ${{ inputs.prior_burn_in_run_id }} + run: | + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$BACKEND_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$DIRECT_RUN_ID" =~ ^[1-9][0-9]*$ ]] + test "$DIRECT_ARTIFACT" = "v254-editorial-direct-exposure-$SOURCE_REVISION" + [[ "$PROOF_RUN_ID" =~ ^[1-9][0-9]*$ ]] + test "$PROOF_ARTIFACT" = "v254-production-proof-$SOURCE_REVISION" + [[ "$PRIOR_RUN_ID" == "0" || "$PRIOR_RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$RELEASE_GATE_PRODUCER_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:-]{2,79}$ ]] + [[ "$RELEASE_GATE_PRODUCER_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + [[ "$RELEASE_DIRECT_EXPOSURE_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:+/-]{0,159}$ ]] + [[ "$RELEASE_DIRECT_EXPOSURE_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + [[ "$RAILWAY_PROJECT_ID" =~ ^[0-9a-f-]{36}$ ]] + test -n "$RAILWAY_TOKEN" + test -n "$RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64" + test -n "$RELEASE_DIRECT_EXPOSURE_VERIFICATION_KEY_BASE64" + + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require the exact merged candidate and immutable RC tag + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + test "$GITHUB_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + test "$(git rev-list -n 1 "$CANDIDATE_TAG")" = "$SOURCE_REVISION" + git fetch --no-tags origin "$DEFAULT_BRANCH" + git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" + + - name: Require successful immutable source evidence + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + DIRECT_RUN_ID: ${{ inputs.direct_exposure_run_id }} + PROOF_RUN_ID: ${{ inputs.production_proof_run_id }} + PRIOR_RUN_ID: ${{ inputs.prior_burn_in_run_id }} + run: | + for RUN_ID in "$BACKEND_RUN_ID" "$DIRECT_RUN_ID" "$PROOF_RUN_ID"; do + RUN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID")" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + done + BACKEND="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$BACKEND_RUN_ID")" + test "$(jq -r '.path' <<<"$BACKEND")" = ".github/workflows/native-schema20-release.yml" + DIRECT="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$DIRECT_RUN_ID")" + test "$(jq -r '.path' <<<"$DIRECT")" = ".github/workflows/v254-editorial-direct-exposure.yml" + PROOF="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$PROOF_RUN_ID")" + test "$(jq -r '.path' <<<"$PROOF")" = ".github/workflows/v254-production-proof.yml" + if [[ "$PRIOR_RUN_ID" != "0" ]]; then + PRIOR="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$PRIOR_RUN_ID")" + test "$(jq -r '.path' <<<"$PRIOR")" = ".github/workflows/v254-production-burn-in.yml" + test "$(jq -r '.status' <<<"$PRIOR")" = "completed" + test "$(jq -r '.conclusion' <<<"$PRIOR")" = "success" + test "$(jq -r '.head_sha' <<<"$PRIOR")" = "$SOURCE_REVISION" + fi + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: native-schema20-promotion-${{ inputs.source_revision }} + path: ${{ runner.temp }}/backend + run-id: ${{ inputs.backend_promotion_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.direct_exposure_artifact }} + path: ${{ runner.temp }}/direct-exposure + run-id: ${{ inputs.direct_exposure_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.production_proof_artifact }} + path: ${{ runner.temp }}/browser + run-id: ${{ inputs.production_proof_run_id }} + github-token: ${{ github.token }} + + - name: Download prior chained checkpoint + if: inputs.prior_burn_in_run_id != '0' + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: v254-production-burn-in-${{ inputs.source_revision }} + path: ${{ runner.temp }}/prior + run-id: ${{ inputs.prior_burn_in_run_id }} + github-token: ${{ github.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Install exact lockfile and pinned Railway CLI + run: | + pnpm install --frozen-lockfile + npm install --global @railway/cli@4.65.0 + test "$(railway --version)" = "railway 4.65.0" + + - name: Materialize protected verification keys + run: | + umask 077 + printf '%s' "$RELEASE_GATE_PRODUCER_PUBLIC_KEY_BASE64" \ + | base64 --decode \ + > "$RUNNER_TEMP/release-gate-producer-public-key.pem" + printf '%s' "$RELEASE_DIRECT_EXPOSURE_VERIFICATION_KEY_BASE64" \ + | base64 --decode \ + > "$RUNNER_TEMP/direct-exposure-verification-key.pem" + + - name: Monitor one bounded segment and fail on the first invariant trip + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + PRIOR_RUN_ID: ${{ inputs.prior_burn_in_run_id }} + run: | + PRIOR_ARGS=() + if [[ "$PRIOR_RUN_ID" != "0" ]]; then + PRIOR_ARGS=( + --prior-receipt + "$RUNNER_TEMP/prior/v254-production-burn-in.json" + ) + fi + node --experimental-transform-types \ + scripts/v254-production-burn-in.ts monitor \ + --candidate-tag "$CANDIDATE_TAG" \ + --source-revision "$SOURCE_REVISION" \ + --version "$RELEASE_VERSION" \ + --backend-promotion "$RUNNER_TEMP/backend/native-schema20-promotion.json" \ + --direct-exposure-authority "$RUNNER_TEMP/direct-exposure/v254-direct-exposure-authority.json" \ + --direct-exposure-rollback-warrant "$RUNNER_TEMP/direct-exposure/v254-direct-exposure-rollback-warrant.json" \ + --direct-exposure-database-activate-receipt "$RUNNER_TEMP/direct-exposure/direct-exposure-database-activate.json" \ + --direct-exposure-runtime-receipt "$RUNNER_TEMP/direct-exposure/direct-exposure-runtime-apply.json" \ + --final-browser-artifact "$RUNNER_TEMP/browser/final-custom-domain-browser.gate.json" \ + --final-browser-attestation "$RUNNER_TEMP/browser/final-custom-domain-browser.attestation.json" \ + --producer-verification-key "$RUNNER_TEMP/release-gate-producer-public-key.pem" \ + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" \ + --producer-key-sha256 "$RELEASE_GATE_PRODUCER_KEY_SHA256" \ + --direct-exposure-verification-key "$RUNNER_TEMP/direct-exposure-verification-key.pem" \ + --direct-exposure-key-id "$RELEASE_DIRECT_EXPOSURE_KEY_ID" \ + --direct-exposure-key-sha256 "$RELEASE_DIRECT_EXPOSURE_KEY_SHA256" \ + "${PRIOR_ARGS[@]}" \ + --project-id "$RAILWAY_PROJECT_ID" \ + --segment-seconds 19200 \ + --poll-interval-seconds 300 \ + --output "$RUNNER_TEMP/v254-production-burn-in.json" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: v254-production-burn-in-${{ inputs.source_revision }} + path: ${{ runner.temp }}/v254-production-burn-in.json + if-no-files-found: error + retention-days: 30 + + - name: Queue the next bounded segment only after preserving this checkpoint + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + BACKEND_RUN_ID: ${{ inputs.backend_promotion_run_id }} + DIRECT_RUN_ID: ${{ inputs.direct_exposure_run_id }} + DIRECT_ARTIFACT: ${{ inputs.direct_exposure_artifact }} + PROOF_RUN_ID: ${{ inputs.production_proof_run_id }} + PROOF_ARTIFACT: ${{ inputs.production_proof_artifact }} + run: | + STATUS="$(jq -r '.status' "$RUNNER_TEMP/v254-production-burn-in.json")" + if [[ "$STATUS" == "complete" ]]; then + exit 0 + fi + test "$STATUS" = "monitoring" + if ! gh workflow run v254-production-burn-in.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref "$CANDIDATE_TAG" \ + --field source_revision="$SOURCE_REVISION" \ + --field release_version="$RELEASE_VERSION" \ + --field candidate_tag="$CANDIDATE_TAG" \ + --field backend_promotion_run_id="$BACKEND_RUN_ID" \ + --field direct_exposure_run_id="$DIRECT_RUN_ID" \ + --field direct_exposure_artifact="$DIRECT_ARTIFACT" \ + --field production_proof_run_id="$PROOF_RUN_ID" \ + --field production_proof_artifact="$PROOF_ARTIFACT" \ + --field prior_burn_in_run_id="$GITHUB_RUN_ID"; then + echo "::warning::The immutable checkpoint is preserved, but automatic resume failed. Dispatch the same workflow with prior_burn_in_run_id=$GITHUB_RUN_ID." + fi diff --git a/.github/workflows/v254-production-proof.yml b/.github/workflows/v254-production-proof.yml new file mode 100644 index 0000000..6d8c1d5 --- /dev/null +++ b/.github/workflows/v254-production-proof.yml @@ -0,0 +1,317 @@ +name: v254 post-exposure production proof + +on: + workflow_dispatch: + inputs: + source_revision: + description: Exact merged v2.5.4 source revision + required: true + type: string + release_version: + description: Exact candidate version + required: true + type: string + candidate_tag: + description: Immutable annotated RC tag + required: true + type: string + image_digest: + description: Immutable promoted backend image digest + required: true + type: string + sites_handoff_run_id: + description: Successful connector evidence handoff run + required: true + type: string + sites_handoff_artifact: + description: Exact connector evidence handoff artifact + required: true + type: string + direct_exposure_run_id: + description: Successful signed direct-exposure run + required: true + type: string + direct_exposure_artifact: + description: Exact signed direct-exposure artifact + required: true + type: string + +concurrency: + group: v254-post-exposure-proof-${{ inputs.source_revision }} + cancel-in-progress: false + +permissions: + actions: read + contents: read + +jobs: + prove: + name: full convergence and real public direct-exposure browser proof + runs-on: ubuntu-latest + timeout-minutes: 45 + environment: production + env: + GH_TOKEN: ${{ github.token }} + RELEASE_PRODUCTION_ORIGIN: https://9enio.com + RELEASE_CANARY_HMAC_SECRET: ${{ secrets.RELEASE_CANARY_HMAC_SECRET }} + RELEASE_VERIFICATION_KEY_ID: ${{ vars.RELEASE_VERIFICATION_KEY_ID }} + RELEASE_VERIFICATION_KEY_SHA256: ${{ vars.RELEASE_VERIFICATION_KEY_SHA256 }} + RELEASE_GATE_PRODUCER_KEY_ID: ${{ vars.RELEASE_GATE_PRODUCER_KEY_ID }} + RELEASE_SITES_CONTROL_PLANE_KEY_ID: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_ID }} + RELEASE_SITES_CONTROL_PLANE_KEY_SHA256: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_SHA256 }} + RELEASE_BROWSER_ARTIFACT_DIR: ${{ runner.temp }}/public-browser + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require the exact immutable candidate and direct-exposure producer + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + SITES_RUN_ID: ${{ inputs.sites_handoff_run_id }} + SITES_ARTIFACT: ${{ inputs.sites_handoff_artifact }} + DIRECT_RUN_ID: ${{ inputs.direct_exposure_run_id }} + DIRECT_ARTIFACT: ${{ inputs.direct_exposure_artifact }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + test "$GITHUB_WORKFLOW_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] + test "$SITES_ARTIFACT" = "v254-sites-connector-handoff-$SOURCE_REVISION" + test "$DIRECT_ARTIFACT" = "v254-editorial-direct-exposure-$SOURCE_REVISION" + git fetch --no-tags origin "$DEFAULT_BRANCH" + git merge-base --is-ancestor "$SOURCE_REVISION" "origin/$DEFAULT_BRANCH" + git fetch --force origin "+refs/tags/$CANDIDATE_TAG:refs/tags/$CANDIDATE_TAG" + test "$(git cat-file -t "refs/tags/$CANDIDATE_TAG")" = "tag" + test "$(git rev-parse "refs/tags/$CANDIDATE_TAG^{}")" = "$SOURCE_REVISION" + for RUN_ID in "$SITES_RUN_ID" "$DIRECT_RUN_ID"; do + [[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]] + RUN="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID")" + test "$(jq -r '.event' <<<"$RUN")" = "workflow_dispatch" + test "$(jq -r '.status' <<<"$RUN")" = "completed" + test "$(jq -r '.conclusion' <<<"$RUN")" = "success" + test "$(jq -r '.head_sha' <<<"$RUN")" = "$SOURCE_REVISION" + done + SITES="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SITES_RUN_ID")" + DIRECT="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$DIRECT_RUN_ID")" + test "$(jq -r '.path' <<<"$SITES")" = ".github/workflows/v254-sites-connector-handoff.yml" + test "$(jq -r '.path' <<<"$DIRECT")" = ".github/workflows/v254-editorial-direct-exposure.yml" + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.sites_handoff_artifact }} + path: ${{ runner.temp }}/sites + run-id: ${{ inputs.sites_handoff_run_id }} + github-token: ${{ github.token }} + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: ${{ inputs.direct_exposure_artifact }} + path: ${{ runner.temp }}/direct + run-id: ${{ inputs.direct_exposure_run_id }} + github-token: ${{ github.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + version: 11.7.0 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22.19.0 + cache: pnpm + + - name: Install exact dependencies and browser + run: | + set -euo pipefail + pnpm install --frozen-lockfile + pnpm exec playwright install --with-deps chromium + + - name: Materialize protected proof inputs + env: + RELEASE_SECRET_VERSIONS_BASE64: ${{ secrets.RELEASE_SECRET_VERSIONS_BASE64 }} + RELEASE_VERIFICATION_PUBLIC_KEY_BASE64: ${{ secrets.RELEASE_VERIFICATION_PUBLIC_KEY_BASE64 }} + RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64: ${{ secrets.RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64 }} + run: | + set -euo pipefail + umask 077 + printf '%s' "$RELEASE_SECRET_VERSIONS_BASE64" | base64 --decode > "$RUNNER_TEMP/release-secret-versions.json" + printf '%s' "$RELEASE_VERIFICATION_PUBLIC_KEY_BASE64" | base64 --decode > "$RUNNER_TEMP/release-verification-public-key.pem" + printf '%s' "$RELEASE_GATE_PRODUCER_PRIVATE_KEY_BASE64" | base64 --decode > "$RUNNER_TEMP/release-gate-producer-private-key.pem" + node -e 'const c=require("node:crypto"),f=require("node:fs"); const pub=c.createPublicKey(f.readFileSync(process.argv[1])); const fp=c.createHash("sha256").update(pub.export({format:"der",type:"spki"})).digest("hex"); if(fp!==process.env.RELEASE_VERIFICATION_KEY_SHA256) throw new Error("protected release verification key mismatch");' "$RUNNER_TEMP/release-verification-public-key.pem" + + - id: bind + name: Cryptographically bind Sites, direct authority, database activation, and runtime apply + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + SITES_PROJECT_ID: appgprj_6a5565cf7d6c8191ab9f2084e8eda856 + run: | + set -euo pipefail + SITES="$RUNNER_TEMP/sites/v254-sites-connector-handoff.json" + AUTHORITY="$RUNNER_TEMP/direct/v254-direct-exposure-authority.json" + WARRANT="$RUNNER_TEMP/direct/v254-direct-exposure-rollback-warrant.json" + RUNTIME="$RUNNER_TEMP/direct/direct-exposure-runtime-apply.json" + ACTIVATE="$RUNNER_TEMP/direct/direct-exposure-database-activate.json" + jq -e --arg sha "$SOURCE_REVISION" --arg version "$RELEASE_VERSION" --arg digest "$IMAGE_DIGEST" --arg project "$SITES_PROJECT_ID" \ + '.schemaVersion == "genio-v254-sites-connector-handoff/v1" + and .candidate.sourceRevision == $sha + and .candidate.version == $version + and .candidate.imageDigest == $digest + and .projectId == $project + and .connectorProduced == true + and .importedVia == "bounded_workflow_dispatch_input" + and (.sitesControlPlaneEvidenceHash | test("^[0-9a-f]{64}$")) + and (.sitesControlPlaneAttestationPayloadHash | test("^[0-9a-f]{64}$")) + and (.handoffHash | test("^[0-9a-f]{64}$")) + and (.versionId | type == "string" and length > 0) + and (.deploymentId | type == "string" and length > 0)' "$SITES" + node --experimental-transform-types scripts/apply-v254-direct-exposure.ts plan \ + --authority "$AUTHORITY" \ + --rollback-warrant "$WARRANT" \ + --verification-key "$RUNNER_TEMP/release-verification-public-key.pem" \ + --expected-key-id "$RELEASE_VERIFICATION_KEY_ID" \ + --expected-key-sha256 "$RELEASE_VERIFICATION_KEY_SHA256" \ + --expected-source-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-image-digest "$IMAGE_DIGEST" \ + --output "$RUNNER_TEMP/direct-exposure-proof-plan.json" + AUTHORITY_HASH="$(jq -er '.authorityPayloadHash | select(test("^[0-9a-f]{64}$"))' "$RUNNER_TEMP/direct-exposure-proof-plan.json")" + WARRANT_HASH="$(jq -er '.rollbackWarrantPayloadHash | select(test("^[0-9a-f]{64}$"))' "$RUNNER_TEMP/direct-exposure-proof-plan.json")" + jq -e --arg sha "$SOURCE_REVISION" --arg authority "$AUTHORITY_HASH" --arg warrant "$WARRANT_HASH" \ + '.schemaVersion == "genio-v254-direct-exposure-runtime/v1" + and .operation == "apply" + and .candidate.sourceRevision == $sha + and .authorityPayloadHash == $authority + and .rollbackWarrantPayloadHash == $warrant + and .exposureClass == "fully_exposed_unproven" + and .organicReliabilityProven == false + and (.services.interactive.deploymentId | type == "string" and length > 0) + and (.services.deep.deploymentId | type == "string" and length > 0) + and (.services.api.deploymentId | type == "string" and length > 0) + and (.heartbeatFenceHash | test("^[0-9a-f]{64}$")) + and (.healthHash | test("^[0-9a-f]{64}$")) + and (.receiptHash | test("^[0-9a-f]{64}$"))' "$RUNTIME" + jq -e --arg authority "$AUTHORITY_HASH" --arg warrant "$WARRANT_HASH" \ + '.schemaVersion == "genio-v254-direct-exposure-apply-receipt/v1" + and .operation == "activate" + and .state == "active" + and .terminal == false + and .authorityPayloadHash == $authority + and .rollbackWarrantPayloadHash == $warrant + and .exposureClass == "fully_exposed_unproven" + and .organicReliabilityProven == false + and (.receiptHash | test("^[0-9a-f]{64}$"))' "$ACTIVATE" + echo "authority_hash=$AUTHORITY_HASH" >> "$GITHUB_OUTPUT" + + - name: Capture full exact-SHA runtime snapshot + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + run: | + set -euo pipefail + node --experimental-transform-types scripts/release-runtime-snapshot.ts \ + --origin https://9enio.com \ + --environment production \ + --scope full \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --secret-versions "$RUNNER_TEMP/release-secret-versions.json" \ + --output "$RUNNER_TEMP/full-runtime-snapshot.json" + + - name: Produce full release convergence + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + node --experimental-transform-types scripts/release-convergence-producer.ts \ + --confirm-production-probe \ + --origin https://9enio.com \ + --scope full \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-sites-revision "$SOURCE_REVISION" \ + --expected-sites-version "$RELEASE_VERSION" \ + --candidate-tag "$CANDIDATE_TAG" \ + --image-digest "$IMAGE_DIGEST" \ + --runtime-snapshot "$RUNNER_TEMP/full-runtime-snapshot.json" \ + --samples 3 \ + --interval-seconds 30 \ + --source-output "$RUNNER_TEMP/release-convergence.source.json" \ + --output "$RUNNER_TEMP/release-convergence.gate.json" \ + --attestation-output "$RUNNER_TEMP/release-convergence.attestation.json" \ + --producer-signing-key "$RUNNER_TEMP/release-gate-producer-private-key.pem" \ + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" + + - name: Produce real clean non-owner direct-exposure browser proof + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + DIRECT_AUTHORITY_HASH: ${{ steps.bind.outputs.authority_hash }} + run: | + set -euo pipefail + node --experimental-transform-types scripts/final-custom-domain-browser-producer.ts \ + --confirm-production-browser \ + --direct-exposure \ + --origin https://9enio.com \ + --expected-revision "$SOURCE_REVISION" \ + --expected-version "$RELEASE_VERSION" \ + --expected-direct-exposure-authority-hash "$DIRECT_AUTHORITY_HASH" \ + --candidate-tag "$CANDIDATE_TAG" \ + --image-digest "$IMAGE_DIGEST" \ + --runtime-snapshot "$RUNNER_TEMP/full-runtime-snapshot.json" \ + --sites-control-plane-evidence "$RUNNER_TEMP/sites/sites-control-plane.json" \ + --sites-control-plane-attestation "$RUNNER_TEMP/sites/sites-control-plane.attestation.json" \ + --sites-control-plane-verification-key "$RUNNER_TEMP/sites/sites-control-plane-public-key.pem" \ + --browser-artifact-dir "$RELEASE_BROWSER_ARTIFACT_DIR" \ + --source-output "$RUNNER_TEMP/final-custom-domain-browser.source.json" \ + --output "$RUNNER_TEMP/final-custom-domain-browser.gate.json" \ + --attestation-output "$RUNNER_TEMP/final-custom-domain-browser.attestation.json" \ + --producer-signing-key "$RUNNER_TEMP/release-gate-producer-private-key.pem" \ + --producer-key-id "$RELEASE_GATE_PRODUCER_KEY_ID" + jq -e --arg authority "$DIRECT_AUTHORITY_HASH" \ + '.gate == "final_custom_domain_browser" + and .sources.browser.schemaVersion == "genio-final-custom-domain-browser/v8" + and .sources.browser.assignmentMode == "direct_exposure" + and .sources.browser.exposureClass == "fully_exposed_unproven" + and .sources.browser.organicReliabilityProven == false + and (.sources.browser.publicAssignmentProbes | length) == 2 + and all(.sources.browser.publicAssignmentProbes[]; + .rolloutEvidenceHash == $authority + and .rolloutStage == "editorial_influence:0->100:fully_exposed_unproven" + and .assignmentAuthority == "signed_public_direct_exposure" + and .publicPercentageBypass == false + and .organicAssignment == false + and .manifestOnly == true + and .appleWriteAccess == "forbidden")' \ + "$RUNNER_TEMP/final-custom-domain-browser.gate.json" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: v254-production-proof-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/direct-exposure-proof-plan.json + ${{ runner.temp }}/full-runtime-snapshot.json + ${{ runner.temp }}/release-convergence.source.json + ${{ runner.temp }}/release-convergence.gate.json + ${{ runner.temp }}/release-convergence.attestation.json + ${{ runner.temp }}/final-custom-domain-browser.source.json + ${{ runner.temp }}/final-custom-domain-browser.gate.json + ${{ runner.temp }}/final-custom-domain-browser.attestation.json + ${{ runner.temp }}/public-browser + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/v254-sites-connector-handoff.yml b/.github/workflows/v254-sites-connector-handoff.yml new file mode 100644 index 0000000..889ce3e --- /dev/null +++ b/.github/workflows/v254-sites-connector-handoff.yml @@ -0,0 +1,119 @@ +name: v254 Sites connector handoff + +on: + workflow_dispatch: + inputs: + source_revision: + description: Exact merged v2.5.4 source revision deployed by Sites + required: true + type: string + release_version: + description: Exact candidate version + required: true + type: string + candidate_tag: + description: Immutable RC tag + required: true + type: string + image_digest: + description: Immutable backend image digest + required: true + type: string + sites_control_plane_evidence_base64: + description: Base64 connector-produced Sites deployment receipt + required: true + type: string + sites_control_plane_attestation_base64: + description: Base64 connector-signed Sites deployment attestation + required: true + type: string + sites_control_plane_public_key_base64: + description: Base64 connector verification public key + required: true + type: string + +concurrency: + group: v254-sites-connector-handoff-${{ inputs.source_revision }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + import: + name: verify bounded connector evidence + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production + env: + RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_ID: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_ID }} + RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_SHA256: ${{ vars.RELEASE_SITES_CONTROL_PLANE_KEY_SHA256 }} + SITES_PROJECT_ID: appgprj_6a5565cf7d6c8191ab9f2084e8eda856 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ inputs.source_revision }} + fetch-depth: 0 + persist-credentials: false + + - name: Require exact immutable source + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + set -euo pipefail + test "$GITHUB_WORKFLOW_SHA" = "$SOURCE_REVISION" + test "$(git rev-parse HEAD)" = "$SOURCE_REVISION" + test "$(node -p 'require("./package.json").version')" = "$RELEASE_VERSION" + [[ "$SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] + [[ "$CANDIDATE_TAG" =~ ^v"$RELEASE_VERSION"-rc\.[1-9][0-9]*$ ]] + [[ "$IMAGE_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] + [[ "$RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_ID" =~ ^[0-9A-Za-z][0-9A-Za-z._:+/-]{2,159}$ ]] + [[ "$RELEASE_SITES_CONTROL_PLANE_TRUSTED_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] + + - name: Materialize bounded connector evidence + env: + SITES_EVIDENCE_BASE64: ${{ inputs.sites_control_plane_evidence_base64 }} + SITES_ATTESTATION_BASE64: ${{ inputs.sites_control_plane_attestation_base64 }} + SITES_PUBLIC_KEY_BASE64: ${{ inputs.sites_control_plane_public_key_base64 }} + run: | + set -euo pipefail + umask 077 + printf '%s' "$SITES_EVIDENCE_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/sites-control-plane.json" + printf '%s' "$SITES_ATTESTATION_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/sites-control-plane.attestation.json" + printf '%s' "$SITES_PUBLIC_KEY_BASE64" | base64 --decode \ + > "$RUNNER_TEMP/sites-control-plane-public-key.pem" + + - name: Verify exact connector handoff + env: + SOURCE_REVISION: ${{ inputs.source_revision }} + RELEASE_VERSION: ${{ inputs.release_version }} + CANDIDATE_TAG: ${{ inputs.candidate_tag }} + IMAGE_DIGEST: ${{ inputs.image_digest }} + run: | + node --experimental-transform-types \ + scripts/v254-sites-connector-handoff.ts \ + --candidate-tag "$CANDIDATE_TAG" \ + --version "$RELEASE_VERSION" \ + --source-revision "$SOURCE_REVISION" \ + --image-digest "$IMAGE_DIGEST" \ + --project-id "$SITES_PROJECT_ID" \ + --sites-control-plane-evidence "$RUNNER_TEMP/sites-control-plane.json" \ + --sites-control-plane-attestation "$RUNNER_TEMP/sites-control-plane.attestation.json" \ + --sites-control-plane-public-key "$RUNNER_TEMP/sites-control-plane-public-key.pem" \ + --output "$RUNNER_TEMP/v254-sites-connector-handoff.json" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: v254-sites-connector-handoff-${{ inputs.source_revision }} + path: | + ${{ runner.temp }}/v254-sites-connector-handoff.json + ${{ runner.temp }}/sites-control-plane.json + ${{ runner.temp }}/sites-control-plane.attestation.json + ${{ runner.temp }}/sites-control-plane-public-key.pem + if-no-files-found: error + retention-days: 30 diff --git a/.railway/README.md b/.railway/README.md index a03e531..9c80c2d 100644 --- a/.railway/README.md +++ b/.railway/README.md @@ -179,6 +179,32 @@ configuration aggregate is recomputed from the API, interactive-worker, and deep-worker hashes. A bare SHA-256 environment value is never accepted as phase-convergence proof. +## Native schema-20 point releases + +Once schema 20, proof architecture 1/native, and protocol 12 are already +authoritative, a code-only point release must not fabricate bridge, expand, +migration, or backfill evidence. Use the protected +`native schema20 point release` workflow. It consumes the successful +`genio-exact-sha-image/v1` receipt and fresh signed candidate evidence, then: + +1. verifies that production is healthy, quiescent, owner-only, and has zero + uncovered jobs; +2. stages only non-secret release identity variables with deploys skipped; +3. connects the same immutable GHCR digest to the existing interactive worker, + then the existing deep worker; +4. requires those worker IDs to be the only fresh heartbeats and to advance + across observations at least 30 seconds apart; +5. connects the existing API last; and +6. requires exact liveness, readiness, schema-20/protocol-12 health, and + backend convergence while Sites remains on the prior proven revision. + +The Railway control-plane phase is `redeploy_native`; the runtime remains +`activate`. This phase is production-only, requires schema 20 and fresh signed +candidate evidence, preserves the existing activation/public-rollout +authority, and has no pre-deploy command. The workflow never invokes +`railway up`, `--from-source`, a service-create command, or a production image +build. + The signed expand envelope also contains the authoritative activation preflight. It must be produced from the fixed complete cohort-inventory query inside a read-only repeatable-read database snapshot, prove the global @@ -206,6 +232,13 @@ from the current production runtime, a fresh signed production-promotion envelope, and (after the first transition) the immediately previous signed rollout envelope: +The v2.5.4 Irish/editorial recovery uses the independent +`editorial_influence` intent group and +`PIPELINE_V3_EDITORIAL_INFLUENCE_PERCENT`. Keep `genre_scene` unchanged while +that repaired route is accepted and exposed. The commands below use +`genre_scene` only as a generic rollout example; substitute the exact signed +intent group for the candidate being advanced. + ```bash pnpm release:rollout:intent-canary:produce -- \ --assignment-receipt owner-candidate-assignment.signed.json \ diff --git a/.railway/railway.ts b/.railway/railway.ts index 8cee26b..c8e5e24 100644 --- a/.railway/railway.ts +++ b/.railway/railway.ts @@ -36,7 +36,9 @@ const workerReplicaCount = releasePhase.phase === "bootstrap" ? 0 : 1; // remains in the already-proven schema-20 native-proof activation mode; only the signed // cohort literals and their evidence marker change. const runtimeDeploymentPhase = - releasePhase.phase === "rollout" ? "activate" : releasePhase.phase; + releasePhase.phase === "rollout" || releasePhase.phase === "redeploy_native" + ? "activate" + : releasePhase.phase; const releaseIdentityVariables = { APP_VERSION: releaseVersion, @@ -45,6 +47,12 @@ const releaseIdentityVariables = { RELEASE_DEPLOYMENT_PHASE: runtimeDeploymentPhase, RELEASE_EXPECTED_DATABASE_SCHEMA_VERSION: releasePhase.expectedDatabaseSchemaVersion, RELEASE_EXECUTION_ENABLED: releasePhase.phase === "bootstrap" ? "false" : "true", + // v2.5.4 is the first production artifact whose admission contract requires + // the V5.1 checkpoint for every new canonical brief. Pin this on all three + // lanes as release identity instead of preserving the v2.5.3 `false` value; + // otherwise an immutable v2.5.4 image would deploy successfully while + // silently continuing to serve the questionless V4 path. + GUIDANCE_V5_ENABLED: "true", RELEASE_SECRET_VERSIONS_HASH: releaseSecretVersionsHash, ...(releasePhase.phase === "bootstrap" ? { @@ -173,14 +181,6 @@ const stagingWorkerVariables: Record = releasePhase.staging } : {}; -// Activate can only use the signed owner-only preflight. Rollout replaces that -// complete variable set with the signed public transition target; no direct -// Railway percentage survives either phase. -const verifiedRolloutVariables = - releasePhase.publicRolloutConfiguration - ?? releasePhase.activationRollout - ?? {}; - const promotedReleaseSource = () => image(releaseImage, { autoUpdates: { type: "disabled" }, }); @@ -190,6 +190,44 @@ const preserved = (names: Names) => [Name in Names[number]]: ReturnType; }; +// A native point release changes only the exact artifact and release identity. +// The schema-20 activation and any signed public-rollout lineage were already +// proven by the currently healthy deployment and must survive byte-for-byte. +const nativeRedeployVariables = releasePhase.phase === "redeploy_native" + ? preserved([ + "RELEASE_EXPECTED_DATABASE_CAPABILITY_VERSION", + "RELEASE_EXPECTED_MANIFEST_CANARY_GUARDS_VERSION", + "RELEASE_EXPECTED_CANONICAL_EXECUTION_HARDENING_VERSION", + "RELEASE_EXPECTED_PROOF_ARCHITECTURE_VERSION", + "PIPELINE_V3_PROOF_ARCHITECTURE_MODE", + "RELEASE_PUBLIC_ROLLOUT_EVIDENCE_HASH", + "RELEASE_PUBLIC_ROLLOUT_STAGE", + "RELEASE_PUBLIC_ROLLOUT_EVIDENCE_ENVELOPE_BASE64", + "RELEASE_PUBLIC_ROLLOUT_ROLLBACK_WARRANT_HASH", + "RELEASE_PUBLIC_ROLLOUT_ROLLBACK_WARRANT_ENVELOPE_BASE64", + "RELEASE_PUBLIC_ROLLOUT_VERIFICATION_KEY_BASE64", + "RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_HASH", + "RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_ENVELOPE_BASE64", + "RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_VERIFICATION_KEY_BASE64", + "RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_KEY_ID", + "RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_KEY_SHA256", + "RELEASE_PUBLIC_ROLLOUT_INTENT_CANARY_AUTHORITY_POLICY_SHA256", + "RELEASE_V254_DIRECT_EXPOSURE_AUTHORITY_HASH", + "RELEASE_V254_DIRECT_EXPOSURE_ROLLBACK_WARRANT_HASH", + "RELEASE_V254_DIRECT_EXPOSURE_STAGE", + "RELEASE_V254_DIRECT_EXPOSURE_TARGET_CONFIGURATION_HASH", + ] as const) + : {}; + +// Activate can only use the signed owner-only preflight. Rollout replaces that +// complete variable set with the signed public transition target; no direct +// Railway percentage survives either phase. Native redeploy preserves the +// exact already-authorized activation/rollout settings. +const verifiedRolloutVariables = + releasePhase.publicRolloutConfiguration + ?? releasePhase.activationRollout + ?? nativeRedeployVariables; + const apiVariables: Record> = releasePhase.phase === "bootstrap" ? {} : preserved([ "APP_ORIGIN", @@ -249,6 +287,7 @@ const apiVariables: Record> = "PIPELINE_V3_BASELINE_MODEL_ID", "PIPELINE_V3_ESCALATION_MODEL_ID", "PIPELINE_V3_MODEL_CATALOG_VALIDATED_AT", + "PIPELINE_V3_EDITORIAL_INFLUENCE_PERCENT", "PIPELINE_V3_GENRE_SCENE_PERCENT", "PIPELINE_V3_MOOD_ACTIVITY_PERCENT", "PIPELINE_V3_SIMILARITY_PERCENT", @@ -315,6 +354,7 @@ const workerVariables: Record> = "PIPELINE_V3_OWNER_CANARY_GROUPS", "PIPELINE_V3_OWNER_CANARY_MAX_TRACKS", "PIPELINE_V3_ASSIGNMENT_ENABLED", + "PIPELINE_V3_EDITORIAL_INFLUENCE_PERCENT", "PIPELINE_V3_GENRE_SCENE_PERCENT", "PIPELINE_V3_MOOD_ACTIVITY_PERCENT", "PIPELINE_V3_SIMILARITY_PERCENT", @@ -464,6 +504,8 @@ export default defineRailway((context) => { PIPELINE_V3_OWNER_CANARY_GROUPS: needleWorker.env.PIPELINE_V3_OWNER_CANARY_GROUPS, PIPELINE_V3_OWNER_CANARY_MAX_TRACKS: needleWorker.env.PIPELINE_V3_OWNER_CANARY_MAX_TRACKS, + PIPELINE_V3_EDITORIAL_INFLUENCE_PERCENT: + needleWorker.env.PIPELINE_V3_EDITORIAL_INFLUENCE_PERCENT, PIPELINE_V3_GENRE_SCENE_PERCENT: needleWorker.env.PIPELINE_V3_GENRE_SCENE_PERCENT, PIPELINE_V3_MOOD_ACTIVITY_PERCENT: needleWorker.env.PIPELINE_V3_MOOD_ACTIVITY_PERCENT, diff --git a/.railway/release-phase.ts b/.railway/release-phase.ts index dd942ea..3f29412 100644 --- a/.railway/release-phase.ts +++ b/.railway/release-phase.ts @@ -36,6 +36,7 @@ export const RELEASE_DEPLOYMENT_PHASES = [ "bridge", "expand", "activate", + "redeploy_native", "rollout", ] as const; export type ReleaseDeploymentPhase = typeof RELEASE_DEPLOYMENT_PHASES[number]; @@ -770,13 +771,16 @@ export function railwayReleasePhaseConfiguration( const phaseValue = required(environment, "GENIO_RELEASE_PHASE"); if (!(RELEASE_DEPLOYMENT_PHASES as readonly string[]).includes(phaseValue)) { throw new Error( - "GENIO_RELEASE_PHASE must be bootstrap, bridge, expand, activate, or rollout", + "GENIO_RELEASE_PHASE must be bootstrap, bridge, expand, activate, redeploy_native, or rollout", ); } const phase = phaseValue as PromotionReleaseDeploymentPhase; if (releaseEnvironment === "staging" && phase === "rollout") { throw new Error("public rollout is production-only"); } + if (phase === "redeploy_native" && releaseEnvironment !== "production") { + throw new Error("redeploy_native is accepted only for production"); + } const verifiedCandidateEvidenceHash = releaseEnvironment === "production" ? signedCandidateEvidenceHash(environment) : null; @@ -788,7 +792,12 @@ export function railwayReleasePhaseConfiguration( throw new Error("GENIO_EXPECTED_DATABASE_SCHEMA_VERSION must be an integer from 13 through 20"); } if ( - (phase === "expand" || phase === "activate" || phase === "rollout") + ( + phase === "expand" + || phase === "activate" + || phase === "redeploy_native" + || phase === "rollout" + ) && expectedDatabaseSchemaVersion !== EXPANDED_DATABASE_SCHEMA_VERSION ) { throw new Error( @@ -816,7 +825,11 @@ export function railwayReleasePhaseConfiguration( let publicRolloutRollbackWarrantHash: string | null = null; let publicRolloutRollbackWarrantEnvelopeBase64: string | null = null; let publicRolloutVerificationKeyBase64: string | null = null; - if (releaseEnvironment === "production" && phase !== "bridge") { + if ( + releaseEnvironment === "production" + && phase !== "bridge" + && phase !== "redeploy_native" + ) { const bridgeEvidence = signedPromotionPhaseEvidence({ environment, fileEnvironmentName: "GENIO_BRIDGE_CONVERGENCE_EVIDENCE_FILE", diff --git a/app/about/page.tsx b/app/about/page.tsx index 772b691..9f19e6b 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -20,8 +20,14 @@ export default function AboutPage() {

ABOUT GÊNIO

gênio v{currentRelease.version}

- CURRENT RELEASE - + {currentRelease.status === "candidate" + ? "RELEASE CANDIDATE" + : "CURRENT RELEASE"} + {currentRelease.releasedAt + ? + : PROOF PENDING}

Deep music research, assembled into public Apple Music playlists. Release history and the deployed API build are shown below.

@@ -42,7 +48,11 @@ export default function AboutPage() { {index === 0 ? "CURRENT" : "ARCHIVE"}

v{release.version} · {release.title}

- + {release.releasedAt + ? + : RELEASE CANDIDATE · PROOF PENDING}
    {release.notes.map((note) =>
  • {note}
  • )} diff --git a/app/playlist-builder-ui-policy.ts b/app/playlist-builder-ui-policy.ts index e52bf33..e7c116a 100644 --- a/app/playlist-builder-ui-policy.ts +++ b/app/playlist-builder-ui-policy.ts @@ -17,6 +17,19 @@ export type PartialReadyRun = { status?: string; phase?: string; pipelineVersion?: string; + executionRouteReceipt?: { + version?: string; + executionRoute?: string; + receiptHash?: string; + executorConfigurationHash?: string; + } | null; + evidenceCoverage?: { + observationCount?: number; + uniqueLeadCount?: number; + materializedCandidateCount?: number; + } | null; + candidateCount?: number; + sourceCount?: number; error?: string | null; actionRequired?: PartialPublicationAction | null; partialAction?: PartialPublicationAction | null; @@ -31,13 +44,22 @@ export type PartialReadyRun = { reason?: string; decisionHash?: string; actions?: { + anotherBoundedPass?: boolean; + reviseNamedPredicate?: boolean; + reduceCount?: boolean; + publishVerifiedPartial?: boolean; + pause?: boolean; resumeLater?: boolean; + cancel?: boolean; }; } | null; resolution?: { + generation?: number | null; state?: string; nextAction?: string; terminal?: boolean; + contractRevisionId?: string | null; + contractHash?: string | null; blocker?: { kind?: string; nextRetryAt?: string | null; @@ -45,8 +67,68 @@ export type PartialReadyRun = { versionHash?: string | null; } | null; } | null; + repairReplayAction?: { + kind?: string; + expectedGeneration?: number; + incidentReference?: string; + contractRevisionId?: string; + contractSemanticHash?: string; + available?: boolean; + availabilityReason?: string; + successorBriefRequestId?: string | null; + resultReuse?: boolean; + autoPublication?: boolean; + } | null; +}; + +export type BriefExecutionDecisionResponse = { + status?: string; + executionAction?: { + decisionHash?: string; + optionId?: string; + kind?: string; + startsResearch?: boolean; + actionHash?: string; + } | null; }; +export type BriefExecutionDecisionDisposition = + | "execute" + | "review" + | "cancelled"; + +/** + * Converts the hash-bound server action into one browser disposition. Review + * and cancel are deliberately terminal for polling/research admission. + */ +export function briefExecutionDecisionDisposition( + response: BriefExecutionDecisionResponse | null | undefined, +): BriefExecutionDecisionDisposition | null { + const action = response?.executionAction; + if (!action + || !validDecisionHash(action.decisionHash) + || !validDecisionHash(action.actionHash) + || typeof action.optionId !== "string") { + return null; + } + if (action.kind === "execute_confirmed_contract" + && action.startsResearch === true + && ["finalizing", "complete"].includes(response?.status ?? "")) { + return "execute"; + } + if (action.kind === "review_interpretation" + && action.startsResearch === false + && response?.status === "review_required") { + return "review"; + } + if (action.kind === "cancel_request" + && action.startsResearch === false + && response?.status === "cancelled") { + return "cancelled"; + } + return null; +} + export type PartialReadyView = { targetTrackCount: number; qualifiedTrackCount: number; @@ -63,10 +145,111 @@ export type PartialReadyView = { export type RunResolutionControl = | "wait_for_retry" | "resume_dependency" + | "replay_after_repair" + | "continue_repair" + | "repair_pending" | "refine_request" | "contact_support" | "cancel_job"; +export type RunExecutionRouteAuthorityIssue = + | "missing_execution_route_receipt" + | "invalid_execution_route_receipt" + | "execution_route_mismatch"; + +export type RunEvidenceDisplayCounts = { + observationCount: number | null; + uniqueLeadCount: number; + materializedCandidateCount: number; +}; + +function validDecisionHash(value: unknown): value is string { + return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value); +} + +function finiteNonNegativeCount(value: unknown): number | null { + const numeric = Number(value); + return Number.isFinite(numeric) && numeric >= 0 + ? Math.floor(numeric) + : null; +} + +function legacyTerminalRun( + run: PartialReadyRun | null | undefined, +): boolean { + return run?.resolution?.terminal === true + || [ + "complete", + "partial", + "cancelled", + "expired", + "deleted", + ].includes(run?.status ?? ""); +} + +/** + * The route receipt is authoritative for admitted work. Legacy terminal runs + * remain readable without one, but active corpus-first work may never render + * as healthy when its receipt is absent, malformed, or contradicts the + * compatibility pipeline label. + */ +export function runExecutionRouteAuthorityIssue( + run: PartialReadyRun | null | undefined, +): RunExecutionRouteAuthorityIssue | null { + if (!run) return null; + const receipt = run.executionRouteReceipt; + if (!receipt) { + return run.pipelineVersion === "corpus_first_v3" + && !legacyTerminalRun(run) + ? "missing_execution_route_receipt" + : null; + } + if (receipt.version !== "execution_route_receipt_v1" + || typeof receipt.executionRoute !== "string" + || receipt.executionRoute.length === 0 + || !validDecisionHash(receipt.receiptHash) + || !validDecisionHash(receipt.executorConfigurationHash)) { + return "invalid_execution_route_receipt"; + } + return typeof run.pipelineVersion === "string" + && run.pipelineVersion.length > 0 + && run.pipelineVersion !== receipt.executionRoute + ? "execution_route_mismatch" + : null; +} + +/** + * Preserve the three different units exposed by execution truth. Cumulative + * provider observations must never inflate deduplicated leads or materialized + * candidates in the browser. + */ +export function runEvidenceDisplayCounts( + run: PartialReadyRun | null | undefined, +): RunEvidenceDisplayCounts { + const coverage = run?.evidenceCoverage; + return { + observationCount: coverage + ? finiteNonNegativeCount(coverage.observationCount) + : null, + uniqueLeadCount: coverage + ? finiteNonNegativeCount(coverage.uniqueLeadCount) ?? 0 + : finiteNonNegativeCount(run?.sourceCount) ?? 0, + materializedCandidateCount: coverage + ? finiteNonNegativeCount(coverage.materializedCandidateCount) ?? 0 + : finiteNonNegativeCount(run?.candidateCount) ?? 0, + }; +} + +function hasSupportedContractRevisionAction( + run: PartialReadyRun | null | undefined, +): boolean { + return validDecisionHash(run?.decisionAction?.decisionHash) + && ( + run?.decisionAction?.actions?.reviseNamedPredicate === true + || run?.decisionAction?.actions?.reduceCount === true + ); +} + /** * Controls rendered for a public run resolution. This is deliberately based * on supported visitor behavior, not on aspirational orchestration actions. @@ -75,16 +258,61 @@ export type RunResolutionControl = export function runResolutionControls( run: PartialReadyRun | null | undefined, ): RunResolutionControl[] { + if (runExecutionRouteAuthorityIssue(run)) { + return legacyTerminalRun(run) + ? ["contact_support"] + : ["contact_support", "cancel_job"]; + } const resolution = run?.resolution; if (!resolution || resolution.terminal || partialReadyView(run)) return []; + const repairAction = run?.repairReplayAction; + const repairFenceValid = repairAction?.kind === "repair_replay" + && Number.isSafeInteger(repairAction.expectedGeneration) + && Number(repairAction.expectedGeneration) > 0 + && typeof repairAction.incidentReference === "string" + && repairAction.incidentReference.length > 0 + && typeof repairAction.contractRevisionId === "string" + && /^[a-f0-9]{64}$/u.test( + repairAction.contractSemanticHash ?? "", + ) + && repairAction.resultReuse === false + && repairAction.autoPublication === false + && resolution.generation === repairAction.expectedGeneration + && resolution.contractRevisionId === repairAction.contractRevisionId + && resolution.contractHash === repairAction.contractSemanticHash; + if (resolution.state === "quarantined" && repairFenceValid) { + if (repairAction?.availabilityReason === "already_started" + && typeof repairAction.successorBriefRequestId === "string" + && repairAction.successorBriefRequestId.length > 0) { + return ["continue_repair", "cancel_job"]; + } + if (repairAction?.available === false + && ["repair_pending", "route_paused"].includes( + repairAction.availabilityReason ?? "", + )) { + return ["repair_pending", "contact_support", "cancel_job"]; + } + } switch (resolution.nextAction) { case "wait_for_dependency": case "authorize_apple": - return ["wait_for_retry", "refine_request", "cancel_job"]; + return ["wait_for_retry", "cancel_job"]; case "contact_support": - return ["contact_support", "refine_request", "cancel_job"]; + return ["contact_support", "cancel_job"]; + case "replay_after_repair": { + const action = repairAction; + return resolution.state === "quarantined" + && repairFenceValid + && action?.available === true + && action.availabilityReason === "ready" + ? ["replay_after_repair", "cancel_job"] + : ["contact_support", "cancel_job"]; + } case "review_contract": + return hasSupportedContractRevisionAction(run) + ? ["refine_request", "cancel_job"] + : ["contact_support", "cancel_job"]; case "answer_initial_guidance": case "answer_rescue_guidance": return ["refine_request", "cancel_job"]; @@ -95,20 +323,20 @@ export function runResolutionControls( && /^[a-f0-9]{64}$/u.test(resolution.blocker.versionHash) && run?.decisionAction?.reason === "dependency_retry_window_expired" && run.decisionAction.actions?.resumeLater === true - && typeof run.decisionAction.decisionHash === "string" - && /^[a-f0-9]{64}$/u.test(run.decisionAction.decisionHash) - ? ["resume_dependency", "refine_request", "cancel_job"] - : ["refine_request", "cancel_job"]; + && validDecisionHash(run.decisionAction.decisionHash) + ? ["resume_dependency", "cancel_job"] + : ["contact_support", "cancel_job"]; case "decide_verified_partial": // A malformed or stale partial action cannot power the explicit - // decision API, so fall back to a fresh contract revision. - return ["refine_request", "cancel_job"]; + // decision API. Treat that as a technical repair instead of implying + // that rewriting the user's request will repair our state. + return ["contact_support", "cancel_job"]; default: if (resolution.state === "quarantined") { - return ["contact_support", "refine_request", "cancel_job"]; + return ["contact_support", "cancel_job"]; } if (resolution.state === "needs_input" || resolution.state === "needs_decision") { - return ["refine_request", "cancel_job"]; + return ["contact_support", "cancel_job"]; } return []; } @@ -117,7 +345,23 @@ export function runResolutionControls( export function shouldKeepPollingBlockedRun( run: PartialReadyRun | null | undefined, ): boolean { - return run?.resolution?.state === "blocked_dependency"; + const resolution = run?.resolution; + return resolution?.state === "blocked_dependency" + || ( + resolution?.state === "quarantined" + && resolution.terminal !== true + && ![ + "complete", + "partial", + "failed", + "no_compatible_tracks", + "cancelled", + "failed_system", + "failed_integrity", + "expired", + "deleted", + ].includes(run?.status ?? "") + ); } function asObject(value: unknown): UnknownObject { @@ -192,6 +436,20 @@ function finiteCount(...values: unknown[]): number | null { */ export function partialReadyView(run: PartialReadyRun | null | undefined): PartialReadyView | null { if (!run) return null; + // The partial-decision payload remains attached to the run as immutable + // consent lineage after the visitor accepts it. It is no longer actionable + // once manifest handoff or Apple publication begins. Treating that retained + // payload as a live decision tears down polling before the authoritative + // publishing/completed resolution can arrive. + if ([ + "manifest_ready", + "publishing", + "waiting_for_apple_authorization", + "complete", + "partial", + ].includes(run.status ?? "")) { + return null; + } const action = run.partialAction ?? run.actionRequired; const explicitAction = action?.kind === "partial_publication" || action?.kind === "partial_confirmation"; diff --git a/app/playlist-builder.tsx b/app/playlist-builder.tsx index 8a7747d..e59af4f 100644 --- a/app/playlist-builder.tsx +++ b/app/playlist-builder.tsx @@ -12,6 +12,8 @@ import { } from "../shared/fast-run-sla.ts"; import type { RunDecisionActionView, + RunEvidenceCoverageView, + RunExecutionRouteReceiptView, RunGuidanceActionView, RunProgressView, } from "../shared/types.ts"; @@ -23,12 +25,15 @@ import { WorkingIndicator } from "./working-indicator"; import { actionRequiredJobLabel, apiErrorCode, + briefExecutionDecisionDisposition, evidenceCountSummary, partialDecisionHeading, partialDecisionSummary, partialReadyView, publishedTrackCountSummary, publishedResultHeading, + runEvidenceDisplayCounts, + runExecutionRouteAuthorityIssue, runResolutionControls, shouldKeepPollingBlockedRun, shouldPresentShortfallWithoutError, @@ -89,6 +94,7 @@ type ResearchRun = { frontier: FrontierItem[]; pipelineVersion?: string; policyVersion?: string; + executionRouteReceipt?: RunExecutionRouteReceiptView | null; selectionPlan?: { requestedTrackCount?: number; reserveTrackCount?: number; @@ -108,12 +114,13 @@ type ResearchRun = { decisionAction?: RunDecisionActionView | null; guidanceAction?: RunGuidanceActionView | null; candidateStageCounts?: Partial>; + evidenceCoverage?: RunEvidenceCoverageView; progress?: RunProgressView; resolution?: { generation?: number | null; state: "accepted" | "needs_input" | "probing" | "executing" | "blocked_dependency" | "needs_decision" | "ready" | "publishing" | "completed" | "cancelled" | "quarantined"; workMotion?: "running" | "retry_scheduled" | "waiting_dependency" | "paused" | "stalled" | "none"; - nextAction: "none" | "answer_initial_guidance" | "answer_rescue_guidance" | "wait_for_dependency" | "resume_research" | "authorize_apple" | "decide_verified_partial" | "review_contract" | "contact_support"; + nextAction: "none" | "answer_initial_guidance" | "answer_rescue_guidance" | "wait_for_dependency" | "resume_research" | "replay_after_repair" | "authorize_apple" | "decide_verified_partial" | "review_contract" | "contact_support"; terminal: boolean; contractRevisionId: string | null; contractRevision: number | null; @@ -126,6 +133,22 @@ type ResearchRun = { versionHash: string | null; } | null; }; + repairReplayAction?: { + kind: "repair_replay"; + expectedGeneration: number; + incidentReference: string; + contractRevisionId: string; + contractSemanticHash: string; + available: boolean; + availabilityReason: + | "ready" + | "repair_pending" + | "route_paused" + | "already_started"; + successorBriefRequestId?: string | null; + resultReuse: false; + autoPublication: false; + } | null; createdAt?: string; updatedAt?: string; }; @@ -241,8 +264,23 @@ type BriefResponse = { questions?: GuidedQuestion[]; briefContractVersion?: 1 | 2 | 3; questionSetHash?: string | null; - checkpointMode?: "correctness_blocking" | "nuance_optional" | "interpretation_confirmation" | null; + checkpointMode?: + | "correctness_blocking" + | "nuance_optional" + | "interpretation_confirmation" + | "execution_decision" + | null; interpretationSummary?: GuidanceInterpretationSummary | null; + executionAction?: { + decisionHash: string; + optionId: string; + kind: + | "execute_confirmed_contract" + | "review_interpretation" + | "cancel_request"; + startsResearch: boolean; + actionHash: string; + } | null; error?: string; }; @@ -259,7 +297,15 @@ type GuidedQuestionOption = { label: string; description?: string; recommended?: boolean; + recommendationReason?: string; feasibility?: "broad" | "moderate" | "narrow"; + executionAction?: { + kind: + | "execute_confirmed_contract" + | "review_interpretation" + | "cancel_request"; + startsResearch: boolean; + }; }; type GuidedQuestionGrounding = { @@ -276,6 +322,11 @@ type GuidedQuestion = { criticality?: "required" | "optional"; selectionMode?: "single" | "multiple"; allowCustom?: boolean; + guidanceMode?: + | "correctness_blocking" + | "nuance_optional" + | "interpretation_confirmation" + | "execution_decision"; interpretationSummary?: GuidanceInterpretationSummary; options: GuidedQuestionOption[]; }; @@ -438,6 +489,12 @@ async function waitForBrief(requestId: string, initialDelayMs = 1_500, signal?: await abortableDelay(delayMs, signal); const response = await api("/api/v1/brief/" + encodeURIComponent(requestId), { signal }); if (response.status === "failed") throw new BriefInterpretationError(response.error || "Scope interpretation failed."); + const executionDisposition = + briefExecutionDecisionDisposition(response); + if (executionDisposition === "review" + || executionDisposition === "cancelled") { + return response; + } if (response.status === "awaiting_answers" && ( response.questions?.length || ( @@ -755,7 +812,27 @@ async function copyText(value: string): Promise { if (!copied) throw new Error("The transfer link could not be copied in this browser."); } +function isTechnicalRepairRun(run: ResearchRun): boolean { + const phase = run.phase.trim().toLowerCase(); + return run.resolution?.state === "quarantined" + || phase === "capability_evidence_coverage_audit" + || phase === "runtime_feasibility_unknown" + || phase === "canonical_contract_unknown" + || phase === "evidence_verification_unknown" + || phase.startsWith("v3_semantic_collapse_"); +} + function phaseMessage(run: ResearchRun): string { + const routeAuthorityIssue = runExecutionRouteAuthorityIssue(run); + if (routeAuthorityIssue === "missing_execution_route_receipt") { + return "Execution is paused because this active run has no immutable execution-route receipt. Your request is preserved for technical repair; rewriting it will not fix the route authority."; + } + if (routeAuthorityIssue === "execution_route_mismatch") { + return "Execution is paused because the saved route receipt does not match the displayed execution route. Your request is preserved and no route will be trusted until the mismatch is repaired."; + } + if (routeAuthorityIssue === "invalid_execution_route_receipt") { + return "Execution is paused because its immutable route receipt could not be validated. Your request is preserved for technical repair."; + } if (run.phase === "dependency_resume_scheduled") { return "You authorized another exact-contract attempt. It will run when the dependency and matching executor are available."; } @@ -775,6 +852,16 @@ function phaseMessage(run: ResearchRun): string { ? `Research is safely paused for a dependency and will retry after ${new Date(retryAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "Research is safely paused for a dependency. Your progress remains saved."; } + if (isTechnicalRepairRun(run)) { + if (run.evidenceCoverage) { + return `We found ${ + run.evidenceCoverage.uniqueLeadCount.toLocaleString() + } unique leads and ${ + run.evidenceCoverage.storefrontPlayable.toLocaleString() + } Apple-available candidates, but the evidence service could not verify the required obligations. Your request is preserved.`; + } + return "Research found candidate recordings, but the evidence verifier could not validate the required proof. Your saved request is preserved for technical repair; you do not need to rewrite it."; + } if (run.resolution?.state === "needs_input") { return run.resolution.nextAction === "answer_rescue_guidance" ? "A focused scope decision is needed to finish this playlist without weakening its quality." @@ -846,7 +933,7 @@ function useRunPolling( const automaticHandoff = !actionRequired && autoPublish && Boolean( runStatus && (reviewStatuses.has(runStatus) || runStatus === "manifest_ready"), ); - if (actionRequired) return; + if (actionRequired && !pollWhileBlocked) return; if (runStatus && !automaticHandoff && !pollWhileBlocked && (terminalStatuses.has(runStatus) || reviewStatuses.has(runStatus) || runStatus === "manifest_ready")) return; let cancelled = false; @@ -862,7 +949,7 @@ function useRunPolling( const nextBlocked = shouldKeepPollingBlockedRun(next); const nextAutomaticHandoff = !nextActionRequired && next.autoPublish === true && (reviewStatuses.has(next.status) || next.status === "manifest_ready"); - if (nextActionRequired) return; + if (nextActionRequired && !nextBlocked) return; if (!nextAutomaticHandoff && !nextBlocked && (terminalStatuses.has(next.status) || reviewStatuses.has(next.status) || next.status === "manifest_ready")) return; pollCount += 1; @@ -1262,6 +1349,10 @@ function GuidedQuestionScreen({ const currentAnswer = answers[question.id]; const customSelected = typeof currentAnswer?.customText === "string"; const customText = currentAnswer?.customText ?? ""; + const selectedExecutionAction = question.guidanceMode === "execution_decision" + ? question.options.find(({ id }) => id === currentAnswer?.optionId) + ?.executionAction?.kind + : undefined; const orderedOptions = [...question.options] .sort((left, right) => Number(right.recommended) - Number(left.recommended)) .slice(0, 4); @@ -1413,6 +1504,11 @@ function GuidedQuestionScreen({ {option.recommended && RECOMMENDED} {option.description && {option.description}} + {option.recommended && option.recommendationReason && ( + + Why recommended: {option.recommendationReason} + + )} ); @@ -1446,7 +1542,10 @@ function GuidedQuestionScreen({ onChange={(event) => onAnswer({ questionId: question.id, customText: event.target.value })} /> } - {question.criticality === "optional" && ( + {question.criticality === "optional" + && !question.options.some( + ({ id }) => id === "keep_current_interpretation", + ) && (