11name : Publish
22
33on :
4- push :
5- branches :
6- - main
7- paths :
8- - .github/workflows/publish.yml
4+ deployment :
95 workflow_run :
106 workflows :
117 - Release Please
1410
1511permissions :
1612 actions : read
13+ checks : read
1714 contents : read
1815
1916concurrency :
@@ -29,72 +26,159 @@ jobs:
2926 github.event.workflow_run.conclusion == 'success' &&
3027 github.event.workflow_run.event == 'push' &&
3128 github.event.workflow_run.head_branch == 'main') ||
32- (github.event_name == 'push' && github.ref == 'refs/heads/main'))
29+ (github.event_name == 'deployment' &&
30+ github.event.deployment.task == 'npm-publish-recovery' &&
31+ github.event.deployment.environment == 'npm' &&
32+ github.event.deployment.ref == 'v0.1.1' &&
33+ github.event.deployment.sha == 'c98b514227858cd183c781270a7f78f65b577e82'))
3334 runs-on : ubuntu-latest
3435 timeout-minutes : 30
3536 env :
36- SOURCE_RELEASE_COMMIT : ${{ github.event_name == 'push ' && 'c98b514227858cd183c781270a7f78f65b577e82' || github.event.workflow_run.head_sha }}
37- SOURCE_RELEASE_RUN_ATTEMPT : ${{ github.event_name == 'push ' && '1' || github.event.workflow_run.run_attempt }}
38- SOURCE_RELEASE_RUN_ID : ${{ github.event_name == 'push ' && '30469181724' || github.event.workflow_run.id }}
37+ SOURCE_RELEASE_COMMIT : ${{ github.event_name == 'deployment ' && 'c98b514227858cd183c781270a7f78f65b577e82' || github.event.workflow_run.head_sha }}
38+ SOURCE_RELEASE_RUN_ATTEMPT : ${{ github.event_name == 'deployment ' && '1' || github.event.workflow_run.run_attempt }}
39+ SOURCE_RELEASE_RUN_ID : ${{ github.event_name == 'deployment ' && '30469181724' || github.event.workflow_run.id }}
3940 outputs :
4041 artifact-name : ${{ steps.artifact-name.outputs.name }}
4142 dist-tag : ${{ steps.version.outputs.dist-tag }}
4243 release-commit : ${{ steps.trust.outputs.release-commit }}
4344 release-tag : ${{ steps.trust.outputs.release-tag }}
45+ reuse-live-smoke : ${{ steps.recovery-evidence.outputs.reuse-live-smoke }}
4446 version : ${{ steps.version.outputs.version }}
4547 steps :
4648 - name : Check out the workflow control commit
4749 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
4850 with :
4951 fetch-depth : 0
5052 persist-credentials : false
51- ref : ${{ github.sha }}
52- - name : Validate the one-cycle exact release recovery
53- if : github.event_name == 'push '
53+ ref : ${{ github.event_name == 'deployment' && github.workflow_sha || github. sha }}
54+ - name : Validate the exact tag deployment recovery
55+ if : github.event_name == 'deployment '
5456 env :
5557 ACTOR : ${{ github.actor }}
5658 CHANGED_FILES : ${{ runner.temp }}/publish-recovery-files
57- EVENT_AFTER : ${{ github.event.after }}
58- EVENT_BEFORE : ${{ github.event.before }}
59+ CONTROL_COMMIT : ${{ github.workflow_sha }}
60+ DEPLOYMENT_CREATOR : ${{ github.event.deployment.creator.login }}
61+ DEPLOYMENT_ENVIRONMENT : ${{ github.event.deployment.environment }}
62+ DEPLOYMENT_ID : ${{ github.event.deployment.id }}
63+ DEPLOYMENT_REF : ${{ github.event.deployment.ref }}
64+ DEPLOYMENT_RELEASE_COMMIT : ${{ github.event.deployment.payload.release_commit }}
65+ DEPLOYMENT_RELEASE_TAG : ${{ github.event.deployment.payload.release_tag }}
66+ DEPLOYMENT_SHA : ${{ github.event.deployment.sha }}
67+ DEPLOYMENT_SOURCE_RUN_ATTEMPT : ${{ github.event.deployment.payload.source_run_attempt }}
68+ DEPLOYMENT_SOURCE_RUN_ID : ${{ github.event.deployment.payload.source_run_id }}
69+ DEPLOYMENT_TASK : ${{ github.event.deployment.task }}
5970 EVENT_NAME : ${{ github.event_name }}
6071 EVENT_REF : ${{ github.ref }}
61- MAIN_COMMIT : ${{ github.sha }}
72+ EVENT_SHA : ${{ github.sha }}
73+ MAIN_COMMIT : ${{ github.workflow_sha }}
74+ TRIGGERING_ACTOR : ${{ github.triggering_actor }}
6275 WORKFLOW_RUN_ATTEMPT : ${{ github.run_attempt }}
6376 shell : bash
6477 run : |
6578 set -euo pipefail
66- if [[ "$(git rev-parse HEAD)" != "$MAIN_COMMIT " ]]; then
79+ if [[ "$(git rev-parse HEAD)" != "$CONTROL_COMMIT " ]]; then
6780 echo "The recovery control checkout does not match the triggering SHA." >&2
6881 exit 1
6982 fi
7083 git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
71- if [[ "$(git rev-parse refs/remotes/origin/main)" != "$MAIN_COMMIT" ]]; then
84+ MAIN_COMMIT="$(git rev-parse refs/remotes/origin/main)"
85+ if [[ "$MAIN_COMMIT" != "$CONTROL_COMMIT" ]]; then
7286 echo "main moved after the publish recovery was triggered." >&2
7387 exit 1
7488 fi
75- git diff --name-only "$EVENT_BEFORE" "$MAIN_COMMIT" > "$CHANGED_FILES"
76- MAIN_FIRST_PARENT="$(git rev-parse "${MAIN_COMMIT}^1")" \
89+ CONTROL_FIRST_PARENT="$(git rev-parse "${CONTROL_COMMIT}^1")"
90+ git diff --name-only "$CONTROL_FIRST_PARENT" "$CONTROL_COMMIT" > "$CHANGED_FILES"
91+ MAIN_COMMIT="$MAIN_COMMIT" CONTROL_FIRST_PARENT="$CONTROL_FIRST_PARENT" \
7792 node --input-type=module <<'EOF'
7893 import { readFileSync } from "node:fs";
79- import { validatePublishRecoveryTrigger } from "./scripts/release-workflow-validation.mjs";
94+ import { validatePublishDeploymentRecoveryTrigger } from "./scripts/release-workflow-validation.mjs";
8095
81- validatePublishRecoveryTrigger ({
96+ validatePublishDeploymentRecoveryTrigger ({
8297 actor: process.env.ACTOR,
8398 changedFiles: readFileSync(process.env.CHANGED_FILES, "utf8")
8499 .split("\n")
85100 .filter((file) => file !== ""),
86- eventAfter: process.env.EVENT_AFTER,
87- eventBefore: process.env.EVENT_BEFORE,
101+ controlCommit: process.env.CONTROL_COMMIT,
102+ controlFirstParent: process.env.CONTROL_FIRST_PARENT,
103+ deploymentCreator: process.env.DEPLOYMENT_CREATOR,
104+ deploymentEnvironment: process.env.DEPLOYMENT_ENVIRONMENT,
105+ deploymentId: Number(process.env.DEPLOYMENT_ID),
106+ deploymentRef: process.env.DEPLOYMENT_REF,
107+ deploymentReleaseCommit: process.env.DEPLOYMENT_RELEASE_COMMIT,
108+ deploymentReleaseTag: process.env.DEPLOYMENT_RELEASE_TAG,
109+ deploymentSha: process.env.DEPLOYMENT_SHA,
110+ deploymentSourceRunAttempt: Number(
111+ process.env.DEPLOYMENT_SOURCE_RUN_ATTEMPT,
112+ ),
113+ deploymentSourceRunId: Number(process.env.DEPLOYMENT_SOURCE_RUN_ID),
114+ deploymentTask: process.env.DEPLOYMENT_TASK,
88115 eventName: process.env.EVENT_NAME,
89116 eventRef: process.env.EVENT_REF,
117+ eventSha: process.env.EVENT_SHA,
90118 mainCommit: process.env.MAIN_COMMIT,
91- mainFirstParent: process.env.MAIN_FIRST_PARENT,
92119 sourceReleaseCommit: process.env.SOURCE_RELEASE_COMMIT,
93120 sourceRunAttempt: Number(process.env.SOURCE_RELEASE_RUN_ATTEMPT),
94121 sourceRunId: Number(process.env.SOURCE_RELEASE_RUN_ID),
122+ triggeringActor: process.env.TRIGGERING_ACTOR,
95123 workflowRunAttempt: Number(process.env.WORKFLOW_RUN_ATTEMPT),
96124 });
97125 EOF
126+ - name : Validate the prior artifact and bounded live evidence
127+ id : recovery-evidence
128+ if : github.event_name == 'deployment'
129+ env :
130+ GH_TOKEN : ${{ github.token }}
131+ RECOVERY_ANNOTATIONS : ${{ runner.temp }}/publish-recovery-annotations.json
132+ RECOVERY_ARTIFACTS : ${{ runner.temp }}/publish-recovery-artifacts.json
133+ RECOVERY_JOBS : ${{ runner.temp }}/publish-recovery-jobs.json
134+ RECOVERY_LIVE_LOG : ${{ runner.temp }}/publish-recovery-live.log
135+ RECOVERY_RUN : ${{ runner.temp }}/publish-recovery-run.json
136+ SOURCE_PUBLISH_ATTEMPT : ${{ github.event.deployment.payload.source_run_attempt }}
137+ SOURCE_PUBLISH_RUN_ID : ${{ github.event.deployment.payload.source_run_id }}
138+ shell : bash
139+ run : |
140+ set -euo pipefail
141+ gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_PUBLISH_RUN_ID}" \
142+ > "$RECOVERY_RUN"
143+ gh api \
144+ "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_PUBLISH_RUN_ID}/attempts/${SOURCE_PUBLISH_ATTEMPT}/jobs?per_page=100" \
145+ > "$RECOVERY_JOBS"
146+ gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_PUBLISH_RUN_ID}/artifacts" \
147+ > "$RECOVERY_ARTIFACTS"
148+ gh api "repos/${GITHUB_REPOSITORY}/check-runs/90643868523/annotations" \
149+ > "$RECOVERY_ANNOTATIONS"
150+ gh api "repos/${GITHUB_REPOSITORY}/actions/jobs/90643725110/logs" \
151+ > "$RECOVERY_LIVE_LOG"
152+ if [[ "$(grep -Fc 'Live smoke passed 3 sequential requests with a 16-token output cap.' "$RECOVERY_LIVE_LOG")" != "1" ]]; then
153+ echo "The source run no longer proves the exact bounded live smoke." >&2
154+ exit 1
155+ fi
156+
157+ node --input-type=module <<'EOF'
158+ import { appendFileSync, readFileSync } from "node:fs";
159+ import { validatePublishRecoveryEvidence } from "./scripts/release-workflow-validation.mjs";
160+
161+ const result = validatePublishRecoveryEvidence({
162+ annotations: JSON.parse(
163+ readFileSync(process.env.RECOVERY_ANNOTATIONS, "utf8"),
164+ ),
165+ artifacts: JSON.parse(
166+ readFileSync(process.env.RECOVERY_ARTIFACTS, "utf8"),
167+ ).artifacts,
168+ jobs: JSON.parse(readFileSync(process.env.RECOVERY_JOBS, "utf8")).jobs,
169+ run: JSON.parse(readFileSync(process.env.RECOVERY_RUN, "utf8")),
170+ });
171+ appendFileSync(
172+ process.env.GITHUB_OUTPUT,
173+ [
174+ `artifact-id=${result.artifactId}`,
175+ `artifact-name=${result.artifactName}`,
176+ `live-job-id=${result.liveJobId}`,
177+ "reuse-live-smoke=true",
178+ "",
179+ ].join("\n"),
180+ );
181+ EOF
98182 - name : Check out the exact release commit
99183 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
100184 with :
@@ -127,7 +211,7 @@ jobs:
127211 EXPECTED_REPOSITORY_URL : git+https://github.com/cometapi-dev/cometapi-node.git
128212 EXPECTED_WORKFLOW : Release Please
129213 EXPECTED_WORKFLOW_PATH : .github/workflows/release-please.yml
130- CONTROL_SHA : ${{ github.sha }}
214+ CONTROL_SHA : ${{ github.event_name == 'deployment' && github.workflow_sha || github. sha }}
131215 EVENT_NAME : ${{ github.event_name }}
132216 RELEASE_RESULT : ${{ runner.temp }}/release-please-result/result.json
133217 SOURCE_RUN_FILE : ${{ runner.temp }}/release-please-source-run.json
@@ -146,7 +230,7 @@ jobs:
146230 git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
147231 case "$EVENT_NAME" in
148232 workflow_run) expected_main="$WORKFLOW_SHA" ;;
149- push ) expected_main="$CONTROL_SHA" ;;
233+ deployment ) expected_main="$CONTROL_SHA" ;;
150234 *) echo "Publish received an unsupported event." >&2; exit 1 ;;
151235 esac
152236 if [[ "$(git rev-parse refs/remotes/origin/main)" != "$expected_main" ]]; then
@@ -362,21 +446,37 @@ jobs:
362446 # without required reviewers and add COMETAPI_KEY before publishing a release.
363447 environment : live-smoke
364448 steps :
449+ - name : Reuse the successful bounded live smoke
450+ if : github.event_name == 'deployment'
451+ env :
452+ REUSE_LIVE_SMOKE : ${{ needs.verify.outputs.reuse-live-smoke }}
453+ shell : bash
454+ run : |
455+ set -euo pipefail
456+ if [[ "$REUSE_LIVE_SMOKE" != "true" ]]; then
457+ echo "The exact recovery run did not validate bounded live evidence." >&2
458+ exit 1
459+ fi
365460 - name : Check out the verified release tag
461+ if : github.event_name != 'deployment'
366462 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
367463 with :
368464 persist-credentials : false
369465 ref : ${{ needs.verify.outputs.release-commit }}
370466 - name : Set up Node.js 24
467+ if : github.event_name != 'deployment'
371468 uses : actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
372469 with :
373470 node-version : 24.x
374471 cache : npm
375472 - name : Install locked dependencies
473+ if : github.event_name != 'deployment'
376474 run : npm ci
377475 - name : Build the release tag
476+ if : github.event_name != 'deployment'
378477 run : npm run build
379478 - name : Run the bounded live smoke
479+ if : github.event_name != 'deployment'
380480 env :
381481 COMETAPI_KEY : ${{ secrets.COMETAPI_KEY }}
382482 COMETAPI_LIVE_SMOKE : " 1"
0 commit comments