Publish #32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish | |
| on: | |
| workflow_run: | |
| workflows: | |
| - Release Please | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| inputs: | |
| publish_operation: | |
| description: Exact publication operation. | |
| required: true | |
| type: string | |
| control_commit: | |
| description: Commit containing the dispatched workflow. | |
| required: true | |
| type: string | |
| release_commit: | |
| description: Immutable release commit. | |
| required: true | |
| type: string | |
| release_tag: | |
| description: Immutable release tag. | |
| required: true | |
| type: string | |
| release_run_id: | |
| description: Successful Release Please run ID. | |
| required: true | |
| type: string | |
| release_run_attempt: | |
| description: Successful Release Please run attempt. | |
| required: true | |
| type: string | |
| permissions: | |
| actions: read | |
| contents: read | |
| concurrency: | |
| group: npm-publish | |
| cancel-in-progress: false | |
| jobs: | |
| handoff: | |
| name: Dispatch publication from the immutable release tag | |
| if: >- | |
| vars.RELEASE_PLEASE_ENABLED == 'true' && | |
| github.event_name == 'workflow_run' && | |
| github.run_attempt == 1 && | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'push' && | |
| github.event.workflow_run.head_branch == 'main' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: write | |
| contents: read | |
| steps: | |
| - name: Check out the successful Release Please commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ github.event.workflow_run.head_sha }} | |
| - name: Set up Node.js 24 | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24.x | |
| cache: npm | |
| - name: Classify the exact Release Please handoff | |
| id: result | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_ARTIFACTS: ${{ runner.temp }}/release-please-artifacts.json | |
| RELEASE_JOBS: ${{ runner.temp }}/release-please-jobs.json | |
| RELEASE_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} | |
| RELEASE_RUN_ID: ${{ github.event.workflow_run.id }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${RELEASE_RUN_ID}/artifacts?per_page=100" \ | |
| > "$RELEASE_ARTIFACTS" | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${RELEASE_RUN_ID}/attempts/${RELEASE_RUN_ATTEMPT}/jobs?per_page=100" \ | |
| > "$RELEASE_JOBS" | |
| node --input-type=module <<'EOF' | |
| import { appendFileSync, readFileSync } from "node:fs"; | |
| import { classifyReleasePleaseHandoff } from "./scripts/release-workflow-validation.mjs"; | |
| const result = classifyReleasePleaseHandoff({ | |
| artifacts: JSON.parse( | |
| readFileSync(process.env.RELEASE_ARTIFACTS, "utf8"), | |
| ).artifacts, | |
| jobs: JSON.parse(readFileSync(process.env.RELEASE_JOBS, "utf8")).jobs, | |
| runAttempt: Number(process.env.RELEASE_RUN_ATTEMPT), | |
| runId: Number(process.env.RELEASE_RUN_ID), | |
| }); | |
| appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| `has-result=${String(result.hasResult)}\n`, | |
| ); | |
| EOF | |
| - name: Install validation dependencies without lifecycle scripts | |
| if: steps.result.outputs.has-result == 'true' | |
| run: npm ci --ignore-scripts | |
| - name: Download the exact Release Please result | |
| if: steps.result.outputs.has-result == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-please-result-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }} | |
| path: ${{ runner.temp }}/release-please-result | |
| github-token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| run-id: ${{ github.event.workflow_run.id }} | |
| - name: Validate the exact release and tag dispatch contract | |
| id: release | |
| if: steps.result.outputs.has-result == 'true' | |
| env: | |
| EXPECTED_REPOSITORY: cometapi-dev/cometapi-node | |
| EXPECTED_WORKFLOW: Release Please | |
| EXPECTED_WORKFLOW_PATH: .github/workflows/release-please.yml | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_RESULT: ${{ runner.temp }}/release-please-result/result.json | |
| TAGGED_PUBLISH_WORKFLOW: ${{ runner.temp }}/tagged-publish.yml | |
| WORKFLOW_SHA: ${{ github.event.workflow_run.head_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "$(git rev-parse HEAD)" != "$WORKFLOW_SHA" ]]; then | |
| echo "The handoff checkout does not match the successful Release Please SHA." >&2 | |
| exit 1 | |
| fi | |
| git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main | |
| if [[ "$(git rev-parse refs/remotes/origin/main)" != "$WORKFLOW_SHA" ]]; then | |
| echo "The release commit is no longer the exact origin/main tip." >&2 | |
| exit 1 | |
| fi | |
| node --input-type=module <<'EOF' | |
| import { appendFileSync, readFileSync } from "node:fs"; | |
| import { | |
| validateReleasePleaseActionResult, | |
| validateReleaseWorkflowRun, | |
| } from "./scripts/release-workflow-validation.mjs"; | |
| const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); | |
| const run = validateReleaseWorkflowRun(event, { | |
| checkedOutSha: process.env.WORKFLOW_SHA, | |
| repository: process.env.EXPECTED_REPOSITORY, | |
| workflowName: process.env.EXPECTED_WORKFLOW, | |
| workflowPath: process.env.EXPECTED_WORKFLOW_PATH, | |
| }); | |
| const manifest = JSON.parse(readFileSync("package.json", "utf8")); | |
| const release = validateReleasePleaseActionResult( | |
| JSON.parse(readFileSync(process.env.RELEASE_RESULT, "utf8")), | |
| { | |
| releaseCommit: run.releaseCommit, | |
| repository: process.env.EXPECTED_REPOSITORY, | |
| runAttempt: run.runAttempt, | |
| runId: run.runId, | |
| version: manifest.version, | |
| workflowName: process.env.EXPECTED_WORKFLOW, | |
| workflowPath: process.env.EXPECTED_WORKFLOW_PATH, | |
| }, | |
| ); | |
| appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| [ | |
| `release-commit=${release.releaseCommit}`, | |
| `release-run-attempt=${run.runAttempt}`, | |
| `release-run-id=${run.runId}`, | |
| `release-tag=${release.tag}`, | |
| `release-url=${release.htmlUrl}`, | |
| "", | |
| ].join("\n"), | |
| ); | |
| EOF | |
| release_tag="$(sed -n 's/^release-tag=//p' "$GITHUB_OUTPUT")" | |
| release_url="$(sed -n 's/^release-url=//p' "$GITHUB_OUTPUT")" | |
| if [[ -z "$release_tag" || -z "$release_url" ]]; then | |
| echo "The exact release handoff outputs are missing." >&2 | |
| exit 1 | |
| fi | |
| release_json="$RUNNER_TEMP/handoff-release.json" | |
| gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${release_tag}" > "$release_json" | |
| git fetch --no-tags origin "+refs/tags/${release_tag}:refs/tags/${release_tag}" | |
| tag_commit="$(git rev-parse --verify "refs/tags/${release_tag}^{commit}")" | |
| git show "refs/tags/${release_tag}:.github/workflows/publish.yml" \ | |
| > "$TAGGED_PUBLISH_WORKFLOW" | |
| RELEASE_JSON="$release_json" RELEASE_TAG="$release_tag" \ | |
| RELEASE_URL="$release_url" TAG_COMMIT="$tag_commit" \ | |
| node --input-type=module <<'EOF' | |
| import { readFileSync } from "node:fs"; | |
| import { parse } from "yaml"; | |
| import { | |
| extractReleaseNotesFromChangelog, | |
| validateGitHubRelease, | |
| validatePublishWorkflowContract, | |
| } from "./scripts/release-workflow-validation.mjs"; | |
| const version = JSON.parse(readFileSync("package.json", "utf8")).version; | |
| validateGitHubRelease( | |
| JSON.parse(readFileSync(process.env.RELEASE_JSON, "utf8")), | |
| { | |
| expectedBody: extractReleaseNotesFromChangelog( | |
| readFileSync("CHANGELOG.md", "utf8"), | |
| version, | |
| ), | |
| htmlUrl: process.env.RELEASE_URL, | |
| releaseCommit: process.env.WORKFLOW_SHA, | |
| tag: process.env.RELEASE_TAG, | |
| tagCommit: process.env.TAG_COMMIT, | |
| }, | |
| ); | |
| validatePublishWorkflowContract( | |
| parse(readFileSync(process.env.TAGGED_PUBLISH_WORKFLOW, "utf8")), | |
| ); | |
| EOF | |
| - name: Dispatch the exact immutable tag | |
| if: steps.result.outputs.has-result == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_COMMIT: ${{ steps.release.outputs.release-commit }} | |
| RELEASE_RUN_ATTEMPT: ${{ steps.release.outputs.release-run-attempt }} | |
| RELEASE_RUN_ID: ${{ steps.release.outputs.release-run-id }} | |
| RELEASE_TAG: ${{ steps.release.outputs.release-tag }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main | |
| if [[ "$(git rev-parse refs/remotes/origin/main)" != "$RELEASE_COMMIT" ]]; then | |
| echo "origin/main moved away from the release commit before handoff." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" != "$RELEASE_COMMIT" ]]; then | |
| echo "The immutable release tag changed before handoff." >&2 | |
| exit 1 | |
| fi | |
| before_runs="$RUNNER_TEMP/tag-dispatch-runs-before.json" | |
| after_runs="$RUNNER_TEMP/tag-dispatch-runs-after.json" | |
| gh api --paginate --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/runs?event=workflow_dispatch&per_page=100" \ | |
| | jq '[.[].workflow_runs[]]' > "$before_runs" | |
| prior_count="$(jq \ | |
| --arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \ | |
| '[.[] | select(.actor.login == "github-actions[bot]" and | |
| .triggering_actor.login == "github-actions[bot]" and | |
| .event == "workflow_dispatch" and .head_branch == $tag and | |
| .head_sha == $commit)] | length' "$before_runs")" | |
| if [[ "$prior_count" != "0" ]]; then | |
| echo "An exact Publish run already exists for this immutable tag and commit." >&2 | |
| exit 1 | |
| fi | |
| gh api --method POST \ | |
| "repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/dispatches" \ | |
| -f ref="$RELEASE_TAG" \ | |
| -f "inputs[publish_operation]=release" \ | |
| -f "inputs[control_commit]=$RELEASE_COMMIT" \ | |
| -f "inputs[release_commit]=$RELEASE_COMMIT" \ | |
| -f "inputs[release_tag]=$RELEASE_TAG" \ | |
| -f "inputs[release_run_id]=$RELEASE_RUN_ID" \ | |
| -f "inputs[release_run_attempt]=$RELEASE_RUN_ATTEMPT" | |
| publish_run_id="" | |
| for _ in {1..12}; do | |
| gh api --paginate --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/runs?event=workflow_dispatch&per_page=100" \ | |
| | jq '[.[].workflow_runs[]]' > "$after_runs" | |
| candidate_count="$(jq \ | |
| --arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \ | |
| --slurpfile before "$before_runs" \ | |
| '[.[] | select(.id as $id | ($before[0] | map(.id) | index($id) | not)) | | |
| select(.actor.login == "github-actions[bot]" and | |
| .triggering_actor.login == "github-actions[bot]" and | |
| .event == "workflow_dispatch" and .head_branch == $tag and | |
| .head_sha == $commit)] | length' "$after_runs")" | |
| if [[ "$candidate_count" -gt 1 ]]; then | |
| echo "Multiple Publish runs matched the immutable tag handoff." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$candidate_count" == "1" ]]; then | |
| publish_run_id="$(jq -r \ | |
| --arg commit "$RELEASE_COMMIT" --arg tag "$RELEASE_TAG" \ | |
| --slurpfile before "$before_runs" \ | |
| '[.[] | select(.id as $id | ($before[0] | map(.id) | index($id) | not)) | | |
| select(.actor.login == "github-actions[bot]" and | |
| .triggering_actor.login == "github-actions[bot]" and | |
| .event == "workflow_dispatch" and .head_branch == $tag and | |
| .head_sha == $commit)][0].id' "$after_runs")" | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [[ ! "$publish_run_id" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "The immutable tag dispatch did not create one exact Publish run." >&2 | |
| exit 1 | |
| fi | |
| echo "Dispatched exact Publish run ${publish_run_id} from ${RELEASE_TAG}." | |
| verify: | |
| name: Verify the immutable release artifact | |
| if: >- | |
| vars.RELEASE_PLEASE_ENABLED == 'true' && | |
| github.event_name == 'workflow_dispatch' && | |
| github.run_attempt == 1 && | |
| inputs.publish_operation == 'release' && | |
| startsWith(github.ref, 'refs/tags/v0.1.') && | |
| github.ref == format('refs/tags/{0}', inputs.release_tag) && | |
| github.sha == inputs.release_commit && | |
| github.workflow_sha == inputs.control_commit && | |
| inputs.control_commit == inputs.release_commit | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| env: | |
| SOURCE_RELEASE_COMMIT: ${{ inputs.release_commit }} | |
| SOURCE_RELEASE_RUN_ATTEMPT: ${{ inputs.release_run_attempt }} | |
| SOURCE_RELEASE_RUN_ID: ${{ inputs.release_run_id }} | |
| outputs: | |
| artifact-name: ${{ steps.artifact-name.outputs.name }} | |
| control-commit: ${{ inputs.control_commit }} | |
| dist-tag: ${{ steps.version.outputs.dist-tag }} | |
| expected-next-version: ${{ steps.registry-baseline.outputs.next-version }} | |
| release-commit: ${{ steps.trust.outputs.release-commit }} | |
| release-tag: ${{ steps.trust.outputs.release-tag }} | |
| release-please-snapshot: ${{ steps.release-please-snapshot.outputs.digest }} | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Reject a repeated verification attempt | |
| env: | |
| WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" ]]; then | |
| echo "Exact-artifact verification is restricted to the initial run attempt." >&2 | |
| exit 1 | |
| fi | |
| - name: Check out the workflow control commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ github.workflow_sha }} | |
| - name: Validate the exact workflow dispatch | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| CONTROL_COMMIT: ${{ github.workflow_sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_REF: ${{ github.ref }} | |
| EVENT_SHA: ${{ github.sha }} | |
| OPERATION: ${{ inputs.publish_operation }} | |
| RELEASE_COMMIT: ${{ inputs.release_commit }} | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| RELEASE_RUN_ATTEMPT: ${{ inputs.release_run_attempt }} | |
| RELEASE_RUN_ID: ${{ inputs.release_run_id }} | |
| TRIGGERING_ACTOR: ${{ github.triggering_actor }} | |
| WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "$(git rev-parse HEAD)" != "$CONTROL_COMMIT" ]]; then | |
| echo "The tag dispatch checkout does not match the triggering SHA." >&2 | |
| exit 1 | |
| fi | |
| git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main | |
| if [[ "$(git rev-parse refs/remotes/origin/main)" != "$RELEASE_COMMIT" ]]; then | |
| echo "The tag release commit is no longer the exact origin/main tip." >&2 | |
| exit 1 | |
| fi | |
| node --input-type=module <<'EOF' | |
| import { validatePublishWorkflowDispatchTrigger } from "./scripts/release-workflow-validation.mjs"; | |
| const dispatchIdentity = { | |
| actor: process.env.ACTOR, | |
| controlCommit: process.env.CONTROL_COMMIT, | |
| eventName: process.env.EVENT_NAME, | |
| eventRef: process.env.EVENT_REF, | |
| eventSha: process.env.EVENT_SHA, | |
| operation: process.env.OPERATION, | |
| releaseCommit: process.env.RELEASE_COMMIT, | |
| releaseTag: process.env.RELEASE_TAG, | |
| sourceReleaseCommit: process.env.SOURCE_RELEASE_COMMIT, | |
| sourceRunAttempt: Number(process.env.SOURCE_RELEASE_RUN_ATTEMPT), | |
| sourceRunId: Number(process.env.SOURCE_RELEASE_RUN_ID), | |
| triggeringActor: process.env.TRIGGERING_ACTOR, | |
| workflowRunAttempt: Number(process.env.WORKFLOW_RUN_ATTEMPT), | |
| workflowSha: process.env.WORKFLOW_SHA, | |
| }; | |
| validatePublishWorkflowDispatchTrigger(dispatchIdentity); | |
| EOF | |
| - name: Freeze the Release Please run set | |
| id: release-please-snapshot | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_PLEASE_RUNS: ${{ runner.temp }}/release-please-runs.json | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| gh api --paginate --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/workflows/release-please.yml/runs?per_page=100" \ | |
| | jq '[.[].workflow_runs[]]' > "$RELEASE_PLEASE_RUNS" | |
| node --input-type=module <<'EOF' | |
| import { createHash } from "node:crypto"; | |
| import { appendFileSync, readFileSync } from "node:fs"; | |
| import { snapshotReleasePleaseRuns } from "./scripts/release-workflow-validation.mjs"; | |
| const snapshot = snapshotReleasePleaseRuns( | |
| JSON.parse(readFileSync(process.env.RELEASE_PLEASE_RUNS, "utf8")), | |
| ); | |
| const digest = createHash("sha256").update(snapshot).digest("hex"); | |
| appendFileSync(process.env.GITHUB_OUTPUT, `digest=${digest}\n`); | |
| EOF | |
| - name: Check out the exact release commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ env.SOURCE_RELEASE_COMMIT }} | |
| - name: Read the exact Release Please source run | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| SOURCE_RUN_FILE: ${{ runner.temp }}/release-please-source-run.json | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RELEASE_RUN_ID}" \ | |
| > "$SOURCE_RUN_FILE" | |
| - name: Download the exact Release Please result | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: release-please-result-${{ env.SOURCE_RELEASE_RUN_ID }}-${{ env.SOURCE_RELEASE_RUN_ATTEMPT }} | |
| path: ${{ runner.temp }}/release-please-result | |
| github-token: ${{ github.token }} | |
| repository: ${{ github.repository }} | |
| run-id: ${{ env.SOURCE_RELEASE_RUN_ID }} | |
| - name: Reject an untrusted Release Please workflow run | |
| id: trust | |
| env: | |
| EXPECTED_BUGS_URL: https://github.com/cometapi-dev/cometapi-node/issues | |
| EXPECTED_REPOSITORY: cometapi-dev/cometapi-node | |
| EXPECTED_REPOSITORY_URL: git+https://github.com/cometapi-dev/cometapi-node.git | |
| EXPECTED_WORKFLOW: Release Please | |
| EXPECTED_WORKFLOW_PATH: .github/workflows/release-please.yml | |
| RELEASE_RESULT: ${{ runner.temp }}/release-please-result/result.json | |
| SOURCE_RUN_FILE: ${{ runner.temp }}/release-please-source-run.json | |
| SOURCE_RUN_ATTEMPT: ${{ env.SOURCE_RELEASE_RUN_ATTEMPT }} | |
| SOURCE_RUN_ID: ${{ env.SOURCE_RELEASE_RUN_ID }} | |
| WORKFLOW_SHA: ${{ env.SOURCE_RELEASE_COMMIT }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| head_commit="$(git rev-parse HEAD)" | |
| if [[ "$head_commit" != "$WORKFLOW_SHA" ]]; then | |
| echo "The successful Release Please SHA is no longer the exact main tip." >&2 | |
| exit 1 | |
| fi | |
| git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main | |
| if [[ "$(git rev-parse refs/remotes/origin/main)" != "$WORKFLOW_SHA" ]]; then | |
| echo "The release commit is no longer the exact origin/main tip." >&2 | |
| exit 1 | |
| fi | |
| node --input-type=module <<'EOF' | |
| import { appendFileSync, readFileSync } from "node:fs"; | |
| import { | |
| validateReleasePleaseActionResult, | |
| validateReleaseWorkflowRun, | |
| } from "./scripts/release-workflow-validation.mjs"; | |
| const sourceRun = JSON.parse( | |
| readFileSync(process.env.SOURCE_RUN_FILE, "utf8"), | |
| ); | |
| const sourceEvent = { | |
| action: "completed", | |
| repository: { full_name: sourceRun.repository?.full_name }, | |
| workflow_run: { | |
| conclusion: sourceRun.conclusion, | |
| event: sourceRun.event, | |
| head_branch: sourceRun.head_branch, | |
| head_repository: sourceRun.head_repository, | |
| head_sha: sourceRun.head_sha, | |
| id: sourceRun.id, | |
| name: sourceRun.name, | |
| path: sourceRun.path, | |
| run_attempt: sourceRun.run_attempt, | |
| }, | |
| }; | |
| const run = validateReleaseWorkflowRun(sourceEvent, { | |
| checkedOutSha: process.env.WORKFLOW_SHA, | |
| repository: process.env.EXPECTED_REPOSITORY, | |
| workflowName: process.env.EXPECTED_WORKFLOW, | |
| workflowPath: process.env.EXPECTED_WORKFLOW_PATH, | |
| }); | |
| if ( | |
| run.runId !== Number(process.env.SOURCE_RUN_ID) || | |
| run.runAttempt !== Number(process.env.SOURCE_RUN_ATTEMPT) | |
| ) { | |
| throw new Error( | |
| "Release workflow source run ID or attempt changed before publication.", | |
| ); | |
| } | |
| const manifest = JSON.parse(readFileSync("package.json", "utf8")); | |
| if ( | |
| manifest.repository?.type !== "git" || | |
| manifest.repository?.url !== process.env.EXPECTED_REPOSITORY_URL | |
| ) { | |
| throw new Error( | |
| `package.json repository must equal ${process.env.EXPECTED_REPOSITORY_URL}.`, | |
| ); | |
| } | |
| if (manifest.bugs?.url !== process.env.EXPECTED_BUGS_URL) { | |
| throw new Error(`package.json bugs.url must equal ${process.env.EXPECTED_BUGS_URL}.`); | |
| } | |
| const actionResult = JSON.parse( | |
| readFileSync(process.env.RELEASE_RESULT, "utf8"), | |
| ); | |
| const release = validateReleasePleaseActionResult(actionResult, { | |
| releaseCommit: run.releaseCommit, | |
| repository: process.env.EXPECTED_REPOSITORY, | |
| runAttempt: run.runAttempt, | |
| runId: run.runId, | |
| version: manifest.version, | |
| workflowName: process.env.EXPECTED_WORKFLOW, | |
| workflowPath: process.env.EXPECTED_WORKFLOW_PATH, | |
| }); | |
| appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| [ | |
| `release-commit=${release.releaseCommit}`, | |
| `release-tag=${release.tag}`, | |
| `release-url=${release.htmlUrl}`, | |
| `release-version=${release.version}`, | |
| "", | |
| ].join("\n"), | |
| ); | |
| EOF | |
| - name: Set up Node.js 24 | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24.x | |
| cache: npm | |
| - name: Install validation dependencies without lifecycle scripts | |
| run: npm ci --ignore-scripts | |
| - name: Verify release metadata and derive the npm dist-tag | |
| id: version | |
| env: | |
| RELEASE_TAG: ${{ steps.trust.outputs.release-tag }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node scripts/validate-release.mjs \ | |
| --tag "$RELEASE_TAG" \ | |
| --require-final \ | |
| --require-releasable-docs >> "$GITHUB_OUTPUT" | |
| - name: Verify the exact immutable GitHub release and tag | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_COMMIT: ${{ steps.trust.outputs.release-commit }} | |
| RELEASE_HTML_URL: ${{ steps.trust.outputs.release-url }} | |
| RELEASE_TAG: ${{ steps.trust.outputs.release-tag }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| release_json="$RUNNER_TEMP/github-release.json" | |
| gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" > "$release_json" | |
| git fetch --no-tags origin \ | |
| "+refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" | |
| tag_commit="$(git rev-parse --verify "refs/tags/${RELEASE_TAG}^{commit}")" | |
| RELEASE_JSON="$release_json" TAG_COMMIT="$tag_commit" \ | |
| node --input-type=module <<'EOF' | |
| import { readFileSync } from "node:fs"; | |
| import { | |
| extractReleaseNotesFromChangelog, | |
| validateGitHubRelease, | |
| } from "./scripts/release-workflow-validation.mjs"; | |
| const release = JSON.parse(readFileSync(process.env.RELEASE_JSON, "utf8")); | |
| const version = JSON.parse(readFileSync("package.json", "utf8")).version; | |
| validateGitHubRelease(release, { | |
| expectedBody: extractReleaseNotesFromChangelog( | |
| readFileSync("CHANGELOG.md", "utf8"), | |
| version, | |
| ), | |
| htmlUrl: process.env.RELEASE_HTML_URL, | |
| releaseCommit: process.env.RELEASE_COMMIT, | |
| tag: process.env.RELEASE_TAG, | |
| tagCommit: process.env.TAG_COMMIT, | |
| }); | |
| EOF | |
| - name: Use a Trusted Publishing-capable npm CLI | |
| run: npm install --global npm@11.12.1 | |
| - name: Freeze the prerelease dist-tag | |
| id: registry-baseline | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| next_version="$(npm view cometapi@next version)" | |
| NEXT_VERSION="$next_version" node --input-type=module <<'EOF' | |
| import { validatePrereleaseDistTagBaseline } from "./scripts/release-workflow-validation.mjs"; | |
| validatePrereleaseDistTagBaseline(process.env.NEXT_VERSION); | |
| EOF | |
| echo "next-version=${next_version}" >> "$GITHUB_OUTPUT" | |
| - name: Install locked dependencies | |
| run: npm ci | |
| - name: Run release checks | |
| run: | | |
| npm run format:check | |
| npm run lint | |
| npm run typecheck | |
| npm test | |
| npm run test:secrets | |
| npm run build | |
| npm run test:package | |
| npm run test:live-contract | |
| npm run test:compat -- --lane locked | |
| npm run check:self-contained | |
| - name: Pack the exact release artifact | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p release-artifacts | |
| npm pack --pack-destination release-artifacts | |
| - name: Select the exact release artifact | |
| id: pack | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mapfile -t tarballs < <(find release-artifacts -maxdepth 1 -type f -name '*.tgz' -print) | |
| if [[ "${#tarballs[@]}" -ne 1 ]]; then | |
| echo "Expected exactly one packed artifact, found ${#tarballs[@]}." >&2 | |
| exit 1 | |
| fi | |
| echo "tarball=${tarballs[0]}" >> "$GITHUB_OUTPUT" | |
| - name: Test consumers against the exact release artifact | |
| run: | | |
| npm run test:package -- \ | |
| --tarball "${{ steps.pack.outputs.tarball }}" \ | |
| --tag "${{ steps.trust.outputs.release-tag }}" | |
| npm run test:examples -- --tarball "${{ steps.pack.outputs.tarball }}" | |
| npm run test:fixtures -- --tarball "${{ steps.pack.outputs.tarball }}" | |
| - name: Name the attempt-qualified release artifact | |
| id: artifact-name | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "name=npm-package-${{ steps.version.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT" | |
| - name: Upload the verified release artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ steps.artifact-name.outputs.name }} | |
| path: ${{ steps.pack.outputs.tarball }} | |
| if-no-files-found: error | |
| retention-days: 30 | |
| live-smoke: | |
| name: Verify the release tag against CometAPI | |
| if: >- | |
| github.run_attempt == 1 && | |
| needs.verify.result == 'success' | |
| needs: | |
| - verify | |
| concurrency: | |
| group: live-smoke | |
| cancel-in-progress: false | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # Required repository configuration: configure the protected live-smoke environment | |
| # without required reviewers and add COMETAPI_KEY before publishing a release. | |
| environment: live-smoke | |
| steps: | |
| - name: Reject a repeated live-smoke attempt | |
| env: | |
| WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" ]]; then | |
| echo "The bounded live smoke is restricted to the initial run attempt." >&2 | |
| exit 1 | |
| fi | |
| - name: Check out the verified release tag | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ needs.verify.outputs.release-commit }} | |
| - name: Set up Node.js 24 | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24.x | |
| cache: npm | |
| - name: Install locked dependencies | |
| run: npm ci | |
| - name: Build the release tag | |
| run: npm run build | |
| - name: Run the bounded live smoke | |
| env: | |
| COMETAPI_KEY: ${{ secrets.COMETAPI_KEY }} | |
| COMETAPI_LIVE_SMOKE: "1" | |
| COMETAPI_SMOKE_MODEL: ${{ vars.COMETAPI_SMOKE_MODEL || 'gpt-5.6-sol' }} | |
| COMETAPI_LIVE_REQUEST_LIMIT: "3" | |
| COMETAPI_LIVE_MAX_OUTPUT_TOKENS: "16" | |
| COMETAPI_LIVE_REQUEST_TIMEOUT_MS: "60000" | |
| COMETAPI_LIVE_CONCURRENCY: "1" | |
| run: npm run test:live | |
| publish: | |
| name: Publish with npm Trusted Publishing | |
| if: >- | |
| always() && | |
| (github.run_attempt == 1 || github.run_attempt == 2) && | |
| needs.live-smoke.result == 'success' && | |
| needs.verify.result == 'success' | |
| needs: | |
| - live-smoke | |
| - verify | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # Required repository configuration: configure the protected npm environment with | |
| # approval by the current release approver and self-review allowed, plus package ownership. | |
| environment: | |
| name: npm | |
| url: https://www.npmjs.com/package/cometapi/v/${{ needs.verify.outputs.version }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| deployments: read | |
| id-token: write | |
| steps: | |
| - name: Reject an out-of-bounds publication attempt | |
| env: | |
| WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" && | |
| "$WORKFLOW_RUN_ATTEMPT" != "2" ]]; then | |
| echo "Publication permits only the initial attempt and one failed-job replay." >&2 | |
| exit 1 | |
| fi | |
| - name: Check out the verified release commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| ref: ${{ needs.verify.outputs.release-commit }} | |
| - name: Set up Node.js 24 | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 24.x | |
| package-manager-cache: false | |
| registry-url: https://registry.npmjs.org | |
| - name: Use a Trusted Publishing-capable npm CLI | |
| run: npm install --global npm@11.12.1 | |
| - name: Download the verified release artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ needs.verify.outputs.artifact-name }} | |
| path: release-artifacts | |
| - name: Reconfirm protected state immediately before publication | |
| id: pre-publish | |
| env: | |
| CONTROL_COMMIT: ${{ needs.verify.outputs.control-commit }} | |
| CONTROL_VALIDATOR: ${{ runner.temp }}/release-workflow-validation.mjs | |
| ENVIRONMENT_FILE: ${{ runner.temp }}/npm-environment.json | |
| EVENT_REF: ${{ github.ref }} | |
| EVENT_SHA: ${{ github.sha }} | |
| EXPECTED_NEXT_VERSION: ${{ needs.verify.outputs.expected-next-version }} | |
| GH_TOKEN: ${{ github.token }} | |
| POLICIES_FILE: ${{ runner.temp }}/npm-deployment-policies.json | |
| RELEASE_COMMIT: ${{ needs.verify.outputs.release-commit }} | |
| RELEASE_FILE: ${{ runner.temp }}/github-release-before-publish.json | |
| RELEASE_RUN_ATTEMPT: ${{ inputs.release_run_attempt }} | |
| RELEASE_RUN_ID: ${{ inputs.release_run_id }} | |
| RELEASE_RUN_FILE: ${{ runner.temp }}/release-run-before-publish.json | |
| RELEASE_TAG: ${{ needs.verify.outputs.release-tag }} | |
| RELEASE_PLEASE_ENABLED: ${{ vars.RELEASE_PLEASE_ENABLED }} | |
| RELEASE_PLEASE_RUNS: ${{ runner.temp }}/release-please-runs-before-publish.json | |
| RELEASE_PLEASE_SNAPSHOT: ${{ needs.verify.outputs.release-please-snapshot }} | |
| VERSION: ${{ needs.verify.outputs.version }} | |
| WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| WORKFLOW_SHA: ${{ github.workflow_sha }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "$(git rev-parse HEAD)" != "$RELEASE_COMMIT" ]]; then | |
| echo "The publication checkout no longer matches the verified release commit." >&2 | |
| exit 1 | |
| fi | |
| git fetch --no-tags origin \ | |
| +refs/heads/main:refs/remotes/origin/main \ | |
| "+refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" | |
| main_commit="$(git rev-parse refs/remotes/origin/main)" | |
| tag_commit="$(git rev-parse --verify "refs/tags/${RELEASE_TAG}^{commit}")" | |
| if [[ "$tag_commit" != "$RELEASE_COMMIT" ]]; then | |
| echo "The release tag no longer resolves to the verified commit." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$EVENT_REF" != "refs/tags/${RELEASE_TAG}" || | |
| "$EVENT_SHA" != "$RELEASE_COMMIT" || | |
| "$WORKFLOW_SHA" != "$RELEASE_COMMIT" || | |
| "$CONTROL_COMMIT" != "$RELEASE_COMMIT" ]]; then | |
| echo "The tag publication identity changed while awaiting approval." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$main_commit" != "$RELEASE_COMMIT" ]]; then | |
| echo "The release commit is no longer the exact origin/main tip." >&2 | |
| exit 1 | |
| fi | |
| git show "${CONTROL_COMMIT}:scripts/release-workflow-validation.mjs" \ | |
| > "$CONTROL_VALIDATOR" | |
| gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" \ | |
| > "$RELEASE_FILE" | |
| gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RELEASE_RUN_ID}" \ | |
| > "$RELEASE_RUN_FILE" | |
| gh api "repos/${GITHUB_REPOSITORY}/environments/npm" \ | |
| > "$ENVIRONMENT_FILE" | |
| gh api --paginate --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/environments/npm/deployment-branch-policies?per_page=100" \ | |
| | jq '{branch_policies: [.[].branch_policies[]]}' > "$POLICIES_FILE" | |
| if [[ "$RELEASE_PLEASE_ENABLED" != "true" ]]; then | |
| echo "RELEASE_PLEASE_ENABLED changed while publication awaited approval." >&2 | |
| exit 1 | |
| fi | |
| for state in in_progress queued waiting requested pending; do | |
| runs_file="$RUNNER_TEMP/publish-${state}-runs.json" | |
| gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/publish.yml/runs?status=${state}&per_page=100" \ | |
| > "$runs_file" | |
| if ! jq -e --argjson current "$GITHUB_RUN_ID" \ | |
| '[.workflow_runs[] | select(.id != $current)] | length == 0' \ | |
| "$runs_file" >/dev/null; then | |
| echo "A competing Publish run appeared before registry mutation." >&2 | |
| exit 1 | |
| fi | |
| done | |
| view_error="$RUNNER_TEMP/npm-view-before-publish.err" | |
| set +e | |
| exact_version="$(npm view "cometapi@${VERSION}" version 2>"$view_error")" | |
| view_status=$? | |
| set -e | |
| if [[ "$view_status" -ne 0 ]]; then | |
| if grep -q "E404" "$view_error"; then | |
| exact_version="" | |
| else | |
| echo "Unable to read the exact registry version before publication." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ "$WORKFLOW_RUN_ATTEMPT" == "2" && -z "$exact_version" ]]; then | |
| echo "The failed-job replay requires the exact registry version to exist." >&2 | |
| exit 1 | |
| fi | |
| latest_version="$(npm view cometapi@latest version)" | |
| next_version="$(npm view cometapi@next version)" | |
| gh api --paginate --slurp \ | |
| "repos/${GITHUB_REPOSITORY}/actions/workflows/release-please.yml/runs?per_page=100" \ | |
| | jq '[.[].workflow_runs[]]' > "$RELEASE_PLEASE_RUNS" | |
| EXACT_VERSION="$exact_version" LATEST_VERSION="$latest_version" \ | |
| NEXT_VERSION="$next_version" TAG_COMMIT="$tag_commit" \ | |
| node --input-type=module <<'EOF' | |
| import { createHash } from "node:crypto"; | |
| import { readFileSync } from "node:fs"; | |
| import { pathToFileURL } from "node:url"; | |
| const { | |
| extractReleaseNotesFromChangelog, | |
| validateGitHubRelease, | |
| validateNpmEnvironmentState, | |
| validateRegistryStateBeforePublish, | |
| validateReleaseWorkflowRun, | |
| snapshotReleasePleaseRuns, | |
| } = await import(pathToFileURL(process.env.CONTROL_VALIDATOR)); | |
| const snapshot = snapshotReleasePleaseRuns( | |
| JSON.parse(readFileSync(process.env.RELEASE_PLEASE_RUNS, "utf8")), | |
| ); | |
| const digest = createHash("sha256").update(snapshot).digest("hex"); | |
| if (digest !== process.env.RELEASE_PLEASE_SNAPSHOT) { | |
| throw new Error( | |
| "Release workflow Release Please run set changed while publication awaited approval.", | |
| ); | |
| } | |
| const sourceRun = JSON.parse( | |
| readFileSync(process.env.RELEASE_RUN_FILE, "utf8"), | |
| ); | |
| const run = validateReleaseWorkflowRun( | |
| { | |
| action: "completed", | |
| repository: { full_name: sourceRun.repository?.full_name }, | |
| workflow_run: { | |
| conclusion: sourceRun.conclusion, | |
| event: sourceRun.event, | |
| head_branch: sourceRun.head_branch, | |
| head_repository: sourceRun.head_repository, | |
| head_sha: sourceRun.head_sha, | |
| id: sourceRun.id, | |
| name: sourceRun.name, | |
| path: sourceRun.path, | |
| run_attempt: sourceRun.run_attempt, | |
| }, | |
| }, | |
| { | |
| checkedOutSha: process.env.RELEASE_COMMIT, | |
| repository: "cometapi-dev/cometapi-node", | |
| workflowName: "Release Please", | |
| workflowPath: ".github/workflows/release-please.yml", | |
| }, | |
| ); | |
| if ( | |
| run.runId !== Number(process.env.RELEASE_RUN_ID) || | |
| run.runAttempt !== Number(process.env.RELEASE_RUN_ATTEMPT) | |
| ) { | |
| throw new Error( | |
| "Release workflow source run changed while publication awaited approval.", | |
| ); | |
| } | |
| const version = JSON.parse(readFileSync("package.json", "utf8")).version; | |
| validateGitHubRelease( | |
| JSON.parse(readFileSync(process.env.RELEASE_FILE, "utf8")), | |
| { | |
| expectedBody: extractReleaseNotesFromChangelog( | |
| readFileSync("CHANGELOG.md", "utf8"), | |
| version, | |
| ), | |
| htmlUrl: `https://github.com/cometapi-dev/cometapi-node/releases/tag/${process.env.RELEASE_TAG}`, | |
| releaseCommit: process.env.RELEASE_COMMIT, | |
| tag: process.env.RELEASE_TAG, | |
| tagCommit: process.env.TAG_COMMIT, | |
| }, | |
| ); | |
| validateNpmEnvironmentState({ | |
| environment: JSON.parse( | |
| readFileSync(process.env.ENVIRONMENT_FILE, "utf8"), | |
| ), | |
| expectedPolicyIds: { "tag:v*": 55718965 }, | |
| operation: "release", | |
| policies: JSON.parse( | |
| readFileSync(process.env.POLICIES_FILE, "utf8"), | |
| ).branch_policies, | |
| }); | |
| validateRegistryStateBeforePublish({ | |
| exactVersion: process.env.EXACT_VERSION || null, | |
| expectedNextVersion: process.env.EXPECTED_NEXT_VERSION, | |
| latestVersion: process.env.LATEST_VERSION, | |
| nextVersion: process.env.NEXT_VERSION, | |
| version, | |
| }); | |
| EOF | |
| if [[ -n "$exact_version" ]]; then | |
| echo "expect-existing=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "expect-existing=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Publish the exact artifact with provenance | |
| env: | |
| DIST_TAG: ${{ needs.verify.outputs.dist-tag }} | |
| EXPECT_EXISTING: ${{ steps.pre-publish.outputs.expect-existing }} | |
| VERSION: ${{ needs.verify.outputs.version }} | |
| run: bash scripts/publish-artifact.sh | |
| - name: Verify the public registry artifact | |
| env: | |
| CONTROL_COMMIT: ${{ needs.verify.outputs.control-commit }} | |
| CONTROL_VALIDATOR: ${{ runner.temp }}/release-workflow-validation.mjs | |
| DIST_TAG: ${{ needs.verify.outputs.dist-tag }} | |
| EXPECTED_NEXT_VERSION: ${{ needs.verify.outputs.expected-next-version }} | |
| GH_TOKEN: ${{ github.token }} | |
| WORKFLOW_REF: ${{ github.ref }} | |
| VERSION: ${{ needs.verify.outputs.version }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mapfile -t tarballs < <(find "$GITHUB_WORKSPACE/release-artifacts" -maxdepth 1 -type f -name '*.tgz' -print) | |
| if [[ "${#tarballs[@]}" -ne 1 ]]; then | |
| echo "Expected exactly one downloaded artifact for registry verification." >&2 | |
| exit 1 | |
| fi | |
| local_integrity="$(node -e 'const {createHash}=require("node:crypto");const {readFileSync}=require("node:fs");process.stdout.write("sha512-"+createHash("sha512").update(readFileSync(process.argv[1])).digest("base64"))' "${tarballs[0]}")" | |
| registry_ready="false" | |
| for attempt in {1..12}; do | |
| resolved="$(npm view "cometapi@${VERSION}" version 2>/dev/null || true)" | |
| tagged="$(npm view "cometapi@${DIST_TAG}" version 2>/dev/null || true)" | |
| registry_dist="$(npm view "cometapi@${VERSION}" dist --json 2>/dev/null || true)" | |
| if [[ "$resolved" == "$VERSION" && "$tagged" == "$VERSION" && -n "$registry_dist" ]] && \ | |
| REGISTRY_DIST="$registry_dist" LOCAL_INTEGRITY="$local_integrity" node <<'EOF' | |
| let ready = false; | |
| try { | |
| const dist = JSON.parse(process.env.REGISTRY_DIST); | |
| ready = | |
| dist.integrity === process.env.LOCAL_INTEGRITY && | |
| Boolean(dist.attestations?.url) && | |
| dist.attestations?.provenance?.predicateType === | |
| "https://slsa.dev/provenance/v1"; | |
| } catch {} | |
| process.exitCode = ready ? 0 : 1; | |
| EOF | |
| then | |
| registry_ready="true" | |
| break | |
| fi | |
| if [[ "$attempt" -lt 12 ]]; then | |
| sleep 10 | |
| fi | |
| done | |
| if [[ "$registry_ready" != "true" ]]; then | |
| echo "Registry state did not converge for cometapi@${VERSION}, ${DIST_TAG}, integrity, and provenance." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$(npm view cometapi@next version)" != "$EXPECTED_NEXT_VERSION" ]]; then | |
| echo "The next dist-tag changed during publication." >&2 | |
| exit 1 | |
| fi | |
| attestations_url="$(REGISTRY_DIST="$registry_dist" node -e 'process.stdout.write(JSON.parse(process.env.REGISTRY_DIST).attestations.url)')" | |
| attestations_file="$RUNNER_TEMP/npm-attestations.json" | |
| ATTESTATIONS_URL="$attestations_url" \ | |
| node --input-type=module <<'EOF' | |
| import { pathToFileURL } from "node:url"; | |
| const { validateRegistryAttestationUrl } = await import( | |
| pathToFileURL(process.env.CONTROL_VALIDATOR) | |
| ); | |
| validateRegistryAttestationUrl({ | |
| url: process.env.ATTESTATIONS_URL, | |
| version: process.env.VERSION, | |
| }); | |
| EOF | |
| ATTESTATIONS_FILE="$attestations_file" \ | |
| ATTESTATIONS_URL="$attestations_url" \ | |
| bash scripts/fetch-attestations.sh | |
| post_exact_version="$(npm view "cometapi@${VERSION}" version)" | |
| post_tagged_version="$(npm view "cometapi@${DIST_TAG}" version)" | |
| post_next_version="$(npm view cometapi@next version)" | |
| post_registry_dist="$(npm view "cometapi@${VERSION}" dist --json)" | |
| ATTESTATIONS_URL="$attestations_url" \ | |
| EXACT_VERSION="$post_exact_version" \ | |
| EXPECTED_INTEGRITY="$local_integrity" \ | |
| NEXT_VERSION="$post_next_version" \ | |
| REGISTRY_DIST="$post_registry_dist" \ | |
| TAGGED_VERSION="$post_tagged_version" \ | |
| node --input-type=module <<'EOF' | |
| import { pathToFileURL } from "node:url"; | |
| const { validatePublishedRegistryState } = await import( | |
| pathToFileURL(process.env.CONTROL_VALIDATOR) | |
| ); | |
| validatePublishedRegistryState({ | |
| attestationUrl: process.env.ATTESTATIONS_URL, | |
| dist: JSON.parse(process.env.REGISTRY_DIST), | |
| exactVersion: process.env.EXACT_VERSION, | |
| expectedNextVersion: process.env.EXPECTED_NEXT_VERSION, | |
| expectedIntegrity: process.env.EXPECTED_INTEGRITY, | |
| nextVersion: process.env.NEXT_VERSION, | |
| taggedVersion: process.env.TAGGED_VERSION, | |
| version: process.env.VERSION, | |
| }); | |
| EOF | |
| registry_dist="$post_registry_dist" | |
| local_sha512="$(node -e 'const {createHash}=require("node:crypto");const {readFileSync}=require("node:fs");process.stdout.write(createHash("sha512").update(readFileSync(process.argv[1])).digest("hex"))' "${tarballs[0]}")" | |
| provenance_identity="$(ATTESTATIONS_FILE="$attestations_file" \ | |
| LOCAL_SHA512="$local_sha512" node --input-type=module <<'EOF' | |
| import { readFileSync } from "node:fs"; | |
| import { pathToFileURL } from "node:url"; | |
| const { validateRegistryProvenance } = await import( | |
| pathToFileURL(process.env.CONTROL_VALIDATOR) | |
| ); | |
| const provenance = validateRegistryProvenance({ | |
| attestations: JSON.parse( | |
| readFileSync(process.env.ATTESTATIONS_FILE, "utf8"), | |
| ), | |
| commit: process.env.CONTROL_COMMIT, | |
| sha512: process.env.LOCAL_SHA512, | |
| version: process.env.VERSION, | |
| workflowRef: process.env.WORKFLOW_REF, | |
| }); | |
| process.stdout.write( | |
| `${provenance.provenanceRunId} ${provenance.provenanceRunAttempt}`, | |
| ); | |
| EOF | |
| )" | |
| read -r provenance_run_id provenance_run_attempt <<< "$provenance_identity" | |
| provenance_run_file="$RUNNER_TEMP/provenance-run.json" | |
| provenance_jobs_file="$RUNNER_TEMP/provenance-jobs.json" | |
| provenance_run_valid="false" | |
| for attempt in {1..6}; do | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${provenance_run_id}/attempts/${provenance_run_attempt}" \ | |
| > "$provenance_run_file" | |
| gh api \ | |
| "repos/${GITHUB_REPOSITORY}/actions/runs/${provenance_run_id}/attempts/${provenance_run_attempt}/jobs?per_page=100" \ | |
| > "$provenance_jobs_file" | |
| if PROVENANCE_JOBS_FILE="$provenance_jobs_file" \ | |
| PROVENANCE_RUN_ATTEMPT="$provenance_run_attempt" \ | |
| PROVENANCE_RUN_FILE="$provenance_run_file" \ | |
| PROVENANCE_RUN_ID="$provenance_run_id" node --input-type=module <<'EOF' | |
| import { readFileSync } from "node:fs"; | |
| import { pathToFileURL } from "node:url"; | |
| const { validateRegistryProvenanceInvocation } = await import( | |
| pathToFileURL(process.env.CONTROL_VALIDATOR) | |
| ); | |
| validateRegistryProvenanceInvocation({ | |
| commit: process.env.CONTROL_COMMIT, | |
| jobs: JSON.parse( | |
| readFileSync(process.env.PROVENANCE_JOBS_FILE, "utf8"), | |
| ).jobs, | |
| run: JSON.parse( | |
| readFileSync(process.env.PROVENANCE_RUN_FILE, "utf8"), | |
| ), | |
| runAttempt: Number(process.env.PROVENANCE_RUN_ATTEMPT), | |
| runId: Number(process.env.PROVENANCE_RUN_ID), | |
| workflowRef: process.env.WORKFLOW_REF, | |
| }); | |
| EOF | |
| then | |
| provenance_run_valid="true" | |
| break | |
| fi | |
| if [[ "$attempt" -lt 6 ]]; then | |
| sleep 5 | |
| fi | |
| done | |
| if [[ "$provenance_run_valid" != "true" ]]; then | |
| echo "The signed provenance invocation did not match a successful npm publish step." >&2 | |
| exit 1 | |
| fi | |
| verify_dir="$(mktemp -d)" | |
| cd "$verify_dir" | |
| npm init --yes >/dev/null | |
| npm install --ignore-scripts --no-audit --no-fund \ | |
| "openai@6.47.0" "cometapi@${VERSION}" "typescript@5.9.3" | |
| signatures_verified="false" | |
| for attempt in {1..3}; do | |
| if npm audit signatures; then | |
| signatures_verified="true" | |
| break | |
| fi | |
| if [[ "$attempt" -lt 3 ]]; then | |
| sleep 10 | |
| fi | |
| done | |
| if [[ "$signatures_verified" != "true" ]]; then | |
| echo "Registry signature and provenance verification did not converge." >&2 | |
| exit 1 | |
| fi | |
| npm ls openai --all | |
| if [[ -d node_modules/cometapi/node_modules/openai ]]; then | |
| echo "The registry fixture contains a nested OpenAI installation." >&2 | |
| exit 1 | |
| fi | |
| node --input-type=module <<'EOF' | |
| import assert from "node:assert/strict"; | |
| import { CometAPI } from "cometapi"; | |
| const client = new CometAPI({ | |
| apiKey: "mock-registry-key", | |
| maxRetries: 0, | |
| fetch: async (input) => { | |
| const url = new URL( | |
| input instanceof Request ? input.url : String(input), | |
| ); | |
| let body; | |
| if (url.pathname.endsWith("/chat/completions")) { | |
| body = { | |
| id: "chatcmpl_registry", | |
| object: "chat.completion", | |
| created: 1, | |
| model: "gpt-5.4", | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { role: "assistant", content: "ok" }, | |
| finish_reason: "stop", | |
| }, | |
| ], | |
| }; | |
| } else if (url.pathname.endsWith("/responses")) { | |
| body = { | |
| id: "resp_registry", | |
| object: "response", | |
| created_at: 1, | |
| status: "completed", | |
| model: "gpt-5.4", | |
| output: [], | |
| parallel_tool_calls: true, | |
| tool_choice: "auto", | |
| tools: [], | |
| }; | |
| } else if (url.pathname.endsWith("/models")) { | |
| body = { object: "list", data: [] }; | |
| } else { | |
| throw new Error(`Unexpected registry fixture URL: ${url}`); | |
| } | |
| return new Response(JSON.stringify(body), { | |
| status: 200, | |
| headers: { "content-type": "application/json" }, | |
| }); | |
| }, | |
| }); | |
| const completion = await client.chat.completions.create({ | |
| model: "gpt-5.4", | |
| messages: [{ role: "user", content: "test" }], | |
| }); | |
| assert.equal(completion.id, "chatcmpl_registry"); | |
| const response = await client.responses.create({ | |
| model: "gpt-5.4", | |
| input: "test", | |
| }); | |
| assert.equal(response.id, "resp_registry"); | |
| const models = await client.models.list(); | |
| assert.deepEqual(models.data, []); | |
| EOF | |
| node --input-type=module <<'EOF' | |
| import { writeFileSync } from "node:fs"; | |
| const source = `import { CometAPI, type CometAPIOptions } from "cometapi"; | |
| const options: CometAPIOptions = { apiKey: "typed-registry-key", maxRetries: 0 }; | |
| const client = new CometAPI(options); | |
| void client.chat.completions.create({ model: "gpt-5.4", messages: [] }); | |
| void client.responses.create({ model: "gpt-5.4", input: "test" }); | |
| void client.models.list(); | |
| `; | |
| writeFileSync("consumer.mts", source); | |
| writeFileSync("consumer.cts", source); | |
| writeFileSync( | |
| "tsconfig.json", | |
| `${JSON.stringify( | |
| { | |
| compilerOptions: { | |
| module: "NodeNext", | |
| moduleResolution: "NodeNext", | |
| noEmit: true, | |
| strict: true, | |
| target: "ES2022", | |
| }, | |
| include: ["consumer.mts", "consumer.cts"], | |
| }, | |
| null, | |
| 2, | |
| )}\n`, | |
| ); | |
| EOF | |
| ./node_modules/.bin/tsc --noEmit | |
| node <<'EOF' | |
| const assert = require("node:assert/strict"); | |
| const { CometAPI } = require("cometapi"); | |
| const { APIError } = require("openai"); | |
| const client = new CometAPI({ | |
| apiKey: "mock-registry-key", | |
| maxRetries: 0, | |
| fetch: async () => | |
| new Response( | |
| JSON.stringify({ | |
| error: { | |
| message: "mock registry failure", | |
| type: "invalid_request_error", | |
| }, | |
| }), | |
| { | |
| status: 400, | |
| headers: { "content-type": "application/json" }, | |
| }, | |
| ), | |
| }); | |
| (async () => { | |
| let caught; | |
| try { | |
| await client.models.list(); | |
| } catch (error) { | |
| caught = error; | |
| } | |
| assert.ok(caught instanceof APIError); | |
| })().catch((error) => { | |
| console.error(error); | |
| process.exitCode = 1; | |
| }); | |
| EOF | |
| result: | |
| name: Enforce the bounded publication result | |
| if: >- | |
| always() && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.publish_operation == 'release' | |
| needs: | |
| - live-smoke | |
| - publish | |
| - verify | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| steps: | |
| - name: Reject skipped or out-of-bounds publication | |
| env: | |
| LIVE_SMOKE_RESULT: ${{ needs.live-smoke.result }} | |
| PUBLISH_RESULT: ${{ needs.publish.result }} | |
| VERIFY_RESULT: ${{ needs.verify.result }} | |
| WORKFLOW_RUN_ATTEMPT: ${{ github.run_attempt }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$WORKFLOW_RUN_ATTEMPT" != "1" && | |
| "$WORKFLOW_RUN_ATTEMPT" != "2" ]]; then | |
| echo "Publication permits only the initial attempt and one failed-job replay." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$VERIFY_RESULT" != "success" || | |
| "$LIVE_SMOKE_RESULT" != "success" || | |
| "$PUBLISH_RESULT" != "success" ]]; then | |
| echo "The bounded publication job set did not complete successfully." >&2 | |
| exit 1 | |
| fi |