@rightkit/git 0.2.23 → 0.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/manifest.test.mjs CHANGED
@@ -32,7 +32,14 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
32
32
  const candidate = {
33
33
  ...valid,
34
34
  lanes: ["ci", "release-candidate"],
35
- candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "legion-unsigned", os: ["ubuntu-latest"] },
35
+ candidate: {
36
+ buildScript: "ci:candidate",
37
+ checkScript: "ci:candidate-check",
38
+ admissionScript: "release:admit",
39
+ stageSummaryScript: "release:stage-summary",
40
+ artifactRoot: "legion-unsigned",
41
+ os: ["ubuntu-latest"],
42
+ },
36
43
  };
37
44
  assert.equal(validateManifest(candidate).valid, true);
38
45
  assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, signWindows: true } }).valid, false);
@@ -53,6 +60,7 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
53
60
  candidate: {
54
61
  ...windowsCandidate.candidate,
55
62
  releaseChain: {
63
+ evidenceVerificationScript: "release:verify-evidence",
56
64
  windowsFinalizeScript: "release:finalize:win",
57
65
  installedQualificationScript: "release:qualify:installed",
58
66
  publishScript: "release:publish:qualified",
@@ -68,6 +76,103 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
68
76
  assert.equal(validateManifest({ ...releaseChain, candidate: { ...releaseChain.candidate, releaseChain: { ...releaseChain.candidate.releaseChain, publishSecrets: ["TOKEN", "TOKEN"] } } }).valid, false);
69
77
  });
70
78
 
