11name : Publish
22
33on :
4+ push :
5+ branches :
6+ - main
7+ paths :
8+ - .github/workflows/publish.yml
49 workflow_run :
510 workflows :
611 - Release Please
@@ -20,32 +25,100 @@ jobs:
2025 name : Verify the immutable release artifact
2126 if : >-
2227 vars.RELEASE_PLEASE_ENABLED == 'true' &&
23- github.event.workflow_run.conclusion == 'success' &&
24- github.event.workflow_run.event == 'push' &&
25- github.event.workflow_run.head_branch == 'main'
28+ ((github.event_name == 'workflow_run' &&
29+ github.event.workflow_run.conclusion == 'success' &&
30+ github.event.workflow_run.event == 'push' &&
31+ github.event.workflow_run.head_branch == 'main') ||
32+ (github.event_name == 'push' && github.ref == 'refs/heads/main'))
2633 runs-on : ubuntu-latest
2734 timeout-minutes : 30
35+ 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 }}
2839 outputs :
2940 artifact-name : ${{ steps.artifact-name.outputs.name }}
3041 dist-tag : ${{ steps.version.outputs.dist-tag }}
3142 release-commit : ${{ steps.trust.outputs.release-commit }}
3243 release-tag : ${{ steps.trust.outputs.release-tag }}
3344 version : ${{ steps.version.outputs.version }}
3445 steps :
35- - name : Check out the current main branch
46+ - name : Check out the workflow control commit
3647 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3748 with :
3849 fetch-depth : 0
3950 persist-credentials : false
40- ref : refs/heads/main
51+ ref : ${{ github.sha }}
52+ - name : Validate the one-cycle exact release recovery
53+ if : github.event_name == 'push'
54+ env :
55+ ACTOR : ${{ github.actor }}
56+ CHANGED_FILES : ${{ runner.temp }}/publish-recovery-files
57+ EVENT_AFTER : ${{ github.event.after }}
58+ EVENT_BEFORE : ${{ github.event.before }}
59+ EVENT_NAME : ${{ github.event_name }}
60+ EVENT_REF : ${{ github.ref }}
61+ MAIN_COMMIT : ${{ github.sha }}
62+ WORKFLOW_RUN_ATTEMPT : ${{ github.run_attempt }}
63+ shell : bash
64+ run : |
65+ set -euo pipefail
66+ if [[ "$(git rev-parse HEAD)" != "$MAIN_COMMIT" ]]; then
67+ echo "The recovery control checkout does not match the triggering SHA." >&2
68+ exit 1
69+ fi
70+ git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
71+ if [[ "$(git rev-parse refs/remotes/origin/main)" != "$MAIN_COMMIT" ]]; then
72+ echo "main moved after the publish recovery was triggered." >&2
73+ exit 1
74+ fi
75+ git diff --name-only "$EVENT_BEFORE" "$MAIN_COMMIT" > "$CHANGED_FILES"
76+ MAIN_FIRST_PARENT="$(git rev-parse "${MAIN_COMMIT}^1")" \
77+ node --input-type=module <<'EOF'
78+ import { readFileSync } from "node:fs";
79+ import { validatePublishRecoveryTrigger } from "./scripts/release-workflow-validation.mjs";
80+
81+ validatePublishRecoveryTrigger({
82+ actor: process.env.ACTOR,
83+ changedFiles: readFileSync(process.env.CHANGED_FILES, "utf8")
84+ .split("\n")
85+ .filter((file) => file !== ""),
86+ eventAfter: process.env.EVENT_AFTER,
87+ eventBefore: process.env.EVENT_BEFORE,
88+ eventName: process.env.EVENT_NAME,
89+ eventRef: process.env.EVENT_REF,
90+ mainCommit: process.env.MAIN_COMMIT,
91+ mainFirstParent: process.env.MAIN_FIRST_PARENT,
92+ sourceReleaseCommit: process.env.SOURCE_RELEASE_COMMIT,
93+ sourceRunAttempt: Number(process.env.SOURCE_RELEASE_RUN_ATTEMPT),
94+ sourceRunId: Number(process.env.SOURCE_RELEASE_RUN_ID),
95+ workflowRunAttempt: Number(process.env.WORKFLOW_RUN_ATTEMPT),
96+ });
97+ EOF
98+ - name : Check out the exact release commit
99+ uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
100+ with :
101+ fetch-depth : 0
102+ persist-credentials : false
103+ ref : ${{ env.SOURCE_RELEASE_COMMIT }}
104+ - name : Read the exact Release Please source run
105+ env :
106+ GH_TOKEN : ${{ github.token }}
107+ SOURCE_RUN_FILE : ${{ runner.temp }}/release-please-source-run.json
108+ shell : bash
109+ run : |
110+ set -euo pipefail
111+ gh api \
112+ "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RELEASE_RUN_ID}" \
113+ > "$SOURCE_RUN_FILE"
41114 - name : Download the exact Release Please result
42115 uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
43116 with :
44- name : release-please-result-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
45- path : release-please-result
117+ name : release-please-result-${{ env.SOURCE_RELEASE_RUN_ID }}-${{ env.SOURCE_RELEASE_RUN_ATTEMPT }}
118+ path : ${{ runner.temp }}/ release-please-result
46119 github-token : ${{ github.token }}
47120 repository : ${{ github.repository }}
48- run-id : ${{ github.event.workflow_run.id }}
121+ run-id : ${{ env.SOURCE_RELEASE_RUN_ID }}
49122 - name : Reject an untrusted Release Please workflow run
50123 id : trust
51124 env :
@@ -54,8 +127,13 @@ jobs:
54127 EXPECTED_REPOSITORY_URL : git+https://github.com/cometapi-dev/cometapi-node.git
55128 EXPECTED_WORKFLOW : Release Please
56129 EXPECTED_WORKFLOW_PATH : .github/workflows/release-please.yml
57- RELEASE_RESULT : release-please-result/result.json
58- WORKFLOW_SHA : ${{ github.event.workflow_run.head_sha }}
130+ CONTROL_SHA : ${{ github.sha }}
131+ EVENT_NAME : ${{ github.event_name }}
132+ RELEASE_RESULT : ${{ runner.temp }}/release-please-result/result.json
133+ SOURCE_RUN_FILE : ${{ runner.temp }}/release-please-source-run.json
134+ SOURCE_RUN_ATTEMPT : ${{ env.SOURCE_RELEASE_RUN_ATTEMPT }}
135+ SOURCE_RUN_ID : ${{ env.SOURCE_RELEASE_RUN_ID }}
136+ WORKFLOW_SHA : ${{ env.SOURCE_RELEASE_COMMIT }}
59137 shell : bash
60138 run : |
61139 set -euo pipefail
@@ -66,8 +144,13 @@ jobs:
66144 fi
67145
68146 git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
69- if [[ "$(git rev-parse refs/remotes/origin/main)" != "$WORKFLOW_SHA" ]]; then
70- echo "origin/main moved after the successful Release Please run." >&2
147+ case "$EVENT_NAME" in
148+ workflow_run) expected_main="$WORKFLOW_SHA" ;;
149+ push) expected_main="$CONTROL_SHA" ;;
150+ *) echo "Publish received an unsupported event." >&2; exit 1 ;;
151+ esac
152+ if [[ "$(git rev-parse refs/remotes/origin/main)" != "$expected_main" ]]; then
153+ echo "origin/main moved after the trusted publish event." >&2
71154 exit 1
72155 fi
73156
@@ -78,13 +161,54 @@ jobs:
78161 validateReleaseWorkflowRun,
79162 } from "./scripts/release-workflow-validation.mjs";
80163
81- const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
82- const run = validateReleaseWorkflowRun(event, {
164+ const sourceRun = JSON.parse(
165+ readFileSync(process.env.SOURCE_RUN_FILE, "utf8"),
166+ );
167+ const sourceEvent = {
168+ action: "completed",
169+ repository: { full_name: sourceRun.repository?.full_name },
170+ workflow_run: {
171+ conclusion: sourceRun.conclusion,
172+ event: sourceRun.event,
173+ head_branch: sourceRun.head_branch,
174+ head_repository: sourceRun.head_repository,
175+ head_sha: sourceRun.head_sha,
176+ id: sourceRun.id,
177+ name: sourceRun.name,
178+ path: sourceRun.path,
179+ run_attempt: sourceRun.run_attempt,
180+ },
181+ };
182+ const run = validateReleaseWorkflowRun(sourceEvent, {
83183 checkedOutSha: process.env.WORKFLOW_SHA,
84184 repository: process.env.EXPECTED_REPOSITORY,
85185 workflowName: process.env.EXPECTED_WORKFLOW,
86186 workflowPath: process.env.EXPECTED_WORKFLOW_PATH,
87187 });
188+ if (
189+ run.runId !== Number(process.env.SOURCE_RUN_ID) ||
190+ run.runAttempt !== Number(process.env.SOURCE_RUN_ATTEMPT)
191+ ) {
192+ throw new Error(
193+ "Release workflow source run ID or attempt changed before publication.",
194+ );
195+ }
196+ if (process.env.EVENT_NAME === "workflow_run") {
197+ const eventRun = validateReleaseWorkflowRun(
198+ JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")),
199+ {
200+ checkedOutSha: process.env.WORKFLOW_SHA,
201+ repository: process.env.EXPECTED_REPOSITORY,
202+ workflowName: process.env.EXPECTED_WORKFLOW,
203+ workflowPath: process.env.EXPECTED_WORKFLOW_PATH,
204+ },
205+ );
206+ if (JSON.stringify(eventRun) !== JSON.stringify(run)) {
207+ throw new Error(
208+ "Release workflow source run differs from the workflow_run event.",
209+ );
210+ }
211+ }
88212 const manifest = JSON.parse(readFileSync("package.json", "utf8"));
89213 if (
90214 manifest.repository?.type !== "git" ||
0 commit comments