From 9f4172178ca386fa97ab8d523cfa95b3f3862cb4 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Sun, 9 Aug 2026 18:59:25 +0000 Subject: [PATCH 1/3] test(release): validate credential-free brand pilot Amp-Thread-ID: https://ampcode.com/threads/T-019fe7ac-7d82-754e-a203-5d0214817d24 --- .github/scripts/brand-matrix.test.mjs | 81 +++++++++++ .github/workflows/release-brand-matrix.yml | 150 ++++++++++++++++++++- 2 files changed, 230 insertions(+), 1 deletion(-) diff --git a/.github/scripts/brand-matrix.test.mjs b/.github/scripts/brand-matrix.test.mjs index 9b74dc37..d54de09c 100644 --- a/.github/scripts/brand-matrix.test.mjs +++ b/.github/scripts/brand-matrix.test.mjs @@ -226,3 +226,84 @@ describe('parseBrandBuildMatrix', () => { ); }); }); + +describe('release brand matrix workflow', () => { + it('keeps local runtime validation independent of provider and signing inputs', async () => { + const workflow = await readFile( + new URL('../workflows/release-brand-matrix.yml', import.meta.url), + 'utf8', + ); + const validation = workflow.slice( + workflow.indexOf(' credential-free-validation:'), + workflow.indexOf(' release-environment-preflight:'), + ); + + expect(validation).toContain('needs: prepare'); + expect(validation).toContain('xvfb-run -a pnpm -F @linkcode/desktop e2e:config-canary'); + expect(validation).toContain('pnpm -F @linkcode/mobile smoke:export'); + expect(validation).toContain('expo prebuild --clean --no-install --platform android'); + expect(validation).toContain('expo prebuild --clean --no-install --platform ios'); + expect(validation).toContain('"local-static-origin"'); + expect(validation).toContain('providerDeploymentId:null'); + expect(validation).not.toContain('environment: release'); + expect(validation).not.toContain('secrets.'); + expect(validation).not.toContain('release-environment-preflight'); + }); + + it('fails closed unless the live-pilot environment is protected', async () => { + const workflow = await readFile( + new URL('../workflows/release-brand-matrix.yml', import.meta.url), + 'utf8', + ); + const preflight = workflow.slice( + workflow.indexOf(' release-environment-preflight:'), + workflow.indexOf(' render-inputs:'), + ); + + expect(preflight).toContain('environment: release'); + expect(preflight).toContain('protection_rules'); + expect(preflight).toContain('required_reviewers'); + expect(preflight).toContain('deployment_branch_policy'); + expect(preflight).toContain('gh api "repos/$GITHUB_REPOSITORY/environments/release"'); + expect(preflight).toContain('inputs.sign'); + expect(preflight).not.toContain('inputs.build'); + const renderInputs = workflow.slice( + workflow.indexOf(' render-inputs:'), + workflow.indexOf(' signing-inputs:'), + ); + expect(renderInputs).toContain('needs: prepare'); + expect(renderInputs).not.toContain('release-environment-preflight'); + const signingInputs = workflow.slice( + workflow.indexOf(' signing-inputs:'), + workflow.indexOf(' render:'), + ); + expect(signingInputs).toContain('needs: [prepare, release-environment-preflight]'); + }); + + it('binds credential-free desktop recovery evidence to immutable release inputs', async () => { + const workflow = await readFile( + new URL('../workflows/release-brand-matrix.yml', import.meta.url), + 'utf8', + ); + const desktopValidation = workflow.slice( + workflow.indexOf(' desktop-validation:'), + workflow.indexOf(' mobile-validation:'), + ); + + expect(desktopValidation).toContain('inputs.build && !inputs.sign'); + expect(desktopValidation).toContain('xvfb-run -a pnpm -F @linkcode/desktop e2e:config-canary'); + expect(desktopValidation).toContain( + '54ce1fc855e12295a8dd1490463c9afac8e84a526f1e16340bcefe4f0fec8e39', + ); + expect(desktopValidation).toContain('"normal":["1","2","3","4"]'); + expect(desktopValidation).toContain('"emergency":["1","2","3"]'); + expect(desktopValidation).toContain('"kind":"local-static-origin"'); + expect(desktopValidation).toContain('"providerDeploymentId":null'); + expect(desktopValidation).toContain('--expected-delivery-sha256'); + expect(desktopValidation).toContain( + '--release-manifest release-inputs/release-manifest.desktop.json', + ); + expect(desktopValidation).toContain('--out release-provenance.desktop.json'); + expect(desktopValidation).not.toContain('environment: release'); + }); +}); diff --git a/.github/workflows/release-brand-matrix.yml b/.github/workflows/release-brand-matrix.yml index 18bb0c5e..208f4d6a 100644 --- a/.github/workflows/release-brand-matrix.yml +++ b/.github/workflows/release-brand-matrix.yml @@ -39,6 +39,7 @@ concurrency: permissions: contents: read + deployments: read jobs: prepare: @@ -126,6 +127,84 @@ jobs: --sign "${{ inputs.sign }}" \ --upload "${{ inputs.upload }}" + credential-free-validation: + name: Credential-free runtime validation + needs: prepare + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ inputs.ref }} + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + cache: true + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version-file: .nvmrc + package-manager-cache: false + - run: pnpm install --frozen-lockfile + - name: Exercise Electron recovery with Xvfb + run: | + set -euo pipefail + pnpm -F @linkcode/desktop exec playwright-core install-deps chromium + printf '%s %s\n' \ + '54ce1fc855e12295a8dd1490463c9afac8e84a526f1e16340bcefe4f0fec8e39' \ + 'apps/desktop/e2e/fixtures/pilot-e2e-v1.json' | sha256sum --check --strict + xvfb-run -a pnpm -F @linkcode/desktop e2e:config-canary + - name: Exercise production Hermes and native generation + env: + CI: "1" + EXPO_NO_TELEMETRY: "1" + run: | + set -euo pipefail + pnpm -F @linkcode/mobile smoke:export + pnpm --dir apps/mobile exec expo prebuild --clean --no-install --platform android + test -f apps/mobile/android/app/build.gradle + rm -rf apps/mobile/android + pnpm --dir apps/mobile exec expo prebuild --clean --no-install --platform ios + test -f apps/mobile/ios/Podfile + rm -rf apps/mobile/ios + - name: Record local-only evidence + env: + CLIENT_REF: ${{ inputs.ref }} + DELIVERY_SHA256: ${{ needs.prepare.outputs.delivery_descriptor_sha256 }} + run: | + mkdir -p credential-free-evidence + jq -cn \ + --arg clientGitSha "$CLIENT_REF" \ + --arg deliveryDescriptorSha256 "$DELIVERY_SHA256" \ + '{clientGitSha:$clientGitSha,deliveryDescriptorSha256:$deliveryDescriptorSha256,deploymentIdentity:{kind:"local-static-origin",providerDeploymentId:null},evidenceVersion:1,pilotFixtureSha256:"54ce1fc855e12295a8dd1490463c9afac8e84a526f1e16340bcefe4f0fec8e39",runtimes:["electron+xvfb","production-hermes+android-prebuild","production-hermes+ios-prebuild"]}' \ + > credential-free-evidence/runtime-validation.json + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: credential-free-runtime-validation + path: credential-free-evidence + if-no-files-found: error + retention-days: 7 + + release-environment-preflight: + name: Protected live-pilot preflight + if: ${{ inputs.sign }} + needs: prepare + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} + environment: release + steps: + - name: Require protected nonproduction release environment + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + environment="$RUNNER_TEMP/release-environment.json" + gh api "repos/$GITHUB_REPOSITORY/environments/release" > "$environment" + if ! jq -e ' + ([.protection_rules[]?.type] | index("required_reviewers") != null) and + (.deployment_branch_policy != null) + ' "$environment" >/dev/null; then + echo "::error::release must require reviewers and a deployment branch policy before a live pilot" + exit 1 + fi + render-inputs: name: Validate immutable render inputs if: ${{ inputs.build }} @@ -146,7 +225,7 @@ jobs: signing-inputs: name: Validate signing inputs if: ${{ inputs.sign }} - needs: prepare + needs: [prepare, release-environment-preflight] runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} environment: release env: @@ -317,6 +396,75 @@ jobs: rendered_artifact: brand-render-${{ matrix.brandId }} update_url: ${{ matrix.distribution.desktop.updateUrl || '' }} + desktop-validation: + name: Desktop validation ${{ matrix.brandId }} + if: ${{ inputs.build && !inputs.sign && !cancelled() && needs.render.result == 'success' }} + needs: [prepare, render] + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ inputs.ref }} + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + cache: true + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version-file: .nvmrc + package-manager-cache: false + - run: pnpm install --frozen-lockfile + - name: Install Electron system dependencies + run: pnpm -F @linkcode/desktop exec playwright-core install-deps chromium + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: brand-render-${{ matrix.brandId }} + path: . + - name: Verify real Electron configuration recovery + run: | + set -euo pipefail + printf '%s %s\n' \ + '54ce1fc855e12295a8dd1490463c9afac8e84a526f1e16340bcefe4f0fec8e39' \ + 'apps/desktop/e2e/fixtures/pilot-e2e-v1.json' | sha256sum --check --strict + rendered="$RUNNER_TEMP/rendered-desktop-${{ matrix.brandId }}" + mv apps/desktop/generated "$rendered" + status=0 + xvfb-run -a pnpm -F @linkcode/desktop e2e:config-canary || status=$? + if [ -e apps/desktop/generated ]; then + echo "::error::the fixture build created release-rendered output" + exit 1 + fi + mv "$rendered" apps/desktop/generated + exit "$status" + - name: Record isolated validation evidence + run: | + set -euo pipefail + root="release-validation/${{ matrix.brandId }}" + mkdir -p "$root" + printf '%s\n' '{"activationVersions":{"emergency":["1","2","3"],"normal":["1","2","3","4"]},"consumer":"desktop","deploymentIdentity":{"kind":"local-static-origin","providerDeploymentId":null},"drills":["emergency-kill-switch","emergency-release","offline-startup","pointer-and-snapshot-tamper","replay-rejection","rollback","roll-forward"],"evidenceVersion":1,"pilotFixtureSha256":"54ce1fc855e12295a8dd1490463c9afac8e84a526f1e16340bcefe4f0fec8e39","runtime":"electron+xvfb"}' \ + > "$root/validation.desktop.json" + pnpm exec tsx packages/foundation/common/src/node/release-artifact-cli.mts \ + --artifact-root "$root" \ + --artifact validation.desktop.json \ + --bundle apps/desktop/generated/config-build-bundle.json \ + --brand-identity apps/desktop/generated/brand-identity.json \ + --brand-manifest release-inputs/brands.manifest.yaml \ + --client-git-sha '${{ inputs.ref }}' \ + --delivery-descriptor release-inputs/brand-build-matrix.json \ + --expected-delivery-sha256 '${{ needs.prepare.outputs.delivery_descriptor_sha256 }}' \ + --release-manifest release-inputs/release-manifest.desktop.json \ + --compliance release-inputs/compliance.desktop.json \ + --out release-provenance.desktop.json + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: brand-validation-desktop-${{ matrix.brandId }} + path: release-validation/${{ matrix.brandId }} + if-no-files-found: error + retention-days: 7 + mobile-validation: name: Mobile validation ${{ matrix.brandId }} if: ${{ inputs.build && !inputs.sign && !cancelled() && needs.render.result == 'success' }} From a7f15dfc8a31c836f1165b00cead57759879a0e5 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Sun, 9 Aug 2026 19:39:14 +0000 Subject: [PATCH 2/3] fix(release): harden pilot matrix gates Amp-Thread-ID: https://ampcode.com/threads/T-019fe7ac-7d82-754e-a203-5d0214817d24 --- .../brand-matrices/code-561-pilot.json | 203 ++++++++++++++++++ .github/scripts/brand-matrix.test.mjs | 77 ++++++- .github/workflows/build-desktop.yml | 9 +- .github/workflows/build-mobile.yml | 11 +- .github/workflows/release-brand-matrix.yml | 72 ++++--- 5 files changed, 335 insertions(+), 37 deletions(-) create mode 100644 .github/release/brand-matrices/code-561-pilot.json diff --git a/.github/release/brand-matrices/code-561-pilot.json b/.github/release/brand-matrices/code-561-pilot.json new file mode 100644 index 00000000..ddc0f41a --- /dev/null +++ b/.github/release/brand-matrices/code-561-pilot.json @@ -0,0 +1,203 @@ +{ + "brandBuildMatrixVersion": 1, + "brands": [ + { + "brandId": "acme", + "channel": "canary", + "compliance": { + "android": { + "checklist": { + "configurableFeaturesDisclosed": true, + "dataPracticesReviewed": true, + "noExecutableCode": true, + "permissionsReviewed": true, + "storeMetadataReviewed": true + }, + "disclosedFeatures": [ + "feature.aiAssist", + "feature.newEditor", + "modules.messaging.enabled", + "modules.terminal.enabled", + "modules.workspace.enabled" + ] + }, + "desktop": { + "checklist": { + "configurableFeaturesDisclosed": true, + "dataPracticesReviewed": true, + "noExecutableCode": true, + "permissionsReviewed": true, + "storeMetadataReviewed": true + }, + "disclosedFeatures": [ + "feature.aiAssist", + "feature.newEditor", + "modules.messaging.enabled", + "modules.terminal.enabled", + "modules.workspace.enabled" + ] + }, + "ios": { + "checklist": { + "configurableFeaturesDisclosed": true, + "dataPracticesReviewed": true, + "noExecutableCode": true, + "permissionsReviewed": true, + "storeMetadataReviewed": true + }, + "disclosedFeatures": [ + "feature.aiAssist", + "feature.newEditor", + "modules.messaging.enabled", + "modules.terminal.enabled", + "modules.workspace.enabled" + ] + } + }, + "distribution": { + "desktop": null, + "mobile": null + }, + "releaseManifests": { + "android": { + "brandId": "acme", + "channel": "canary", + "configRevisionId": "code-561-operational", + "expectedSnapshotSha256": "0675b1b33e81d4898f75233fdf9bda7243348e286ebfd3b06f807d82fff8818f", + "platform": "android", + "publicKeyringsSha256": "1a674a4c47d1ef57e51f7e50e8f044f32cffa0450574f12f23ddeb5cb619d445", + "publisherGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "releaseManifestFormatVersion": 1, + "revisionSha256": "e8389e2edc8273c5ec1b029c7101ae18af6e5b55218f0c8fd2dfe640cf695c5b", + "sourceGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "telemetryEndpoint": "https://acme.example.invalid/telemetry" + }, + "desktop": { + "brandId": "acme", + "channel": "canary", + "configRevisionId": "code-561-operational", + "expectedSnapshotSha256": "936250a3ef922cede3a200b5dc401cc7697ee1db90dc3efd0f873358524f01e3", + "platform": "desktop", + "publicKeyringsSha256": "1a674a4c47d1ef57e51f7e50e8f044f32cffa0450574f12f23ddeb5cb619d445", + "publisherGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "releaseManifestFormatVersion": 1, + "revisionSha256": "e8389e2edc8273c5ec1b029c7101ae18af6e5b55218f0c8fd2dfe640cf695c5b", + "sourceGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "telemetryEndpoint": "https://acme.example.invalid/telemetry" + }, + "ios": { + "brandId": "acme", + "channel": "canary", + "configRevisionId": "code-561-operational", + "expectedSnapshotSha256": "a689a8d95f74d9cb00b5d9850af3ecfd50edb23d2496c71805c9ffe4659d56ae", + "platform": "ios", + "publicKeyringsSha256": "1a674a4c47d1ef57e51f7e50e8f044f32cffa0450574f12f23ddeb5cb619d445", + "publisherGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "releaseManifestFormatVersion": 1, + "revisionSha256": "e8389e2edc8273c5ec1b029c7101ae18af6e5b55218f0c8fd2dfe640cf695c5b", + "sourceGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "telemetryEndpoint": "https://acme.example.invalid/telemetry" + } + } + }, + { + "brandId": "zenith", + "channel": "canary", + "compliance": { + "android": { + "checklist": { + "configurableFeaturesDisclosed": true, + "dataPracticesReviewed": true, + "noExecutableCode": true, + "permissionsReviewed": true, + "storeMetadataReviewed": true + }, + "disclosedFeatures": [ + "feature.aiAssist", + "feature.newEditor", + "modules.messaging.enabled", + "modules.terminal.enabled", + "modules.workspace.enabled" + ] + }, + "desktop": { + "checklist": { + "configurableFeaturesDisclosed": true, + "dataPracticesReviewed": true, + "noExecutableCode": true, + "permissionsReviewed": true, + "storeMetadataReviewed": true + }, + "disclosedFeatures": [ + "feature.aiAssist", + "feature.newEditor", + "modules.messaging.enabled", + "modules.terminal.enabled", + "modules.workspace.enabled" + ] + }, + "ios": { + "checklist": { + "configurableFeaturesDisclosed": true, + "dataPracticesReviewed": true, + "noExecutableCode": true, + "permissionsReviewed": true, + "storeMetadataReviewed": true + }, + "disclosedFeatures": [ + "feature.aiAssist", + "feature.newEditor", + "modules.messaging.enabled", + "modules.terminal.enabled", + "modules.workspace.enabled" + ] + } + }, + "distribution": { + "desktop": null, + "mobile": null + }, + "releaseManifests": { + "android": { + "brandId": "zenith", + "channel": "canary", + "configRevisionId": "code-561-operational", + "expectedSnapshotSha256": "a0ef5196645ae3b857343784f7a5ab5d6f5184b15c7cb646d8e86c93ff5384b0", + "platform": "android", + "publicKeyringsSha256": "1a674a4c47d1ef57e51f7e50e8f044f32cffa0450574f12f23ddeb5cb619d445", + "publisherGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "releaseManifestFormatVersion": 1, + "revisionSha256": "e8389e2edc8273c5ec1b029c7101ae18af6e5b55218f0c8fd2dfe640cf695c5b", + "sourceGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "telemetryEndpoint": "https://zenith.example.invalid/telemetry" + }, + "desktop": { + "brandId": "zenith", + "channel": "canary", + "configRevisionId": "code-561-operational", + "expectedSnapshotSha256": "99a93cec0ca5381faa15a5def6727736f220b5d7d111e1fce04afda1d321aef2", + "platform": "desktop", + "publicKeyringsSha256": "1a674a4c47d1ef57e51f7e50e8f044f32cffa0450574f12f23ddeb5cb619d445", + "publisherGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "releaseManifestFormatVersion": 1, + "revisionSha256": "e8389e2edc8273c5ec1b029c7101ae18af6e5b55218f0c8fd2dfe640cf695c5b", + "sourceGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "telemetryEndpoint": "https://zenith.example.invalid/telemetry" + }, + "ios": { + "brandId": "zenith", + "channel": "canary", + "configRevisionId": "code-561-operational", + "expectedSnapshotSha256": "e1b93b64973e0192ed2e1d8ba9a4cca27ae2bb5521ef6011392c2d86b510b95b", + "platform": "ios", + "publicKeyringsSha256": "1a674a4c47d1ef57e51f7e50e8f044f32cffa0450574f12f23ddeb5cb619d445", + "publisherGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "releaseManifestFormatVersion": 1, + "revisionSha256": "e8389e2edc8273c5ec1b029c7101ae18af6e5b55218f0c8fd2dfe640cf695c5b", + "sourceGitSha": "e4a0624abbc8ed1cac4948fa90239176a83cb96e", + "telemetryEndpoint": "https://zenith.example.invalid/telemetry" + } + } + } + ] +} diff --git a/.github/scripts/brand-matrix.test.mjs b/.github/scripts/brand-matrix.test.mjs index d54de09c..6fbc4603 100644 --- a/.github/scripts/brand-matrix.test.mjs +++ b/.github/scripts/brand-matrix.test.mjs @@ -19,6 +19,7 @@ const RE_DIVERGENT_SOURCE = /all platforms must share sourceGitSha/; const RE_SHARED_DESTINATION = /R2 prefixes in one bucket must not overlap/; const RE_SHARED_CREDENTIALS = /credentialSecretPrefix: must be unique/; const RE_SHARED_APP_STORE_APP = /ios\.ascAppId: must be unique/; +const ACTIONS_EXPRESSION = String.fromCodePoint(36); function sha(character) { return character.repeat(64); @@ -81,6 +82,31 @@ function matrix(...brands) { } describe('parseBrandBuildMatrix', () => { + it('pins the CODE-561 credential-free pilot to two brands and all platforms', async () => { + const pilot = JSON.parse( + await readFile( + new URL('../release/brand-matrices/code-561-pilot.json', import.meta.url), + 'utf8', + ), + ); + const plan = buildMatrixPlan(pilot); + + expect(plan.targets.include.map(({ brandId, platform }) => `${brandId}/${platform}`)).toEqual([ + 'acme/desktop', + 'acme/ios', + 'acme/android', + 'zenith/desktop', + 'zenith/ios', + 'zenith/android', + ]); + expect( + new Set(pilot.brands.map((entry) => entry.releaseManifests.desktop.publisherGitSha)), + ).toEqual(new Set(['e4a0624abbc8ed1cac4948fa90239176a83cb96e'])); + expect( + pilot.brands.every((entry) => Object.values(entry.distribution).every((x) => x === null)), + ).toBe(true); + }); + it('builds the complete brand by platform plan', () => { const input = matrix(brand('acme'), brand('zenith')); const plan = buildMatrixPlan(input); @@ -239,10 +265,19 @@ describe('release brand matrix workflow', () => { ); expect(validation).toContain('needs: prepare'); + expect(validation).toContain( + `matrix: ${ACTIONS_EXPRESSION}{{ fromJSON(needs.prepare.outputs.targets) }}`, + ); expect(validation).toContain('xvfb-run -a pnpm -F @linkcode/desktop e2e:config-canary'); expect(validation).toContain('pnpm -F @linkcode/mobile smoke:export'); - expect(validation).toContain('expo prebuild --clean --no-install --platform android'); - expect(validation).toContain('expo prebuild --clean --no-install --platform ios'); + expect(validation).toContain( + `expo prebuild --clean --no-install --platform '${ACTIONS_EXPRESSION}{{ matrix.platform }}'`, + ); + expect(validation).toContain("matrix.platform == 'desktop'"); + expect(validation).toContain("matrix.platform != 'desktop'"); + expect(validation).toContain( + `credential-free-${ACTIONS_EXPRESSION}{{ matrix.brandId }}-${ACTIONS_EXPRESSION}{{ matrix.platform }}`, + ); expect(validation).toContain('"local-static-origin"'); expect(validation).toContain('providerDeploymentId:null'); expect(validation).not.toContain('environment: release'); @@ -260,24 +295,50 @@ describe('release brand matrix workflow', () => { workflow.indexOf(' render-inputs:'), ); - expect(preflight).toContain('environment: release'); + expect(preflight).not.toContain('environment:'); expect(preflight).toContain('protection_rules'); expect(preflight).toContain('required_reviewers'); expect(preflight).toContain('deployment_branch_policy'); - expect(preflight).toContain('gh api "repos/$GITHUB_REPOSITORY/environments/release"'); - expect(preflight).toContain('inputs.sign'); - expect(preflight).not.toContain('inputs.build'); + expect(preflight).toContain( + 'gh api "repos/$GITHUB_REPOSITORY/environments/pilot-nonproduction"', + ); + expect(preflight).toContain('secrets.PILOT_ENVIRONMENT_ADMIN_TOKEN'); + expect(preflight).toContain('inputs.build'); const renderInputs = workflow.slice( workflow.indexOf(' render-inputs:'), workflow.indexOf(' signing-inputs:'), ); - expect(renderInputs).toContain('needs: prepare'); - expect(renderInputs).not.toContain('release-environment-preflight'); + expect(renderInputs).toContain('needs: [prepare, release-environment-preflight]'); + expect(renderInputs).toContain('environment: pilot-nonproduction'); const signingInputs = workflow.slice( workflow.indexOf(' signing-inputs:'), workflow.indexOf(' render:'), ); expect(signingInputs).toContain('needs: [prepare, release-environment-preflight]'); + expect(signingInputs).toContain('environment: pilot-nonproduction'); + expect(workflow).not.toContain('environment: release'); + expect(workflow.split('release_environment: pilot-nonproduction')).toHaveLength(3); + }); + + it('passes the isolated pilot environment through reusable signing workflows', async () => { + const [desktop, mobile] = await Promise.all([ + readFile(new URL('../workflows/build-desktop.yml', import.meta.url), 'utf8'), + readFile(new URL('../workflows/build-mobile.yml', import.meta.url), 'utf8'), + ]); + + expect(desktop).toContain('release_environment:'); + expect(desktop).toContain( + `environment: ${ACTIONS_EXPRESSION}{{ inputs.release_environment || 'release' }}`, + ); + expect(desktop).toContain( + `environment: ${ACTIONS_EXPRESSION}{{ inputs.sign && (inputs.release_environment || 'release') || '' }}`, + ); + expect(mobile).toContain('release_environment:'); + expect( + mobile.split( + `environment: ${ACTIONS_EXPRESSION}{{ inputs.release_environment || 'release' }}`, + ), + ).toHaveLength(4); }); it('binds credential-free desktop recovery evidence to immutable release inputs', async () => { diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 1ecbe8fb..cdce70f7 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -40,6 +40,11 @@ on: type: string required: false default: "" + release_environment: + description: GitHub environment that owns signing and publisher credentials + type: string + required: false + default: release # CI builds on PRs — unsigned. # pull_request: # paths: @@ -87,7 +92,7 @@ jobs: name: Render immutable config if: ${{ inputs.sign && inputs.rendered_artifact == '' }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release + environment: ${{ inputs.release_environment || 'release' }} steps: - name: Checkout uses: actions/checkout@v7 @@ -132,7 +137,7 @@ jobs: if: ${{ !cancelled() && (needs.render-config.result == 'success' || needs.render-config.result == 'skipped') }} runs-on: ${{ matrix.os }} # Signing is gated by the `release` environment (secrets + tag policy); '' = no environment. - environment: ${{ inputs.sign && 'release' || '' }} + environment: ${{ inputs.sign && (inputs.release_environment || 'release') || '' }} strategy: fail-fast: false # Runners resolve through repo/org `vars` (ArcBox pins Blacksmith labels there); forks diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index dbf97624..654d9302 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -31,6 +31,11 @@ on: type: boolean required: false default: false + release_environment: + description: GitHub environment that owns build, signing, and store credentials + type: string + required: false + default: release workflow_dispatch: inputs: submit: @@ -79,7 +84,7 @@ jobs: if: ${{ inputs.rendered_artifact == '' }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} timeout-minutes: 20 - environment: release + environment: ${{ inputs.release_environment || 'release' }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -117,7 +122,7 @@ jobs: if: ${{ !cancelled() && needs.preflight.result == 'success' && (needs.render-config.result == 'success' || needs.render-config.result == 'skipped') }} runs-on: ${{ matrix.os }} timeout-minutes: 120 - environment: release + environment: ${{ inputs.release_environment || 'release' }} strategy: fail-fast: false matrix: @@ -245,7 +250,7 @@ jobs: needs: build runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} timeout-minutes: 30 - environment: release + environment: ${{ inputs.release_environment || 'release' }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/release-brand-matrix.yml b/.github/workflows/release-brand-matrix.yml index 208f4d6a..99724028 100644 --- a/.github/workflows/release-brand-matrix.yml +++ b/.github/workflows/release-brand-matrix.yml @@ -128,9 +128,12 @@ jobs: --upload "${{ inputs.upload }}" credential-free-validation: - name: Credential-free runtime validation + name: Credential-free ${{ matrix.brandId }}/${{ matrix.platform }} needs: prepare runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.prepare.outputs.targets) }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -145,6 +148,7 @@ jobs: package-manager-cache: false - run: pnpm install --frozen-lockfile - name: Exercise Electron recovery with Xvfb + if: ${{ matrix.platform == 'desktop' }} run: | set -euo pipefail pnpm -F @linkcode/desktop exec playwright-core install-deps chromium @@ -153,64 +157,82 @@ jobs: 'apps/desktop/e2e/fixtures/pilot-e2e-v1.json' | sha256sum --check --strict xvfb-run -a pnpm -F @linkcode/desktop e2e:config-canary - name: Exercise production Hermes and native generation + if: ${{ matrix.platform != 'desktop' }} env: CI: "1" EXPO_NO_TELEMETRY: "1" run: | set -euo pipefail pnpm -F @linkcode/mobile smoke:export - pnpm --dir apps/mobile exec expo prebuild --clean --no-install --platform android - test -f apps/mobile/android/app/build.gradle - rm -rf apps/mobile/android - pnpm --dir apps/mobile exec expo prebuild --clean --no-install --platform ios - test -f apps/mobile/ios/Podfile - rm -rf apps/mobile/ios + pnpm --dir apps/mobile exec expo prebuild --clean --no-install --platform '${{ matrix.platform }}' + if [ '${{ matrix.platform }}' = android ]; then + test -f apps/mobile/android/app/build.gradle + rm -rf apps/mobile/android + else + test -f apps/mobile/ios/Podfile + rm -rf apps/mobile/ios + fi - name: Record local-only evidence env: + BRAND_ID: ${{ matrix.brandId }} CLIENT_REF: ${{ inputs.ref }} DELIVERY_SHA256: ${{ needs.prepare.outputs.delivery_descriptor_sha256 }} + PLATFORM: ${{ matrix.platform }} run: | mkdir -p credential-free-evidence + if [ "$PLATFORM" = desktop ]; then + runtime=electron+xvfb + else + runtime=production-hermes+prebuild + fi jq -cn \ + --arg brandId "$BRAND_ID" \ --arg clientGitSha "$CLIENT_REF" \ --arg deliveryDescriptorSha256 "$DELIVERY_SHA256" \ - '{clientGitSha:$clientGitSha,deliveryDescriptorSha256:$deliveryDescriptorSha256,deploymentIdentity:{kind:"local-static-origin",providerDeploymentId:null},evidenceVersion:1,pilotFixtureSha256:"54ce1fc855e12295a8dd1490463c9afac8e84a526f1e16340bcefe4f0fec8e39",runtimes:["electron+xvfb","production-hermes+android-prebuild","production-hermes+ios-prebuild"]}' \ + --arg platform "$PLATFORM" \ + --arg runtime "$runtime" \ + '{brandId:$brandId,clientGitSha:$clientGitSha,deliveryDescriptorSha256:$deliveryDescriptorSha256,deploymentIdentity:{kind:"local-static-origin",providerDeploymentId:null},evidenceVersion:1,pilotFixtureSha256:"54ce1fc855e12295a8dd1490463c9afac8e84a526f1e16340bcefe4f0fec8e39",platform:$platform,runtime:$runtime}' \ > credential-free-evidence/runtime-validation.json - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: credential-free-runtime-validation + name: credential-free-${{ matrix.brandId }}-${{ matrix.platform }} path: credential-free-evidence if-no-files-found: error retention-days: 7 release-environment-preflight: name: Protected live-pilot preflight - if: ${{ inputs.sign }} + if: ${{ inputs.build }} needs: prepare runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release steps: - name: Require protected nonproduction release environment env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.PILOT_ENVIRONMENT_ADMIN_TOKEN }} run: | set -euo pipefail - environment="$RUNNER_TEMP/release-environment.json" - gh api "repos/$GITHUB_REPOSITORY/environments/release" > "$environment" + if [ -z "$GH_TOKEN" ]; then + echo "::error::PILOT_ENVIRONMENT_ADMIN_TOKEN is required to inspect environment protection" + exit 1 + fi + environment="$RUNNER_TEMP/pilot-environment.json" + gh api "repos/$GITHUB_REPOSITORY/environments/pilot-nonproduction" > "$environment" if ! jq -e ' - ([.protection_rules[]?.type] | index("required_reviewers") != null) and - (.deployment_branch_policy != null) + .name == "pilot-nonproduction" and + ([.protection_rules[]? | select(.type == "required_reviewers") | .reviewers | length] | any(. > 0)) and + (.deployment_branch_policy != null) and + (.deployment_branch_policy.protected_branches == true or .deployment_branch_policy.custom_branch_policies == true) ' "$environment" >/dev/null; then - echo "::error::release must require reviewers and a deployment branch policy before a live pilot" + echo "::error::pilot-nonproduction must require reviewers and a deployment branch policy before a live pilot" exit 1 fi render-inputs: name: Validate immutable render inputs if: ${{ inputs.build }} - needs: prepare + needs: [prepare, release-environment-preflight] runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release + environment: pilot-nonproduction env: CONFIG_PUBLISHER_REPO: ${{ vars.CONFIG_PUBLISHER_REPO }} CONFIG_PUBLISHER_TOKEN: ${{ secrets.CONFIG_PUBLISHER_TOKEN }} @@ -227,7 +249,7 @@ jobs: if: ${{ inputs.sign }} needs: [prepare, release-environment-preflight] runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release + environment: pilot-nonproduction env: APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} @@ -264,7 +286,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release + environment: pilot-nonproduction steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -393,6 +415,7 @@ jobs: sign: ${{ inputs.sign }} brand_id: ${{ matrix.brandId }} delivery_descriptor_sha256: ${{ needs.prepare.outputs.delivery_descriptor_sha256 }} + release_environment: pilot-nonproduction rendered_artifact: brand-render-${{ matrix.brandId }} update_url: ${{ matrix.distribution.desktop.updateUrl || '' }} @@ -531,6 +554,7 @@ jobs: ref: ${{ inputs.ref }} brand_id: ${{ matrix.brandId }} delivery_descriptor_sha256: ${{ needs.prepare.outputs.delivery_descriptor_sha256 }} + release_environment: pilot-nonproduction rendered_artifact: brand-render-${{ matrix.brandId }} submit: false @@ -542,7 +566,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release + environment: pilot-nonproduction env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} R2_ACCESS_KEY_ID: ${{ secrets[format('{0}_R2_ACCESS_KEY_ID', matrix.distribution.desktop.credentialSecretPrefix)] }} @@ -622,7 +646,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release + environment: pilot-nonproduction steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -698,7 +722,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: release + environment: pilot-nonproduction env: AWS_ACCESS_KEY_ID: ${{ secrets[format('{0}_R2_ACCESS_KEY_ID', matrix.distribution.desktop.credentialSecretPrefix)] }} AWS_SECRET_ACCESS_KEY: ${{ secrets[format('{0}_R2_SECRET_ACCESS_KEY', matrix.distribution.desktop.credentialSecretPrefix)] }} From e37d4a1edaaa87715f41f6834b25b13f326d8f56 Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Sun, 9 Aug 2026 21:04:30 +0000 Subject: [PATCH 3/3] fix(release): use protected release environment Amp-Thread-ID: https://ampcode.com/threads/T-019fe7ac-7d82-754e-a203-5d0214817d24 --- .github/scripts/brand-matrix.test.mjs | 18 ++++++------ .github/workflows/release-brand-matrix.yml | 32 +++++++++++----------- docs/ENVIRONMENT.md | 1 + 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/.github/scripts/brand-matrix.test.mjs b/.github/scripts/brand-matrix.test.mjs index 6fbc4603..e5a554e2 100644 --- a/.github/scripts/brand-matrix.test.mjs +++ b/.github/scripts/brand-matrix.test.mjs @@ -285,7 +285,7 @@ describe('release brand matrix workflow', () => { expect(validation).not.toContain('release-environment-preflight'); }); - it('fails closed unless the live-pilot environment is protected', async () => { + it('fails closed unless the release environment is protected', async () => { const workflow = await readFile( new URL('../workflows/release-brand-matrix.yml', import.meta.url), 'utf8', @@ -299,28 +299,26 @@ describe('release brand matrix workflow', () => { expect(preflight).toContain('protection_rules'); expect(preflight).toContain('required_reviewers'); expect(preflight).toContain('deployment_branch_policy'); - expect(preflight).toContain( - 'gh api "repos/$GITHUB_REPOSITORY/environments/pilot-nonproduction"', - ); - expect(preflight).toContain('secrets.PILOT_ENVIRONMENT_ADMIN_TOKEN'); + expect(preflight).toContain('gh api "repos/$GITHUB_REPOSITORY/environments/release"'); + expect(preflight).toContain('secrets.RELEASE_ENVIRONMENT_ADMIN_TOKEN'); expect(preflight).toContain('inputs.build'); const renderInputs = workflow.slice( workflow.indexOf(' render-inputs:'), workflow.indexOf(' signing-inputs:'), ); expect(renderInputs).toContain('needs: [prepare, release-environment-preflight]'); - expect(renderInputs).toContain('environment: pilot-nonproduction'); + expect(renderInputs).toContain('environment: release'); const signingInputs = workflow.slice( workflow.indexOf(' signing-inputs:'), workflow.indexOf(' render:'), ); expect(signingInputs).toContain('needs: [prepare, release-environment-preflight]'); - expect(signingInputs).toContain('environment: pilot-nonproduction'); - expect(workflow).not.toContain('environment: release'); - expect(workflow.split('release_environment: pilot-nonproduction')).toHaveLength(3); + expect(signingInputs).toContain('environment: release'); + expect(workflow.split(' environment: release')).toHaveLength(7); + expect(workflow.split('release_environment: release')).toHaveLength(3); }); - it('passes the isolated pilot environment through reusable signing workflows', async () => { + it('passes the release environment through reusable signing workflows', async () => { const [desktop, mobile] = await Promise.all([ readFile(new URL('../workflows/build-desktop.yml', import.meta.url), 'utf8'), readFile(new URL('../workflows/build-mobile.yml', import.meta.url), 'utf8'), diff --git a/.github/workflows/release-brand-matrix.yml b/.github/workflows/release-brand-matrix.yml index 99724028..82c9f56a 100644 --- a/.github/workflows/release-brand-matrix.yml +++ b/.github/workflows/release-brand-matrix.yml @@ -201,29 +201,29 @@ jobs: retention-days: 7 release-environment-preflight: - name: Protected live-pilot preflight + name: Protected release environment preflight if: ${{ inputs.build }} needs: prepare runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} steps: - - name: Require protected nonproduction release environment + - name: Require protected release environment env: - GH_TOKEN: ${{ secrets.PILOT_ENVIRONMENT_ADMIN_TOKEN }} + GH_TOKEN: ${{ secrets.RELEASE_ENVIRONMENT_ADMIN_TOKEN }} run: | set -euo pipefail if [ -z "$GH_TOKEN" ]; then - echo "::error::PILOT_ENVIRONMENT_ADMIN_TOKEN is required to inspect environment protection" + echo "::error::RELEASE_ENVIRONMENT_ADMIN_TOKEN is required to inspect environment protection" exit 1 fi - environment="$RUNNER_TEMP/pilot-environment.json" - gh api "repos/$GITHUB_REPOSITORY/environments/pilot-nonproduction" > "$environment" + environment="$RUNNER_TEMP/release-environment.json" + gh api "repos/$GITHUB_REPOSITORY/environments/release" > "$environment" if ! jq -e ' - .name == "pilot-nonproduction" and + .name == "release" and ([.protection_rules[]? | select(.type == "required_reviewers") | .reviewers | length] | any(. > 0)) and (.deployment_branch_policy != null) and (.deployment_branch_policy.protected_branches == true or .deployment_branch_policy.custom_branch_policies == true) ' "$environment" >/dev/null; then - echo "::error::pilot-nonproduction must require reviewers and a deployment branch policy before a live pilot" + echo "::error::release must require reviewers and a deployment branch policy before release work" exit 1 fi @@ -232,7 +232,7 @@ jobs: if: ${{ inputs.build }} needs: [prepare, release-environment-preflight] runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: pilot-nonproduction + environment: release env: CONFIG_PUBLISHER_REPO: ${{ vars.CONFIG_PUBLISHER_REPO }} CONFIG_PUBLISHER_TOKEN: ${{ secrets.CONFIG_PUBLISHER_TOKEN }} @@ -249,7 +249,7 @@ jobs: if: ${{ inputs.sign }} needs: [prepare, release-environment-preflight] runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: pilot-nonproduction + environment: release env: APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} @@ -286,7 +286,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: pilot-nonproduction + environment: release steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -415,7 +415,7 @@ jobs: sign: ${{ inputs.sign }} brand_id: ${{ matrix.brandId }} delivery_descriptor_sha256: ${{ needs.prepare.outputs.delivery_descriptor_sha256 }} - release_environment: pilot-nonproduction + release_environment: release rendered_artifact: brand-render-${{ matrix.brandId }} update_url: ${{ matrix.distribution.desktop.updateUrl || '' }} @@ -554,7 +554,7 @@ jobs: ref: ${{ inputs.ref }} brand_id: ${{ matrix.brandId }} delivery_descriptor_sha256: ${{ needs.prepare.outputs.delivery_descriptor_sha256 }} - release_environment: pilot-nonproduction + release_environment: release rendered_artifact: brand-render-${{ matrix.brandId }} submit: false @@ -566,7 +566,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: pilot-nonproduction + environment: release env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} R2_ACCESS_KEY_ID: ${{ secrets[format('{0}_R2_ACCESS_KEY_ID', matrix.distribution.desktop.credentialSecretPrefix)] }} @@ -646,7 +646,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: pilot-nonproduction + environment: release steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -722,7 +722,7 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.prepare.outputs.brands) }} runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} - environment: pilot-nonproduction + environment: release env: AWS_ACCESS_KEY_ID: ${{ secrets[format('{0}_R2_ACCESS_KEY_ID', matrix.distribution.desktop.credentialSecretPrefix)] }} AWS_SECRET_ACCESS_KEY: ${{ secrets[format('{0}_R2_SECRET_ACCESS_KEY', matrix.distribution.desktop.credentialSecretPrefix)] }} diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 18b34708..a8e8c287 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -129,6 +129,7 @@ Set as GitHub repository/environment secrets, never locally. Signing and notariz | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID` | `build-desktop.yml` | `azure/login` **inputs** for OIDC federation. No `AZURE_*` credential env exists during packaging on purpose, so `DefaultAzureCredential` falls through to the Azure CLI entry. | | `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY` | `release-desktop.yml` | Cloudflare R2 credentials for publishing the electron-updater feed. `AWS_REQUEST_CHECKSUM_CALCULATION`/`AWS_RESPONSE_CHECKSUM_VALIDATION` are pinned to `WHEN_REQUIRED` because R2 doesn't implement the checksums recent aws-cli sends. | | `CONFIG_PUBLISHER_TOKEN` | release workflows | Fine-grained token with Contents read-only access to `CONFIG_PUBLISHER_REPO`; used only to fetch exact commits pinned by release manifests. | +| `RELEASE_ENVIRONMENT_ADMIN_TOKEN` | `release-brand-matrix.yml` | Repository/org-scoped token authorized to inspect the `release` environment configuration. The preflight runs before entering that environment and fails unless it has required reviewers and a non-null deployment branch policy, so this token cannot be stored only inside `release`. | | `_R2_ACCOUNT_ID`, `_R2_ACCESS_KEY_ID`, `_R2_SECRET_ACCESS_KEY` | `release-brand-matrix.yml` | Per-brand R2 account and S3 credentials. `` is the validated `credentialSecretPrefix` in that brand's matrix row. Scope each key pair to only that row's bucket/prefix with object read/write/list; never share one prefix between brands. | | `BOT_APP_ID`, `BOT_APP_PRIVATE_KEY` | `release-please.yml`, `finalize-releases.yml`, `release-desktop.yml` | Repository/org-scoped GitHub App credentials. The App needs Contents, Issues, and Pull requests read/write on this repo so release-please can maintain PRs, draft Releases, and tags; the release environment also uses it for the Homebrew cask bump and the WinGet bump (install the App on `arcboxlabs/homebrew-tap` and on the `arcboxlabs/winget-pkgs` fork with contents + pull-requests write). Missing credentials fail release automation before any tag is created; only the package-manager bumps remain an optional self-skip. |