Skip to content

Commit 8a80d82

Browse files
authored
Merge pull request #38 from cometapi-dev/agent/recover-v011-tag-deployment
fix: recover publish from immutable tag
2 parents 22c313d + aa4c3ca commit 8a80d82

5 files changed

Lines changed: 628 additions & 93 deletions

File tree

.github/workflows/publish.yml

Lines changed: 126 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
name: Publish
22

33
on:
4-
push:
5-
branches:
6-
- main
7-
paths:
8-
- .github/workflows/publish.yml
4+
deployment:
95
workflow_run:
106
workflows:
117
- Release Please
@@ -14,6 +10,7 @@ on:
1410

1511
permissions:
1612
actions: read
13+
checks: read
1714
contents: read
1815

1916
concurrency:
@@ -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"

RELEASING.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -295,15 +295,26 @@ The repository maintains four independently auditable workflows:
295295
[30469240186](https://github.com/cometapi-dev/cometapi-node/actions/runs/30469240186)
296296
validated the immutable tag, Release, and Release Please result, then failed
297297
before packing, live smoke, OIDC, or npm because the downloaded runtime result
298-
JSON was inside the workspace scanned by Prettier. One reviewed recovery merge
299-
temporarily adds an automatic `publish.yml`-only `main` push path. It accepts
300-
only human actor `tensornull`, exact Release Please run `30469181724` attempt
301-
1, release commit `c98b514227858cd183c781270a7f78f65b577e82`, a direct
302-
first-parent recovery merge, and the five recorded repair files. The workflow
303-
then checks out and rebuilds the immutable release commit, downloads runtime
304-
evidence under `runner.temp`, and uses the unchanged live, OIDC, artifact, and
305-
registry gates. The recovery trigger and constants must be removed in the
306-
post-release evidence PR; the `runner.temp` isolation remains permanent.
298+
JSON was inside the workspace scanned by Prettier. Recovery run
299+
[30471665743](https://github.com/cometapi-dev/cometapi-node/actions/runs/30471665743)
300+
then validated the exact Release Please result, immutable Release, package,
301+
and artifact and passed the only authorized three-request live smoke. Its npm
302+
job was rejected before runner allocation because a `main` push produces a
303+
`main` deployment while the protected npm environment accepts only `v*` tags.
304+
No OIDC token or npm mutation occurred in either failed run.
305+
306+
The replacement one-cycle recovery uses a human-created GitHub deployment for
307+
the existing immutable `v0.1.1` tag. It accepts only actor and triggering actor
308+
`tensornull`, exact release commit
309+
`c98b514227858cd183c781270a7f78f65b577e82`, Release Please run
310+
`30469181724` attempt 1, failed Publish run `30471665743` attempt 1, the exact
311+
first-parent control merge, and the recorded repair files. It revalidates the
312+
successful source verify job, artifact ID and digest, branch-policy failure,
313+
and the log evidence for exactly three sequential live requests. The new tag
314+
run repeats offline package and exact-artifact gates but does not spend another
315+
live request budget. The protected npm environment and OIDC gate remain
316+
unchanged. The deployment trigger and exact recovery constants must be removed
317+
in the post-release evidence PR; the `runner.temp` isolation remains permanent.
307318

308319
Third-party actions are pinned to full commit SHAs. Workflow permissions remain
309320
read-only except where a documented job requires more; `id-token: write` belongs

0 commit comments

Comments
 (0)