79
+ // Part B fix: admissionScript & stageSummaryScript were moved OUT of the
80
+ // optional candidate.releaseChain object and onto candidate directly,
81
+ // because the admission job and the candidate job's stage-summary
82
+ // init/finalize steps are unconditional parts of the rendered
83
+ // release-candidate template — present for every release-candidate lane,
84
+ // signed or not. This test proves each is independently required with a
85
+ // specific, actionable error, and that the full releaseChain contract
86
+ // (now minus those two promoted fields) is validated key-by-key.
87
+ test("rust-hybrid release-candidate requires admissionScript & stageSummaryScript independent of signing", () => {
88
+ const base = {
89
+ ...VALID_MINIMAL,
90
+ profile: "rust-hybrid",
91
+ gate: { path: "scripts/gate.sh" },
92
+ lanes: ["ci", "release-candidate"],
93
+ candidate: {
94
+ buildScript: "ci:candidate",
95
+ checkScript: "ci:candidate-check",
96
+ admissionScript: "release:admit",
97
+ stageSummaryScript: "release:stage-summary",
98
+ artifactRoot: "widgets-unsigned",
99
+ os: ["ubuntu-latest"],
100
+ },
101
+ };
102
+ assert.equal(validateManifest(base).valid, true, JSON.stringify(validateManifest(base).errors));
103
+
104
+ const { admissionScript, ...withoutAdmission } = base.candidate;
105
+ const missingAdmission = validateManifest({ ...base, candidate: withoutAdmission });
106
+ assert.equal(missingAdmission.valid, false);
107
+ assert.ok(missingAdmission.errors.includes("candidate.admissionScript must be one safe package script name"));
108
+
109
+ const { stageSummaryScript, ...withoutStageSummary } = base.candidate;
110
+ const missingStageSummary = validateManifest({ ...base, candidate: withoutStageSummary });
111
+ assert.equal(missingStageSummary.valid, false);
112
+ assert.ok(missingStageSummary.errors.includes("candidate.stageSummaryScript must be one safe package script name"));
113
+
114
+ // Neither field is accepted inside releaseChain any more.
115
+ const windows = {
116
+ ...base,
117
+ matrix: { ...base.matrix, os: ["ubuntu-latest", "windows-2025"] },
118
+ candidate: { ...base.candidate, os: ["windows-2025"], signWindows: true },
119
+ };
120
+ const misplaced = validateManifest({
121
+ ...windows,
122
+ candidate: {
123
+ ...windows.candidate,
124
+ releaseChain: {
125
+ admissionScript: "release:admit",
126
+ evidenceVerificationScript: "release:verify-evidence",
127
+ windowsFinalizeScript: "release:finalize:win",
128
+ installedQualificationScript: "release:qualify:installed",
129
+ publishScript: "release:publish:qualified",
130
+ qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
131
+ },
132
+ },
133
+ });
134
+ assert.equal(misplaced.valid, false);
135
+ assert.ok(misplaced.errors.includes("unknown candidate.releaseChain field: admissionScript"));
136
+ });
137
+
138
+ // Part B fix: evidenceVerificationScript is the one new script that DOES
139
+ // stay inside releaseChain (it runs in the `publish` job, verifying
140
+ // aggregated same-run evidence before the publish step) — required
141
+ // whenever releaseChain is configured, with its own specific error.
142
+ test("candidate.releaseChain requires evidenceVerificationScript with a specific error", () => {
143
+ const windows = {
144
+ ...VALID_MINIMAL,
145
+ profile: "rust-hybrid",
146
+ gate: { path: "scripts/gate.sh" },
147
+ lanes: ["ci", "release-candidate"],
148
+ matrix: { ...VALID_MINIMAL.matrix, os: ["ubuntu-latest", "windows-2025"] },
149
+ candidate: {
150
+ buildScript: "ci:candidate",
151
+ checkScript: "ci:candidate-check",
152
+ admissionScript: "release:admit",
153
+ stageSummaryScript: "release:stage-summary",
154
+ artifactRoot: "widgets-unsigned",
155
+ os: ["windows-2025"],
156
+ signWindows: true,
157
+ releaseChain: {
158
+ windowsFinalizeScript: "release:finalize:win",
159
+ installedQualificationScript: "release:qualify:installed",
160
+ publishScript: "release:publish:qualified",
161
+ qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
162
+ },
163
+ },
164
+ };
165
+ const result = validateManifest(windows);
166
+ assert.equal(result.valid, false);
167
+ assert.ok(result.errors.includes("candidate.releaseChain.evidenceVerificationScript must be one safe package script name"));
168
+
169
+ const fixed = {
170
+ ...windows,
171
+ candidate: { ...windows.candidate, releaseChain: { ...windows.candidate.releaseChain, evidenceVerificationScript: "release:verify-evidence" } },
172
+ };
173
+ assert.equal(validateManifest(fixed).valid, true, JSON.stringify(validateManifest(fixed).errors));
174
+ });
175
+
71
176
  test("candidate targets permit native multi-architecture matrices independent of normal CI runners", () => {
72
177
  const multiArchitecture = {
73
178
  ...VALID_MINIMAL,
@@ -77,6 +182,8 @@ test("candidate targets permit native multi-architecture matrices independent of
77
182
  candidate: {
78
183
  buildScript: "ci:candidate",
79
184
  checkScript: "ci:candidate-check",
185
+ admissionScript: "release:admit",
186
+ stageSummaryScript: "release:stage-summary",
80
187
  artifactRoot: "widgets-unsigned",
81
188
  os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
82
189
  targets: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/git",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "description": "Public-repo-only GitHub Actions workflow layer: templates, visibility gate, sync/drift/adopt/uninstall for right-git.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -15,6 +15,9 @@
15
15
  "*.json"
16
16
  ],
17
17
  "sideEffects": false,
18
+ "scripts": {
19
+ "test": "node --test *.test.mjs"
20
+ },
18
21
  "publishConfig": {
19
22
  "registry": "https://registry.npmjs.org/",
20
23
  "access": "public"
@@ -24,7 +27,5 @@
24
27
  "url": "git+https://github.com/bogusyogi/claude.git",
25
28
  "directory": "tools/rightkit/packages/git"
26
29
  },
27
- "scripts": {
28
- "test": "node --test *.test.mjs"
29
- }
30
- }
30
+ "packageManager": "pnpm@11.24.0"
31
+ }
package/render.test.mjs CHANGED
@@ -101,7 +101,14 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
101
101
  lanes: ["ci", "release-candidate"],
102
102
  profile: "rust-hybrid",
103
103
  gate: { path: "scripts/gate.sh" },
104
- candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "widgets-unsigned", os: ["macos-latest", "windows-latest"] },
104
+ candidate: {
105
+ buildScript: "ci:candidate",
106
+ checkScript: "ci:candidate-check",
107
+ admissionScript: "release:admit",
108
+ stageSummaryScript: "release:stage-summary",
109
+ artifactRoot: "widgets-unsigned",
110
+ os: ["macos-latest", "windows-latest"],
111
+ },
105
112
  templateVersion: "1.2.0",
106
113
  };
107
114
  const content = renderTemplate(
@@ -110,6 +117,8 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
110
117
  );
111
118
  assert.match(content, /pnpm run ci:candidate/);
112
119
  assert.match(content, /pnpm run ci:candidate-check/);
