@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/cli/right-git.mjs +0 -0
- package/manifest.mjs +281 -51
- package/manifest.schema.json +17 -2
- package/manifest.test.mjs +108 -1
- package/package.json +6 -5
- package/render.test.mjs +252 -21
- package/sync.test.mjs +3 -0
- package/templates/profiles/rust-hybrid/release-candidate.yml +105 -7
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: {
|
|
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.
|
|
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
|
-
"
|
|
28
|
-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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: \$\{\{
|
|
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, /
|
|
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
|
-
|
|
239
|
-
assert.match(content, /name: widgets-
|
|
240
|
-
assert.match(content, /name: widgets-signed-
|
|
241
|
-
assert.
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
assert.match(content, /
|
|
279
|
-
assert.
|
|
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]
|
|
287
|
-
|
|
288
|
-
assert.
|
|
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
|
-
|
|
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",
|