diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f6ff40..f7eb556 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: required: true type: string publish: - description: Publish and tag after every check passes + description: Publish and tag after direct package checks pass required: true default: false type: boolean @@ -20,8 +20,7 @@ concurrency: jobs: candidate: runs-on: ubuntu-latest - timeout-minutes: 180 - environment: release + timeout-minutes: 45 permissions: contents: read outputs: @@ -55,29 +54,13 @@ jobs: - name: Configure external release storage run: echo "BRAID_RELEASE_ARTIFACT_ROOT=$RUNNER_TEMP/braid-release" >> "$GITHUB_ENV" - - name: Restore resumable release state - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 - with: - path: ${{ runner.temp }}/braid-release - key: braid-release-${{ inputs.commit }}-${{ github.run_id }}-${{ github.run_attempt }} - restore-keys: | - braid-release-${{ inputs.commit }}- - - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Install pinned visual tools - run: | - sudo apt-get update - sudo apt-get install --yes fonts-dejavu-core imagemagick - AGG_URL="https://github.com/asciinema/agg/releases/download/v1.9.0/agg-x86_64-unknown-linux-gnu" - AGG_SHA256="f111e315cd71056b116302342553dd765b7297579ed511f111d0cedb442aeda6" - curl --fail --location --silent --show-error "$AGG_URL" --output "$RUNNER_TEMP/agg" - echo "$AGG_SHA256 $RUNNER_TEMP/agg" | sha256sum --check --strict - sudo install -m 0755 "$RUNNER_TEMP/agg" /usr/local/bin/agg - test "$(agg --version | head -1)" = "agg 1.9.0" - - - name: Build and exercise one immutable candidate + - name: Run source checks once + run: pnpm check + + - name: Build and use one immutable candidate run: pnpm release:prepare - name: Read candidate metadata @@ -89,68 +72,12 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT" - - name: Run the complete release checks - env: - BRAID_LIVE_BRIDGE: '1' - BRAID_CLI_BRIDGE_BEARER: ${{ secrets.BRAID_CLI_BRIDGE_BEARER }} - BRAID_CLI_BRIDGE_URL: ${{ vars.BRAID_CLI_BRIDGE_URL }} - BRAID_EVAL_API_KEY: ${{ secrets.BRAID_EVAL_API_KEY }} - BRAID_EVAL_BASE_URL: ${{ vars.BRAID_EVAL_BASE_URL }} - BRAID_EVAL_MODEL: ${{ vars.BRAID_EVAL_MODEL }} - BRAID_TANGLE_API_KEY: ${{ secrets.BRAID_TANGLE_API_KEY }} - BRAID_TANGLE_ENDPOINT: ${{ vars.BRAID_TANGLE_ENDPOINT }} - BRAID_TANGLE_MODEL: ${{ vars.BRAID_TANGLE_MODEL }} - BRAID_TANGLE_PROVIDER: ${{ vars.BRAID_TANGLE_PROVIDER }} - BRAID_TANGLE_RUNNER: ${{ vars.BRAID_TANGLE_RUNNER }} - BRAID_TANGLE_SANDBOX_API_KEY: ${{ secrets.BRAID_TANGLE_SANDBOX_API_KEY }} - BRAID_TANGLE_SANDBOX_ENDPOINT: ${{ vars.BRAID_TANGLE_SANDBOX_ENDPOINT }} - BRAID_TANGLE_SANDBOX_MODEL: ${{ vars.BRAID_TANGLE_SANDBOX_MODEL }} - BRAID_TANGLE_SANDBOX_PROVIDER: ${{ vars.BRAID_TANGLE_SANDBOX_PROVIDER }} - BRAID_TANGLE_SANDBOX_RUNNER: ${{ vars.BRAID_TANGLE_SANDBOX_RUNNER }} - BRAID_ANALYSIS_API_KEY: ${{ secrets.BRAID_ANALYSIS_API_KEY }} - BRAID_ANALYSIS_ENDPOINT: ${{ vars.BRAID_ANALYSIS_ENDPOINT }} - BRAID_ANALYSIS_MODEL: ${{ vars.BRAID_ANALYSIS_MODEL }} - BRAID_ANALYSIS_PROVIDER: ${{ vars.BRAID_ANALYSIS_PROVIDER }} - BRAID_ANALYSIS_RUNNER: ${{ vars.BRAID_ANALYSIS_RUNNER }} - BRAID_SUPERVISOR_ID: ${{ vars.BRAID_SUPERVISOR_ID }} - BRAID_SUPERVISOR_MESSAGE: ${{ vars.BRAID_SUPERVISOR_MESSAGE }} - BRAID_SUPERVISOR_ROOT: ${{ vars.BRAID_SUPERVISOR_ROOT }} - BRAID_SUPERVISOR_WORKER: ${{ vars.BRAID_SUPERVISOR_WORKER }} - BRAID_RELEASE_CHECKOUT: ${{ github.workspace }} - run: >- - pnpm release:collect -- - --artifact-root "$BRAID_RELEASE_ARTIFACT_ROOT" - --tarball "candidate/${{ steps.metadata.outputs.archive }}" - - - name: Preserve resumable release state - if: always() - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 - with: - path: ${{ runner.temp }}/braid-release - key: braid-release-${{ inputs.commit }}-${{ github.run_id }}-${{ github.run_attempt }} - - - name: Upload partial release state - if: always() + - name: Upload the exact candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: braid-release-partial-${{ inputs.commit }}-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/braid-release - if-no-files-found: warn - retention-days: 90 - - - name: Qualify the pre-publication candidate - env: - BRAID_RELEASE_CHECKOUT: ${{ github.workspace }} - BRAID_RELEASE_ISOLATED_CHECKOUT: '1' - run: pnpm verify:candidate - - - name: Upload the qualified candidate evidence - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: braid-release-qualified-${{ inputs.commit }} + name: braid-release-candidate-${{ inputs.commit }} path: ${{ runner.temp }}/braid-release if-no-files-found: error - overwrite: true retention-days: 90 endorse-candidate: @@ -163,7 +90,7 @@ jobs: steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: braid-release-qualified-${{ inputs.commit }} + name: braid-release-candidate-${{ inputs.commit }} path: ${{ runner.temp }}/braid-release - name: Endorse the fixed candidate archive @@ -244,7 +171,7 @@ jobs: name: braid-release-${{ inputs.commit }} path: ${{ runner.temp }}/braid-release - - name: Install and exercise the exact candidate + - name: Install and use the exact candidate env: BRAID_EXPECT_ARCHITECTURE: ${{ matrix.architecture }} BRAID_EXPECT_PLATFORM: ${{ matrix.platform }} @@ -311,15 +238,10 @@ jobs: - name: Publish the approved tarball env: ARCHIVE: ${{ needs.candidate.outputs.archive }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} VERSION: ${{ needs.candidate.outputs.version }} run: | npm install --global npm@11.18.0 test "$(npm --version)" = 11.18.0 - if [ -n "$NODE_AUTH_TOKEN" ]; then - printf '//registry.npmjs.org/:_authToken=%s\n' "$NODE_AUTH_TOKEN" > "$RUNNER_TEMP/braid-npmrc" - export NPM_CONFIG_USERCONFIG="$RUNNER_TEMP/braid-npmrc" - fi if npm view "@tangle-network/braid@$VERSION" version >/dev/null 2>&1; then echo "@tangle-network/braid@$VERSION already exists; checking it instead of republishing" else @@ -381,7 +303,7 @@ jobs: name: braid-release-${{ inputs.commit }} path: ${{ runner.temp }}/braid-release - - name: Download and exercise the registry package + - name: Download and use the registry package env: BRAID_EXPECT_ARCHITECTURE: ${{ matrix.architecture }} BRAID_EXPECT_PLATFORM: ${{ matrix.platform }} @@ -410,15 +332,9 @@ jobs: with: ref: ${{ inputs.commit }} - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.19.0 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: @@ -442,16 +358,12 @@ jobs: name: braid-npm-provenance-${{ inputs.commit }} path: ${{ runner.temp }}/braid-release/publication - - name: Assemble the final report + - name: Validate candidate and registry use env: BRAID_RELEASE_ARTIFACT_ROOT: ${{ runner.temp }}/braid-release - BRAID_RELEASE_CHECKOUT: ${{ github.workspace }} - BRAID_RELEASE_ISOLATED_CHECKOUT: '1' - run: | - pnpm release:record-publication - pnpm verify:release + run: node scripts/release/record-publication.mjs - - name: Upload the validated final release evidence + - name: Upload the validated release bundle uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: braid-release-final-qualified-${{ inputs.commit }} @@ -556,23 +468,23 @@ jobs: INDEX_SHA256="$(sha256sum "$ENDORSEMENT/files.sha256" | cut -d ' ' -f 1)" grep -Fx "index_sha256=$INDEX_SHA256" "$ENDORSEMENT/statement.txt" - - name: Tag the proven commit and attach its report + - name: Tag the shipped commit and attach its artifacts env: GH_TOKEN: ${{ github.token }} VERSION: ${{ needs.candidate.outputs.version }} run: | TAG="v$VERSION" + NOTES="Published @tangle-network/braid@$VERSION from commit ${{ inputs.commit }}. The exact package completed candidate and registry install/use checks on Linux x64, macOS arm64, and Windows x64." if git rev-parse "$TAG" >/dev/null 2>&1; then test "$(git rev-list -n 1 "$TAG")" = "${{ inputs.commit }}" else git tag "$TAG" "${{ inputs.commit }}" git push origin "$TAG" fi - REPORT="$RUNNER_TEMP/braid-release/$VERSION/report.md" if gh release view "$TAG" >/dev/null 2>&1; then - gh release edit "$TAG" --notes-file "$REPORT" + gh release edit "$TAG" --notes "$NOTES" else - gh release create "$TAG" --verify-tag --title "Braid $VERSION" --notes-file "$REPORT" + gh release create "$TAG" --verify-tag --title "Braid $VERSION" --notes "$NOTES" fi EVIDENCE_ARCHIVE="$RUNNER_TEMP/braid-evidence-$VERSION.tar.gz" tar --directory "$RUNNER_TEMP/braid-release" --create --gzip --file "$EVIDENCE_ARCHIVE" . diff --git a/docs/08-verification.md b/docs/08-verification.md index a6b79a5..2894f25 100644 --- a/docs/08-verification.md +++ b/docs/08-verification.md @@ -520,23 +520,35 @@ Each row has a forced-process-kill test at every durable boundary. Each case runs on at least three representative source fixtures and includes a seeded poor output plus a trivial raw-data baseline. -## Release evidence manifest +## Comprehensive audit manifest -The release process requires `BRAID_RELEASE_ARTIFACT_ROOT` to name a real directory outside the clean source checkout. +The comprehensive audit is available when a product decision needs every requirement record in one manifest. -Candidate creation writes one npm tarball, its complete package-file manifest, check streams, terminal captures, resumable collection state, and `release/checks.json` below that external directory. +It is not an npm publication prerequisite. + +The audit requires `BRAID_RELEASE_ARTIFACT_ROOT` to name a real directory outside the clean source checkout. + +Audit candidate creation writes one npm tarball, its package-file manifest, check streams, terminal captures, resumable state, and `release/checks.json` below that directory. The collector executes 25 distinct prerequisite commands and materializes 43 exact `UP-*`, `LIVE-*`, `PERF-*`, `EVAL-*`, and `VR-03` records from their matching command outputs, for 65 check records covering all 154 requirement identifiers. -`verify:release` is the final assembler and never appears as one of its own prerequisite checks. +`verify:release` assembles this optional audit and never appears as one of its own prerequisite checks. + +Publication uses a smaller direct acceptance path. + +The candidate job runs `pnpm check` once, then `pnpm release:prepare` builds and uses one immutable package. -Pre-publication validation checks the candidate before the release key exists in any job that executes package code. +A code-free job endorses that exact package before publication. After npm publication, the same clean-install, plain-flow, encrypted-storage, digest, architecture, and cleanup smoke runs for the candidate and registry package on Linux x64, macOS arm64, and Windows x64. -The final process validates those six records, adds their immutable JSON artifacts to `VR-10`, then writes `/manifest.json` and `/report.md` below the external artifact directory. +The publication process validates those six records, the package SHA-256, and npm provenance. -The report counts passed, failed, unavailable, uncaptured, and unrecognized check results separately. +It writes `publication/proof.json` below the external artifact directory. + +An isolated code-free job endorses the resulting release bundle before tagging the commit. + +When the comprehensive audit runs, its report counts passed, failed, unavailable, uncaptured, and unrecognized check results separately. Each report row includes its exact result. @@ -585,7 +597,9 @@ The manifest contains the following top-level data. Each check records identifier, category, required status, command, working directory, environment identifier, start and end, exit code, attempt count, measured fields, result, stdout and stderr artifact hashes, and failure details. -The complete archive, including every check field and output digest, is covered by the endorsement file index. +An audit archive includes every check field and output digest. + +The publication endorsement covers the exact package, package manifest, six platform-use records, and npm provenance. The publication and tag jobs accept only the public key pinned in `release/endorsement-public-key.pem` and recompute the complete index before accepting its signature. @@ -646,7 +660,7 @@ Implementation must provide the following stable scripts. | `capture` | `pnpm test:capture` | Deterministic baseline real-binary captures | | `visual` | `pnpm capture:visual` | Deterministic real-binary state captures and manifests | | `release` | `pnpm check:release` | Release manifest and evidence-set check | -| `verify:release` | `pnpm verify:release` | Validate publication proof and assemble every required result before isolated archive endorsement | +| `verify:release` | `pnpm verify:release` | Assemble the optional comprehensive audit from collected result records | The deterministic local commands are implemented in this repository. @@ -668,15 +682,15 @@ When cancellation is unavailable, the live driver checks the rejected control ag Tangle, supervisor, and live-analysis commands return a typed unavailable result until protected credentials, deployments, and evidence stores are supplied. -The release workflow uses `pnpm release:prepare`, `pnpm release:collect`, and `pnpm verify:candidate` before publication. +The release workflow runs `pnpm check` once and uses `pnpm release:prepare` before publication. -The candidate job supplies Bridge, eval, Tangle, analysis, and supervisor settings only to the complete release-check step. +It does not require provider, sandbox, supervisor, or analysis credentials. -Inference, sandbox, and analysis use separate GitHub environment secrets. +Those settings remain inputs to explicit live audits. -Endpoints, models, providers, runners, and supervisor identifiers remain environment variables. +After publication, `release:record-publication` validates direct candidate and registry use plus npm provenance. -After publication it uses `pnpm release:record-publication` and `pnpm verify:release`; these workflow commands are not additional check records. +The comprehensive collector and verifier remain separate commands for explicit audits. ## Verification acceptance diff --git a/docs/09-delivery-plan.md b/docs/09-delivery-plan.md index 67f74ff..1f477dc 100644 --- a/docs/09-delivery-plan.md +++ b/docs/09-delivery-plan.md @@ -388,7 +388,7 @@ An analysis cannot pass if its source branch journal changes before explicit pro No critical or high security finding, inaccessible primary action, or missed performance target remains open. -## W13: Live proof, package release, and post-publication verification +## W13: Direct product use, package release, and registry verification ### Repositories @@ -398,21 +398,29 @@ All owning repositories for final compatible releases, then `tangle-network/brai - Verify every upstream package from packed release candidates and publish in dependency order. - Install the resulting registry packages into Braid and commit the exact lockfile. -- Run static, unit, property soak, contract, headless, virtual-terminal, PTY, storage, security, performance, live, semantic, install, and visual checks against one Braid release candidate. -- Collect and validate candidate evidence with every required identifier and artifact hash before a code-free job endorses the complete archive. -- Confirm all live test resources are cleaned up and every external state change is recorded. -- Obtain independent architecture, security, and product-flow review against the exact release candidate digest. +- Run `pnpm check` once against the exact main commit. +- Build one immutable package with `pnpm release:prepare` and use its installed CLI, RPC, and terminal flows. +- Keep live, performance, semantic, soak, and complete-manifest audits available as explicit product audits. +- Do not make provider or sandbox availability an npm publication prerequisite. +- Endorse the exact candidate package and package manifest in an isolated code-free job. - Publish `@tangle-network/braid` with npm provenance. - Download the registry package in clean supported environments and repeat the post-publication smoke. - Require matching candidate and registry package digests plus successful plain messaging, encrypted storage, and temporary-state cleanup on Linux x64, macOS arm64, and Windows x64. -- Add the six platform result artifacts to `VR-10`, assemble the final manifest and readable report, then endorse the complete fixed archive. -- Tag the source commit and attach the readable verification report, checksums, licenses, screenshots, and flow recording. +- Validate the six platform results and npm provenance in one publication record. +- Endorse that fixed release bundle in an isolated code-free job. +- Tag the source commit and attach the package, publication records, screenshots, and flow recording. ### Done when -`PR-12`, `AR-08`, `AR-09`, `UP-11`, `VR-01` through `VR-10`, and every remaining requirement identifier pass against the immutable release build. +The exact main commit passes `pnpm check` and `pnpm release:prepare`. -The registry package integrity must match the approved candidate and post-publication checks must pass before the release is announced complete. +All three candidate checks and all three registry checks must pass. + +The registry package SHA-256 must match the approved candidate. + +The npm provenance must bind the package to the exact commit and release workflow. + +Comprehensive product audits report their own status without blocking an otherwise usable package release. ## Requirement ownership @@ -425,7 +433,7 @@ The registry package integrity must match the approved candidate and post-public | `PR-07`–`PR-08` | W11 | Real analysis and runtime control | | `PR-09` | W5 | Forced restart and journal checksum | | `PR-10`–`PR-11` | W6 and W12 | PTY recording and headless equivalence | -| `PR-12` | W13 | Complete release manifest | +| `PR-12` | W13 | Exact package plus candidate and registry use records | | `UX-01`–`UX-10` | W6 and W12 | Virtual terminal, PTY, captures, and keyboard walkthrough | | `AR-01`–`AR-10` | W0, W5, W6, and W13 | Static boundaries, property tests, package install, and dependency inventory | | `UP-01`–`UP-14` | W1 through W4 | Owning-repository contract, package, and live artifacts | @@ -438,10 +446,10 @@ The registry package integrity must match the approved candidate and post-public | `LIVE-01`–`LIVE-12` | W3, W4, W8, W9, W10, and W11 | Real provider and runtime artifacts | | `PERF-01`–`PERF-10` | W12 | Full measured distributions | | `EVAL-01`–`EVAL-06` | W11 | Calibrated `agent-eval` records | -| `VR-01`–`VR-10` | W13 | Signed exact-build release manifest | +| `VR-01`–`VR-10` | W13 | Explicit signed exact-build audit manifest | | `US-01`–`US-10` | W0, W6, W12, and W13 | Dependency inventory, attribution, boundary tests, upgrade evidence, packed installation, and live analysis | -The release verifier extracts required identifiers from the committed specification and fails if the manifest omits one. +The comprehensive audit verifier extracts required identifiers from the committed specification and fails if the audit manifest omits one. ## Upstream pull-request and release order @@ -452,7 +460,8 @@ The release verifier extracts required identifiers from the committed specificat 5. Update Braid to registry packages, never workspace links, and run contract plus one live smoke before broader release testing. 6. Finish Braid feature pull requests with required terminal stills and recordings. 7. Freeze the Braid release candidate only after all feature changes merge. -8. Run W13 against the frozen digest and publish only that digest. +8. Run `pnpm check`, build and use the packed candidate, then publish only that digest. +9. Install and use the registry package on all supported platforms before tagging the release. Every upstream behavior change updates its owning documentation in the same pull request. diff --git a/scripts/release/smoke-package.mjs b/scripts/release/smoke-package.mjs index bbf1cba..f73c0be 100644 --- a/scripts/release/smoke-package.mjs +++ b/scripts/release/smoke-package.mjs @@ -56,6 +56,68 @@ async function run(file, args, options = {}) { }) } +async function runPlainFlow(binary, cwd) { + const expected = 'Fixture response through pi: platform package smoke' + return await new Promise((resolvePromise, reject) => { + const child = spawn(process.execPath, [binary, '--fixture', 'deterministic', '--plain'], { + cwd, + env: process.env, + shell: false, + stdio: ['pipe', 'pipe', 'pipe'], + }) + let stdout = '' + let stderr = '' + let quitSent = false + let outputExceeded = false + let inputError + const timer = setTimeout(() => child.kill('SIGKILL'), 30_000) + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', (chunk) => { + stdout += chunk + if (stdout.length > 2 * 1024 * 1024) { + outputExceeded = true + child.kill('SIGKILL') + return + } + if (!quitSent && stdout.includes(expected)) { + quitSent = true + child.stdin.end('/quit\n') + } + }) + child.stderr.on('data', (chunk) => { + stderr += chunk + if (stderr.length > 2 * 1024 * 1024) { + outputExceeded = true + child.kill('SIGKILL') + } + }) + child.stdin.on('error', (error) => { + inputError = error + }) + child.once('error', reject) + child.once('close', (code, signal) => { + clearTimeout(timer) + if ( + code === 0 && + signal === null && + quitSent && + !outputExceeded && + inputError === undefined + ) { + resolvePromise({ stdout, stderr }) + return + } + reject( + new Error( + `Installed plain flow exited with code ${String(code)} and signal ${String(signal)}${inputError === undefined ? '' : ` after input error ${String(inputError)}`}\n${stdout}\n${stderr}`, + ), + ) + }) + child.stdin.write('platform package smoke\n') + }) +} + async function sha256(path) { return createHash('sha256') .update(await readFile(path)) @@ -112,11 +174,7 @@ try { const version = await run(process.execPath, [binary, '--version'], { cwd: smokeRoot }) assert(version.stdout.trim() === proof.version, 'Installed binary version differs') - const plain = await run(process.execPath, [binary, '--fixture', 'deterministic', '--plain'], { - cwd: smokeRoot, - stdin: 'platform package smoke\n/quit\n', - timeoutMs: 30_000, - }) + const plain = await runPlainFlow(binary, smokeRoot) assert( plain.stdout.includes('Fixture response through pi: platform package smoke'), 'Installed plain flow did not complete', diff --git a/test/scripts.test.ts b/test/scripts.test.ts index 23e1967..7a6b4f2 100644 --- a/test/scripts.test.ts +++ b/test/scripts.test.ts @@ -621,32 +621,27 @@ test('protected live and semantic checks stay unavailable instead of becoming lo } }) -test('release keys stay isolated and provider credentials are step-scoped', async () => { +test('release keys stay isolated while publication uses the installed product', async () => { const workflow = await readFile('.github/workflows/release.yml', 'utf8') - const candidate = workflow.slice( - workflow.indexOf(' candidate:'), - workflow.indexOf(' endorse-candidate:'), - ) + const job = (name: string, next: string) => { + const start = workflow.indexOf(`\n ${name}:`) + const end = workflow.indexOf(`\n ${next}:`, start + 1) + assert.notEqual(start, -1, name) + assert.notEqual(end, -1, next) + return workflow.slice(start, end) + } + const candidate = job('candidate', 'endorse-candidate') assert.doesNotMatch(candidate, /BRAID_RELEASE_SIGNING_KEY/u) - const releaseCheckMarker = ' - name: Run the complete release checks' - const releaseCheckStart = candidate.indexOf(releaseCheckMarker) - assert.notEqual(releaseCheckStart, -1) - const releaseCheckEnd = candidate.indexOf( - '\n - name:', - releaseCheckStart + releaseCheckMarker.length, - ) - assert.notEqual(releaseCheckEnd, -1) - const releaseCheckStep = candidate.slice(releaseCheckStart, releaseCheckEnd) - const otherCandidateSteps = `${candidate.slice(0, releaseCheckStart)}${candidate.slice(releaseCheckEnd)}` for (const [start, end] of [ [' endorse-candidate:', ' platform-smoke:'], [' endorse-final:', ' tag-and-report:'], ] as const) { - const job = workflow.slice(workflow.indexOf(start), workflow.indexOf(end)) - assert.match(job, /BRAID_RELEASE_SIGNING_KEY_BASE64/u) - assert.doesNotMatch(job, /actions\/checkout|\b(?:node|npm|pnpm)\b|scripts\//u) + const endorsement = job(start.trim().slice(0, -1), end.trim().slice(0, -1)) + assert.match(endorsement, /BRAID_RELEASE_SIGNING_KEY_BASE64/u) + assert.doesNotMatch(endorsement, /actions\/checkout|\b(?:node|npm|pnpm)\b|scripts\//u) } assert.equal(workflow.match(/BRAID_RELEASE_SIGNING_KEY_BASE64/gu)?.length, 2) + for (const name of [ 'BRAID_CLI_BRIDGE_BEARER', 'BRAID_CLI_BRIDGE_URL', @@ -673,17 +668,26 @@ test('release keys stay isolated and provider credentials are step-scoped', asyn 'BRAID_SUPERVISOR_ROOT', 'BRAID_SUPERVISOR_WORKER', ]) { - const declaration = new RegExp(`^\\s+${name}:`, 'gmu') - assert.equal(releaseCheckStep.match(declaration)?.length, 1, name) - assert.doesNotMatch(otherCandidateSteps, declaration, name) - } - for (const name of [ - 'BRAID_TANGLE_API_KEY', - 'BRAID_TANGLE_SANDBOX_API_KEY', - 'BRAID_ANALYSIS_API_KEY', - ]) { - assert.ok(releaseCheckStep.includes(`${name}: ${'$'}{{ secrets.${name} }}`)) + assert.doesNotMatch(workflow, new RegExp(`\\b${name}\\b`, 'u'), name) } + + assert.doesNotMatch(workflow, /release:collect|verify:candidate|verify:release/u) + assert.match(candidate, /name: Run source checks once[\s\S]*?run: pnpm check/u) + assert.match( + candidate, + /name: Build and use one immutable candidate[\s\S]*?run: pnpm release:prepare/u, + ) + + const candidateSmoke = job('platform-smoke', 'publish') + const registrySmoke = job('post-publish-smoke', 'finalize') + assert.match(candidateSmoke, /name: Install and use the exact candidate/u) + assert.match(candidateSmoke, /node scripts\/release\/smoke-package\.mjs/u) + assert.match(registrySmoke, /name: Download and use the registry package/u) + assert.match(registrySmoke, /node scripts\/release\/smoke-package\.mjs/u) + + const finalize = job('finalize', 'endorse-final') + assert.match(finalize, /name: Validate candidate and registry use/u) + assert.match(finalize, /node scripts\/release\/record-publication\.mjs/u) }) test('release acceptance uses candidate product checks without a manual attestation', async () => {