120
+ assert.match(content, /pnpm run release:admit/);
121
+ assert.match(content, /pnpm run release:stage-summary/);
113
122
  assert.match(content, /Mozilla-Actions\/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba/);
114
123
  assert.match(content, /shared-key: widgets-rust/);
115
124
  assert.doesNotMatch(content, /bash \.\/scripts\/gate\.sh/);
@@ -122,6 +131,12 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
122
131
  assert.doesNotMatch(content, /rust-changes|RIGHT_GIT_RUST_CHANGED/);
123
132
  assert.doesNotMatch(content, /if: steps\.rust-changes/);
124
133
  assert.match(content, /uses: dtolnay\/rust-toolchain@[a-f0-9]{40}\n with:/);
134
+ // workflow_dispatch tag-last envelope: no windows-sign/macos-sign/release
135
+ // chain jobs render at all when signing is not configured.
136
+ assert.match(content, /^on:\n workflow_dispatch:/m);
137
+ assert.doesNotMatch(content, /^ {2}(?:windows-sign|macos-sign|installed-qualification|publish):\s*$/m);
138
+ assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
139
+ assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
125
140
  });
126
141
 
127
142
  test("rust-hybrid cache contract renders every declared Cargo workspace", () => {
@@ -151,7 +166,15 @@ test("rust-hybrid release candidate can opt into OIDC Windows signing without lo
151
166
  lanes: ["ci", "release-candidate"],
152
167
  profile: "rust-hybrid",
153
168
  gate: { path: "scripts/gate.sh" },
154
- candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "widgets-unsigned", os: ["windows-2025"], signWindows: true },
169
+ candidate: {
170
+ buildScript: "ci:candidate",
171
+ checkScript: "ci:candidate-check",
172
+ admissionScript: "release:admit",
173
+ stageSummaryScript: "release:stage-summary",
174
+ artifactRoot: "widgets-unsigned",
175
+ os: ["windows-2025"],
176
+ signWindows: true,
177
+ },
155
178
  matrix: { os: ["ubuntu-24.04", "windows-2025"], node: ["22.23.2"] },
156
179
  templateVersion: "1.3.0",
157
180
  };
@@ -160,16 +183,26 @@ test("rust-hybrid release candidate can opt into OIDC Windows signing without lo
160
183
  buildRenderContext(manifest, "/repos/widgets"),
161
184
  );
162
185
  assert.match(content, /environment: release/);
163
- assert.match(content, /windows-sign:\n needs: \[candidate\]\n if: \$\{\{ startsWith\(github\.ref, 'refs\/tags\/v'\) \|\| \(github\.event_name == 'workflow_dispatch' && inputs\.signed_qualification == true\) \}\}/);
186
+ // Tag-last contract: signing is gated on the admitted, same-run
187
+ // signed_qualification OUTPUT of the `admission` job — never on a ref/tag
188
+ // and never on the raw workflow_dispatch input directly.
189
+ assert.match(content, /windows-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
190
+ assert.doesNotMatch(content, /startsWith\(github\.ref, 'refs\/tags\/v'\)/);
164
191
  assert.match(content, /id-token: write/);
165
192
  assert.match(content, /azure\/login@[a-f0-9]{40}/);
166
193
  assert.match(content, /vars\.AZURE_CLIENT_ID/);
167
194
  assert.match(content, /right-release build --platform win --skip-checks/);
168
195
  assert.match(content, /Artifact Signing client hash mismatch/);
169
- assert.match(content, /RIGHT_GIT_SOURCE_REVISION=\$\(git rev-parse HEAD\)/);
196
+ // Every downstream job (admission, candidate, and windows-sign here) checks
197
+ // out the exact admitted revision by SHA, never by resolving HEAD after a
198
+ // ref-triggered checkout.
199
+ assert.match(content, /windows-sign:[\s\S]*?ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+persist-credentials: false/);
200
+ assert.doesNotMatch(content, /git rev-parse HEAD/);
170
201
  assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
171
202
  assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
172
203
  assert.doesNotMatch(content, /secrets\.|client-secret|private.key/i);
204
+ assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
205
+ assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
173
206
  });
174
207
 
175
208
  test("rust-hybrid release candidate can opt into protected macOS signing & notarization", () => {
@@ -178,7 +211,15 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
178
211
  lanes: ["ci", "release-candidate"],
179
212
  profile: "rust-hybrid",
180
213
  gate: { path: "scripts/gate.sh" },
181
- candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "widgets-unsigned", os: ["macos-15"], signMacos: true },
214
+ candidate: {
215
+ buildScript: "ci:candidate",
216
+ checkScript: "ci:candidate-check",
217
+ admissionScript: "release:admit",
218
+ stageSummaryScript: "release:stage-summary",
219
+ artifactRoot: "widgets-unsigned",
220
+ os: ["macos-15"],
221
+ signMacos: true,
222
+ },
182
223
  matrix: { os: ["ubuntu-24.04", "macos-15"], node: ["22.23.2"] },
183
224
  templateVersion: "1.3.0",
184
225
  };
@@ -187,17 +228,21 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
187
228
  buildRenderContext(manifest, "/repos/widgets"),
188
229
  );
189
230
  assert.match(content, /macos-sign:/);
190
- assert.match(content, /macos-sign:\n needs: \[candidate\]\n if: \$\{\{ startsWith\(github\.ref, 'refs\/tags\/v'\) \|\| \(github\.event_name == 'workflow_dispatch' && inputs\.signed_qualification == true\) \}\}/);
231
+ assert.match(content, /macos-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
232
+ assert.doesNotMatch(content, /startsWith\(github\.ref, 'refs\/tags\/v'\)/);
191
233
  assert.match(content, /environment: release/);
192
234
  assert.match(content, /secrets\.APPLE_CERTIFICATE_BASE64/);
193
235
  assert.match(content, /temporary keychain/i);
194
236
  assert.match(content, /right-release build --platform mac --skip-checks/);
195
237
  assert.match(content, /secrets\.APPLE_API_KEY_BASE64/);
196
- assert.match(content, /RIGHT_GIT_SOURCE_REVISION=%s\\n.*git rev-parse HEAD/);
238
+ assert.match(content, /macos-sign:[\s\S]*?ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+persist-credentials: false/);
239
+ assert.doesNotMatch(content, /git rev-parse HEAD/);
197
240
  assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
198
241
  assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
199
242
  assert.match(content, /security delete-keychain/);
200
243
  assert.doesNotMatch(content, /pull_request_target/);
244
+ assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
245
+ assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
201
246
  });
202
247
 
203
248
  test("rust-hybrid multi-architecture candidates bind every native target to exact artifacts and signing", () => {
@@ -210,6 +255,8 @@ test("rust-hybrid multi-architecture candidates bind every native target to exac
210
255
  candidate: {
211
256
  buildScript: "ci:candidate",
212
257
  checkScript: "ci:candidate-check",
258
+ admissionScript: "release:admit",
259
+ stageSummaryScript: "release:stage-summary",
213
260
  artifactRoot: "widgets-unsigned",
214
261
  os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
215
262
  targets: [
@@ -235,10 +282,15 @@ test("rust-hybrid multi-architecture candidates bind every native target to exac
235
282
  assert.doesNotMatch(content, /bash \.\/scripts\/gate\.sh/);
236
283
  assert.match(content, /windows-sign:[\s\S]*matrix:[\s\S]*windows-11-arm/);
237
284
  assert.match(content, /macos-sign:[\s\S]*matrix:[\s\S]*macos-15-intel/);
238
- assert.match(content, /name: widgets-unsigned-\$\{\{ matrix\.platform \}\}-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
239
- assert.match(content, /name: widgets-signed-windows-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
240
- assert.match(content, /name: widgets-signed-macos-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
241
- assert.equal((content.match(/RIGHT_GIT_RELEASE_ARCHITECTURE: \$\{\{ matrix\.architecture \}\}/g) ?? []).length, 4);
285
+ // Every artifact name is run- and identity-bound: <prefix>-<run_id>-<run_attempt>-<kind>-...-<version>-<source_revision>.
286
+ assert.match(content, /name: widgets-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-candidate-\$\{\{ matrix\.platform \}\}-\$\{\{ matrix\.architecture \}\}-\$\{\{ needs\.admission\.outputs\.version \}\}-\$\{\{ needs\.admission\.outputs\.source_revision \}\}/);
287
+ assert.match(content, /name: widgets-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-signed-windows-\$\{\{ matrix\.architecture \}\}-\$\{\{ needs\.admission\.outputs\.version \}\}-\$\{\{ needs\.admission\.outputs\.source_revision \}\}/);
288
+ assert.match(content, /name: widgets-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-signed-macos-\$\{\{ matrix\.architecture \}\}-\$\{\{ needs\.admission\.outputs\.version \}\}-\$\{\{ needs\.admission\.outputs\.source_revision \}\}/);
289
+ // Exact-count regression guard across the candidate job's build/check/
290
+ // stage-summary steps and the windows-sign/macos-sign stage-summary steps.
291
+ assert.equal((content.match(/RIGHT_GIT_RELEASE_ARCHITECTURE: \$\{\{ matrix\.architecture \}\}/g) ?? []).length, 10);
292
+ assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
293
+ assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
242
294
  });
243
295
 
244
296
  test("rust-hybrid release chain preserves signed candidate, installed qualification, then publish", () => {
@@ -250,11 +302,14 @@ test("rust-hybrid release chain preserves signed candidate, installed qualificat
250
302
  candidate: {
251
303
  buildScript: "ci:candidate",
252
304
  checkScript: "ci:candidate-check",
305
+ admissionScript: "release:admit",
306
+ stageSummaryScript: "release:stage-summary",
253
307
  artifactRoot: "widgets-unsigned",
254
308
  os: ["windows-2025", "macos-15"],
255
309
  signWindows: true,
256
310
  signMacos: true,
257
311
  releaseChain: {
312
+ evidenceVerificationScript: "release:verify-evidence",
258
313
  windowsFinalizeScript: "release:finalize:win",
259
314
  macosFinalizeScript: "release:finalize:mac",
260
315
  installedQualificationScript: "release:qualify:installed",
@@ -272,26 +327,202 @@ test("rust-hybrid release chain preserves signed candidate, installed qualificat
272
327
  );
273
328
  assert.match(content, /windows-sign:[\s\S]*pnpm run release:finalize:win/);
274
329
  assert.match(content, /macos-sign:[\s\S]*pnpm run release:finalize:mac/);
275
- assert.match(content, /signed_qualification:\n description: Run protected signing & installed qualification without publishing/);
276
- assert.match(content, /windows-sign:[\s\S]*inputs\.signed_qualification == true/);
277
- assert.match(content, /macos-sign:[\s\S]*inputs\.signed_qualification == true/);
278
- assert.match(content, /installed-qualification:\n needs: \[candidate, windows-sign\]/);
279
- assert.match(content, /installed-qualification:[\s\S]*inputs\.signed_qualification == true/);
330
+ // Tag-last contract: every gate reads the ADMITTED, same-run outputs of
331
+ // the `admission` job — never the raw workflow_dispatch input directly,
332
+ // and never a ref/tag.
333
+ assert.match(content, /signed_qualification:\n description: Run protected signing, installed qualification, & evidence verification/);
334
+ assert.doesNotMatch(content, /inputs\.signed_qualification == true/);
335
+ assert.doesNotMatch(content, /startsWith\(github\.ref, 'refs\/tags\/v'\)/);
336
+ assert.match(content, /windows-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
337
+ assert.match(content, /macos-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
338
+ assert.match(content, /installed-qualification:\n needs: \[admission, candidate, windows-sign\]/);
339
+ assert.match(content, /installed-qualification:[\s\S]*?if: \$\{\{ always\(\) && needs\.admission\.outputs\.signed_qualification == 'true' && needs\.candidate\.result == 'success' \}\}/);
280
340
  assert.match(content, /installed_qualification_failed/);
281
341
  assert.match(content, /shell: pwsh\n run: \|\n throw 'installed_qualification_failed \{ reason: prerequisites_absent, detail: "same-run Windows signing\/finalization did not succeed" \}'/);
282
342
  assert.doesNotMatch(content, /^ run: throw 'installed_qualification_failed .*detail: /m);
283
343
  assert.match(content, /pnpm run release:qualify:installed/);
284
344
  assert.match(content, /installed-qualification:[\s\S]*GH_TOKEN: \$\{\{ github\.token \}\}/);
285
- assert.match(content, /publish:\n needs: \[installed-qualification, macos-sign\]/);
286
- assert.match(content, /publish:[\s\S]*startsWith\(github\.ref, 'refs\/tags\/v'\)/);
287
- assert.doesNotMatch(content.slice(content.indexOf(" publish:")), /inputs\.signed_qualification/);
288
- assert.match(content, /needs\.macos-sign\.result == 'success'/);
345
+ assert.match(content, /publish:\n needs: \[admission, installed-qualification, macos-sign\]/);
346
+ assert.match(content, /publish:[\s\S]*?if: \$\{\{ needs\.admission\.outputs\.publish == 'true' && needs\.installed-qualification\.result == 'success' && needs\.macos-sign\.result == 'success' \}\}/);
347
+ const publishJobIndex = content.search(/^ {2}publish:\n {4}needs:/m);
348
+ assert.ok(publishJobIndex >= 0, "publish job must be present");
349
+ assert.doesNotMatch(content.slice(publishJobIndex), /inputs\.signed_qualification|inputs\.publish/);
289
350
  assert.match(content, /contents: write/);
351
+ // Publish is the sole creator of the tag/release: it must not require a
352
+ // pre-existing tag anywhere in its gate.
353
+ assert.doesNotMatch(content.slice(publishJobIndex), /refs\/tags/);
354
+ assert.match(content, /pnpm run release:verify-evidence/);
355
+ assert.match(content, /Verify aggregated release evidence[\s\S]*?pnpm run release:verify-evidence/);
290
356
  assert.match(content, /pnpm run release:publish:qualified/);
291
357
  assert.match(content, /CLOUDFLARE_API_TOKEN: \$\{\{ secrets\.CLOUDFLARE_API_TOKEN \}\}/);
292
- assert.equal((content.match(/RIGHT_GIT_SOURCE_REVISION=\$\(git rev-parse HEAD\)/g) ?? []).length, 3);
358
+ // The old ref-triggered "resolve HEAD after checkout" trick is gone
359
+ // entirely for the releaseChain path — every job instead checks out the
360
+ // exact admitted SHA from the admission job's own output.
361
+ assert.doesNotMatch(content, /git rev-parse HEAD/);
293
362
  assert.doesNotMatch(content, /RIGHT_GIT_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
363
+ // windows-sign, macos-sign, installed-qualification, publish all check out
364
+ // the admitted revision directly followed by persist-credentials: false;
365
+ // candidate does too but with a fetch-depth: 0 line between them (matched
366
+ // separately below), and admission checks out ${{ inputs.source_revision }}
367
+ // (it IS the admission job — there is no needs.admission yet).
368
+ assert.equal((content.match(/ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+persist-credentials: false/g) ?? []).length, 4, "windows-sign, macos-sign, installed-qualification, publish should all pin the admitted revision");
369
+ assert.match(content, /ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+fetch-depth: 0\n\s+persist-credentials: false/, "candidate must also pin the admitted revision");
370
+ assert.match(content, /ref: \$\{\{ inputs\.source_revision \}\}\n\s+fetch-depth: 0\n\s+persist-credentials: false/, "admission must check out the exact dispatch-supplied revision");
294
371
  assert.doesNotMatch(content.slice(0, content.indexOf(" windows-sign:")), /secrets\.|id-token: write|contents: write/);
372
+ // Downstream download-artifact names must match exactly what windows-sign/
373
+ // macos-sign uploaded (see the multi-architecture test) — this is the
374
+ // regression guard for the artifact-name mismatch bug.
375
+ const windowsSignedArtifact = "widgets-${{ github.run_id }}-${{ github.run_attempt }}-signed-windows-x86_64-${{ needs.admission.outputs.version }}-${{ needs.admission.outputs.source_revision }}";
376
+ const macosSignedArtifact = "widgets-${{ github.run_id }}-${{ github.run_attempt }}-signed-macos-arm64-${{ needs.admission.outputs.version }}-${{ needs.admission.outputs.source_revision }}";
377
+ assert.equal((content.match(new RegExp(windowsSignedArtifact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")) ?? []).length, 2, "windows-sign upload + installed-qualification/publish download must share one artifact name");
378
+ assert.ok(content.includes(macosSignedArtifact), "publish must download the exact macOS artifact macos-sign uploaded");
379
+ assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
380
+ assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
381
+ });
382
+
383
+ // Part E regression guard: every `run: <pm> run <script>` line in a
384
+ // rendered full release-chain workflow must name a script the manifest
385
+ // actually declares. This is the general form of the `pnpm run undefined`
386
+ // class of bug — it would also catch a typo'd or stale script reference
387
+ // even when it resolves to a real (but wrong) string, which the fail-closed
388
+ // undefined/null check in buildRenderContext cannot.
389
+ test("every 'run <script>' in a full release-chain workflow names a script the manifest declares", () => {
390
+ const manifest = {
391
+ ...FULL_MANIFEST,
392
+ lanes: ["ci", "release-candidate"],
393
+ profile: "rust-hybrid",
394
+ gate: { path: "scripts/gate.sh" },
395
+ candidate: {
396
+ buildScript: "ci:candidate",
397
+ checkScript: "ci:candidate-check",
398
+ admissionScript: "release:admit",
399
+ stageSummaryScript: "release:stage-summary",
400
+ artifactRoot: "widgets-unsigned",
401
+ os: ["windows-2025", "macos-15"],
402
+ signWindows: true,
403
+ signMacos: true,
404
+ releaseChain: {
405
+ evidenceVerificationScript: "release:verify-evidence",
406
+ windowsFinalizeScript: "release:finalize:win",
407
+ macosFinalizeScript: "release:finalize:mac",
408
+ installedQualificationScript: "release:qualify:installed",
409
+ publishScript: "release:publish:qualified",
410
+ qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
411
+ publishSecrets: ["CLOUDFLARE_API_TOKEN"],
412
+ },
413
+ },
414
+ matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
415
+ templateVersion: "1.4.0",
416
+ };
417
+ const content = renderTemplate(
418
+ fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
419
+ buildRenderContext(manifest, "/repos/widgets"),
420
+ );
421
+ const declaredScripts = new Set([
422
+ manifest.candidate.buildScript,
423
+ manifest.candidate.checkScript,
424
+ manifest.candidate.admissionScript,
425
+ manifest.candidate.stageSummaryScript,
426
+ manifest.candidate.releaseChain.evidenceVerificationScript,
427
+ manifest.candidate.releaseChain.windowsFinalizeScript,
428
+ manifest.candidate.releaseChain.macosFinalizeScript,
429
+ manifest.candidate.releaseChain.installedQualificationScript,
430
+ manifest.candidate.releaseChain.publishScript,
431
+ ]);
432
+ const runLines = content.match(/run: (?:pnpm|npm|yarn) run \S+/g) ?? [];
433
+ assert.ok(runLines.length > 0, "expected at least one package-script run line");
434
+ for (const line of runLines) {
435
+ const script = line.split(" run ").pop();
436
+ assert.ok(declaredScripts.has(script), `rendered step runs undeclared script: ${line}`);
437
+ }
438
+ });
439
+
440
+ // Downstream-consumer gap fix: the evidence verifier can only prove a stage
441
+ // ran and succeeded if that stage produces a stage summary AND the publish
442
+ // job actually downloads it. This test proves all four authoritative
443
+ // stages (candidate, windows-sign, macos-sign, installed-qualification)
444
+ // produce a summary, that installed-qualification wraps itself in
445
+ // init/finalize like every other stage, that publish downloads the
446
+ // complete same-run set into distinct subdirectories (no collisions
447
+ // between candidate's two platform legs), and that evidence is bound to
448
+ // this exact run (not merely version+source_revision) via RIGHT_GIT_RUN_ID/
449
+ // RIGHT_GIT_RUN_ATTEMPT on both the verification and publish steps.
450
+ test("release chain: installed-qualification has a stage summary and publish downloads the complete same-run evidence set", () => {
451
+ const manifest = {
452
+ ...FULL_MANIFEST,
453
+ lanes: ["ci", "release-candidate"],
454
+ profile: "rust-hybrid",
455
+ gate: { path: "scripts/gate.sh" },
456
+ candidate: {
457
+ buildScript: "ci:candidate",
458
+ checkScript: "ci:candidate-check",
459
+ admissionScript: "release:admit",
460
+ stageSummaryScript: "release:stage-summary",
461
+ artifactRoot: "widgets-unsigned",
462
+ os: ["windows-2025", "macos-15"],
463
+ signWindows: true,
464
+ signMacos: true,
465
+ releaseChain: {
466
+ evidenceVerificationScript: "release:verify-evidence",
467
+ windowsFinalizeScript: "release:finalize:win",
468
+ macosFinalizeScript: "release:finalize:mac",
469
+ installedQualificationScript: "release:qualify:installed",
470
+ publishScript: "release:publish:qualified",
471
+ qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
472
+ publishSecrets: ["CLOUDFLARE_API_TOKEN"],
473
+ },
474
+ },
475
+ matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
476
+ templateVersion: "1.4.0",
477
+ };
478
+ const content = renderTemplate(
479
+ fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
480
+ buildRenderContext(manifest, "/repos/widgets"),
481
+ );
482
+ const installedQualificationJob = content.slice(
483
+ content.search(/^ {2}installed-qualification:\n {4}needs:/m),
484
+ content.search(/^ {2}publish:\n {4}needs:/m),
485
+ );
486
+
487
+ // Point 1: installed-qualification wraps itself in stage-summary
488
+ // init/finalize like candidate/windows-sign/macos-sign, with the correct
489
+ // authoritative stage identifier, and finalize runs unconditionally.
490
+ assert.match(installedQualificationJob, /name: Initialize installed-qualification stage summary\n\s+run: pnpm run release:stage-summary\n\s+env:\n\s+RIGHT_GIT_STAGE_ACTION: init/);
491
+ assert.match(installedQualificationJob, /RIGHT_GIT_STAGE: installed-qualification/);
492
+ assert.match(installedQualificationJob, /name: Finalize installed-qualification stage summary\n\s+if: \$\{\{ always\(\) \}\}\n\s+run: pnpm run release:stage-summary\n\s+env:\n\s+RIGHT_GIT_STAGE_ACTION: finalize/);
493
+ assert.equal((installedQualificationJob.match(/RIGHT_GIT_STAGE: installed-qualification/g) ?? []).length, 2, "both init and finalize must carry the installed-qualification stage id");
494
+ assert.match(installedQualificationJob, /name: Upload installed-qualification stage summary\n\s+if: \$\{\{ always\(\) \}\}/);
495
+
496
+ const publishJob = content.slice(content.search(/^ {2}publish:\n {4}needs:/m));
497
+
498
+ // Point 2: publish downloads the complete same-run evidence set — both
499
+ // candidate legs (windows + macos) in distinct subdirectories, plus
500
+ // windows-sign, macos-sign, and installed-qualification summaries.
501
+ assert.match(publishJob, /Download same-run candidate \(Windows\) stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/candidate-windows/);
502
+ assert.match(publishJob, /Download same-run candidate \(macOS\) stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/candidate-macos/);
503
+ assert.match(publishJob, /Download same-run Windows signing stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/windows-sign/);
504
+ assert.match(publishJob, /Download same-run macOS signing stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/macos-sign/);
505
+ assert.match(publishJob, /Download same-run installed-qualification stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/installed-qualification/);
506
+ // Distinct subdirectories: no two downloads share a path.
507
+ const downloadPaths = [...publishJob.matchAll(/path: (\$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/\S+)/g)].map((m) => m[1]);
508
+ assert.equal(downloadPaths.length, 5);
509
+ assert.equal(new Set(downloadPaths).size, 5, "every stage-summary download must use a distinct subdirectory");
510
+
511
+ // Point 3: both the evidence-verification step and the publish step carry
512
+ // the full run-identity contract (RIGHT_GIT_STAGE_SUMMARY_ROOT, run id,
513
+ // run attempt) alongside version/source_revision — evidence is bound to
514
+ // THIS run, not merely reusable across runs of the same version+SHA.
515
+ const verifyStep = publishJob.slice(publishJob.indexOf("Verify aggregated release evidence"), publishJob.indexOf("Publish qualified signed release"));
516
+ assert.match(verifyStep, /RIGHT_GIT_STAGE_SUMMARY_ROOT: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries/);
517
+ assert.match(verifyStep, /RIGHT_GIT_RUN_ID: \$\{\{ github\.run_id \}\}/);
518
+ assert.match(verifyStep, /RIGHT_GIT_RUN_ATTEMPT: \$\{\{ github\.run_attempt \}\}/);
519
+ const publishStep = publishJob.slice(publishJob.indexOf("Publish qualified signed release"));
520
+ assert.match(publishStep, /RIGHT_GIT_STAGE_SUMMARY_ROOT: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries/);
521
+ assert.match(publishStep, /RIGHT_GIT_RUN_ID: \$\{\{ github\.run_id \}\}/);
522
+ assert.match(publishStep, /RIGHT_GIT_RUN_ATTEMPT: \$\{\{ github\.run_attempt \}\}/);
523
+
524
+ assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
525
+ assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
295
526
  });
296
527
 
297
528
  test("render determinism: same manifest renders byte-identical output every time", () => {
package/sync.test.mjs CHANGED
@@ -67,11 +67,14 @@ test("sync rejects a Membrane-like release chain without root @rightkit/release"
67
67
  candidate: {
68
68
  buildScript: "release:candidate:build",
69
69
  checkScript: "release:candidate:check",
70
+ admissionScript: "release:admit",
71
+ stageSummaryScript: "release:stage-summary",
70
72
  artifactRoot: "membrane-release-candidate",
71
73
  os: ["windows-2025", "macos-15"],
72
74
  signWindows: true,
73
75
  signMacos: true,
74
76
  releaseChain: {
77
+ evidenceVerificationScript: "release:verify-evidence",
75
78
  windowsFinalizeScript: "release:finalize:windows",
76
79
  macosFinalizeScript: "release:finalize:macos",
77
80
  installedQualificationScript: "release:qualify:installed",