@rightkit/git 0.2.23 → 0.2.25
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.mjs +334 -55
- package/manifest.schema.json +30 -2
- package/manifest.test.mjs +115 -1
- package/ownership-registration.mjs +33 -0
- package/ownership-registration.test.mjs +20 -0
- package/pack-smoke.test.mjs +8 -8
- package/package.json +1 -1
- package/render.test.mjs +436 -22
- package/sync.test.mjs +3 -0
- package/templates/ci.yml +1 -1
- package/templates/package-smoke.yml +1 -0
- package/templates/profiles/rust-hybrid/ci.yml +2 -0
- package/templates/profiles/rust-hybrid/release-candidate.yml +119 -7
- package/templates/publish-npm.yml +1 -0
- package/templates/publish-package-managers.yml +1 -0
- package/templates/qualification.yml +1 -0
- package/templates/release-candidate.yml +1 -0
package/render.test.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { renderTemplate, MANAGED_MARKER } from "./render.mjs";
|
|
7
|
-
import { buildRenderContext, LANE_FILENAMES, KNOWN_LANES } from "./manifest.mjs";
|
|
7
|
+
import { buildRenderContext, LANE_FILENAMES, KNOWN_LANES, validateManifest } from "./manifest.mjs";
|
|
8
8
|
|
|
9
9
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
10
10
|
const templatesDir = path.join(packageRoot, "templates");
|
|
@@ -73,6 +73,21 @@ test("rust-hybrid ci renders its pinned package manager and gate", () => {
|
|
|
73
73
|
assert.doesNotMatch(content, /ubuntu-latest|macos-latest/);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
test("every generated workflow profile runs ownership scanning independent of optional repository roots", () => {
|
|
77
|
+
const roots = ["engine", "apps/client"];
|
|
78
|
+
const manifest = { ...FULL_MANIFEST, ownership: { roots } };
|
|
79
|
+
const rendered = renderAllLanes(manifest, "C:/repos/coderight");
|
|
80
|
+
for (const [lane, content] of Object.entries(rendered)) {
|
|
81
|
+
assert.match(content, /right-release ownership --app \./, lane);
|
|
82
|
+
assert.match(content, /protected registry roots/, lane);
|
|
83
|
+
}
|
|
84
|
+
const rust = renderTemplate(fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/ci.yml"), "utf8"), buildRenderContext({ ...manifest, profile: "rust-hybrid", gate: { path: "scripts/gate.sh" } }, "C:/repos/coderight"));
|
|
85
|
+
assert.match(rust, /RightKit ownership \(protected registry roots\)/);
|
|
86
|
+
assert.match(rust, /pnpm exec right-release ownership --app \./);
|
|
87
|
+
const withoutRoots = renderAllLanes(FULL_MANIFEST, "C:/repos/widgets");
|
|
88
|
+
for (const [lane, content] of Object.entries(withoutRoots)) assert.match(content, /right-release ownership --app \./, lane);
|
|
89
|
+
});
|
|
90
|
+
|
|
76
91
|
test("ordinary CI templates cancel superseded runs on the same ref", () => {
|
|
77
92
|
const generic = renderAllLanes(FULL_MANIFEST, "/repos/widgets").ci;
|
|
78
93
|
assert.match(generic, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.ref \}\}/);
|
|
@@ -101,7 +116,14 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
|
|
|
101
116
|
lanes: ["ci", "release-candidate"],
|
|
102
117
|
profile: "rust-hybrid",
|
|
103
118
|
gate: { path: "scripts/gate.sh" },
|
|
104
|
-
candidate: {
|
|
119
|
+
candidate: {
|
|
120
|
+
buildScript: "ci:candidate",
|
|
121
|
+
checkScript: "ci:candidate-check",
|
|
122
|
+
admissionScript: "release:admit",
|
|
123
|
+
stageSummaryScript: "release:stage-summary",
|
|
124
|
+
artifactRoot: "widgets-unsigned",
|
|
125
|
+
os: ["macos-latest", "windows-latest"],
|
|
126
|
+
},
|
|
105
127
|
templateVersion: "1.2.0",
|
|
106
128
|
};
|
|
107
129
|
const content = renderTemplate(
|
|
@@ -110,6 +132,8 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
|
|
|
110
132
|
);
|
|
111
133
|
assert.match(content, /pnpm run ci:candidate/);
|
|
112
134
|
assert.match(content, /pnpm run ci:candidate-check/);
|
|
135
|
+
assert.match(content, /pnpm run release:admit/);
|
|
136
|
+
assert.match(content, /pnpm run release:stage-summary/);
|
|
113
137
|
assert.match(content, /Mozilla-Actions\/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba/);
|
|
114
138
|
assert.match(content, /shared-key: widgets-rust/);
|
|
115
139
|
assert.doesNotMatch(content, /bash \.\/scripts\/gate\.sh/);
|
|
@@ -122,6 +146,12 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
|
|
|
122
146
|
assert.doesNotMatch(content, /rust-changes|RIGHT_GIT_RUST_CHANGED/);
|
|
123
147
|
assert.doesNotMatch(content, /if: steps\.rust-changes/);
|
|
124
148
|
assert.match(content, /uses: dtolnay\/rust-toolchain@[a-f0-9]{40}\n with:/);
|
|
149
|
+
// workflow_dispatch tag-last envelope: no windows-sign/macos-sign/release
|
|
150
|
+
// chain jobs render at all when signing is not configured.
|
|
151
|
+
assert.match(content, /^on:\n workflow_dispatch:/m);
|
|
152
|
+
assert.doesNotMatch(content, /^ {2}(?:windows-sign|macos-sign|installed-qualification|publish):\s*$/m);
|
|
153
|
+
assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
|
|
154
|
+
assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
|
|
125
155
|
});
|
|
126
156
|
|
|
127
157
|
test("rust-hybrid cache contract renders every declared Cargo workspace", () => {
|
|
@@ -151,7 +181,15 @@ test("rust-hybrid release candidate can opt into OIDC Windows signing without lo
|
|
|
151
181
|
lanes: ["ci", "release-candidate"],
|
|
152
182
|
profile: "rust-hybrid",
|
|
153
183
|
gate: { path: "scripts/gate.sh" },
|
|
154
|
-
candidate: {
|
|
184
|
+
candidate: {
|
|
185
|
+
buildScript: "ci:candidate",
|
|
186
|
+
checkScript: "ci:candidate-check",
|
|
187
|
+
admissionScript: "release:admit",
|
|
188
|
+
stageSummaryScript: "release:stage-summary",
|
|
189
|
+
artifactRoot: "widgets-unsigned",
|
|
190
|
+
os: ["windows-2025"],
|
|
191
|
+
signWindows: true,
|
|
192
|
+
},
|
|
155
193
|
matrix: { os: ["ubuntu-24.04", "windows-2025"], node: ["22.23.2"] },
|
|
156
194
|
templateVersion: "1.3.0",
|
|
157
195
|
};
|
|
@@ -160,16 +198,26 @@ test("rust-hybrid release candidate can opt into OIDC Windows signing without lo
|
|
|
160
198
|
buildRenderContext(manifest, "/repos/widgets"),
|
|
161
199
|
);
|
|
162
200
|
assert.match(content, /environment: release/);
|
|
163
|
-
|
|
201
|
+
// Tag-last contract: signing is gated on the admitted, same-run
|
|
202
|
+
// signed_qualification OUTPUT of the `admission` job — never on a ref/tag
|
|
203
|
+
// and never on the raw workflow_dispatch input directly.
|
|
204
|
+
assert.match(content, /windows-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
|
|
205
|
+
assert.doesNotMatch(content, /startsWith\(github\.ref, 'refs\/tags\/v'\)/);
|
|
164
206
|
assert.match(content, /id-token: write/);
|
|
165
207
|
assert.match(content, /azure\/login@[a-f0-9]{40}/);
|
|
166
208
|
assert.match(content, /vars\.AZURE_CLIENT_ID/);
|
|
167
209
|
assert.match(content, /right-release build --platform win --skip-checks/);
|
|
168
210
|
assert.match(content, /Artifact Signing client hash mismatch/);
|
|
169
|
-
|
|
211
|
+
// Every downstream job (admission, candidate, and windows-sign here) checks
|
|
212
|
+
// out the exact admitted revision by SHA, never by resolving HEAD after a
|
|
213
|
+
// ref-triggered checkout.
|
|
214
|
+
assert.match(content, /windows-sign:[\s\S]*?ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+fetch-depth: 0\n\s+persist-credentials: false/);
|
|
215
|
+
assert.match(content, /git rev-parse HEAD.*inputs\.source_revision \|\| github\.sha/);
|
|
170
216
|
assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
|
|
171
217
|
assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
|
|
172
218
|
assert.doesNotMatch(content, /secrets\.|client-secret|private.key/i);
|
|
219
|
+
assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
|
|
220
|
+
assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
|
|
173
221
|
});
|
|
174
222
|
|
|
175
223
|
test("rust-hybrid release candidate can opt into protected macOS signing & notarization", () => {
|
|
@@ -178,7 +226,15 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
|
|
|
178
226
|
lanes: ["ci", "release-candidate"],
|
|
179
227
|
profile: "rust-hybrid",
|
|
180
228
|
gate: { path: "scripts/gate.sh" },
|
|
181
|
-
candidate: {
|
|
229
|
+
candidate: {
|
|
230
|
+
buildScript: "ci:candidate",
|
|
231
|
+
checkScript: "ci:candidate-check",
|
|
232
|
+
admissionScript: "release:admit",
|
|
233
|
+
stageSummaryScript: "release:stage-summary",
|
|
234
|
+
artifactRoot: "widgets-unsigned",
|
|
235
|
+
os: ["macos-15"],
|
|
236
|
+
signMacos: true,
|
|
237
|
+
},
|
|
182
238
|
matrix: { os: ["ubuntu-24.04", "macos-15"], node: ["22.23.2"] },
|
|
183
239
|
templateVersion: "1.3.0",
|
|
184
240
|
};
|
|
@@ -187,17 +243,21 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
|
|
|
187
243
|
buildRenderContext(manifest, "/repos/widgets"),
|
|
188
244
|
);
|
|
189
245
|
assert.match(content, /macos-sign:/);
|
|
190
|
-
assert.match(content, /macos-sign:\n needs: \[candidate\]\n if: \$\{\{
|
|
246
|
+
assert.match(content, /macos-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
|
|
247
|
+
assert.doesNotMatch(content, /startsWith\(github\.ref, 'refs\/tags\/v'\)/);
|
|
191
248
|
assert.match(content, /environment: release/);
|
|
192
249
|
assert.match(content, /secrets\.APPLE_CERTIFICATE_BASE64/);
|
|
193
250
|
assert.match(content, /temporary keychain/i);
|
|
194
251
|
assert.match(content, /right-release build --platform mac --skip-checks/);
|
|
195
252
|
assert.match(content, /secrets\.APPLE_API_KEY_BASE64/);
|
|
196
|
-
assert.match(content, /
|
|
253
|
+
assert.match(content, /macos-sign:[\s\S]*?ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+fetch-depth: 0\n\s+persist-credentials: false/);
|
|
254
|
+
assert.match(content, /git rev-parse HEAD.*inputs\.source_revision \|\| github\.sha/);
|
|
197
255
|
assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
|
|
198
256
|
assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
|
|
199
257
|
assert.match(content, /security delete-keychain/);
|
|
200
258
|
assert.doesNotMatch(content, /pull_request_target/);
|
|
259
|
+
assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
|
|
260
|
+
assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
|
|
201
261
|
});
|
|
202
262
|
|
|
203
263
|
test("rust-hybrid multi-architecture candidates bind every native target to exact artifacts and signing", () => {
|
|
@@ -210,6 +270,8 @@ test("rust-hybrid multi-architecture candidates bind every native target to exac
|
|
|
210
270
|
candidate: {
|
|
211
271
|
buildScript: "ci:candidate",
|
|
212
272
|
checkScript: "ci:candidate-check",
|
|
273
|
+
admissionScript: "release:admit",
|
|
274
|
+
stageSummaryScript: "release:stage-summary",
|
|
213
275
|
artifactRoot: "widgets-unsigned",
|
|
214
276
|
os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
|
|
215
277
|
targets: [
|
|
@@ -235,10 +297,15 @@ test("rust-hybrid multi-architecture candidates bind every native target to exac
|
|
|
235
297
|
assert.doesNotMatch(content, /bash \.\/scripts\/gate\.sh/);
|
|
236
298
|
assert.match(content, /windows-sign:[\s\S]*matrix:[\s\S]*windows-11-arm/);
|
|
237
299
|
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.
|
|
300
|
+
// Every artifact name is run- and identity-bound: <prefix>-<run_id>-<run_attempt>-<kind>-...-<version>-<source_revision>.
|
|
301
|
+
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 \}\}/);
|
|
302
|
+
assert.match(content, /name: widgets-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-signed-windows-\$\{\{ matrix\.architecture \}\}-\$\{\{ needs\.admission\.outputs\.version \}\}-\$\{\{ needs\.admission\.outputs\.source_revision \}\}/);
|
|
303
|
+
assert.match(content, /name: widgets-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}-signed-macos-\$\{\{ matrix\.architecture \}\}-\$\{\{ needs\.admission\.outputs\.version \}\}-\$\{\{ needs\.admission\.outputs\.source_revision \}\}/);
|
|
304
|
+
// Exact-count regression guard across the candidate job's build/check/
|
|
305
|
+
// stage-summary steps and the windows-sign/macos-sign stage-summary steps.
|
|
306
|
+
assert.equal((content.match(/RIGHT_GIT_RELEASE_ARCHITECTURE: \$\{\{ matrix\.architecture \}\}/g) ?? []).length, 10);
|
|
307
|
+
assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
|
|
308
|
+
assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
|
|
242
309
|
});
|
|
243
310
|
|
|
244
311
|
test("rust-hybrid release chain preserves signed candidate, installed qualification, then publish", () => {
|
|
@@ -250,11 +317,14 @@ test("rust-hybrid release chain preserves signed candidate, installed qualificat
|
|
|
250
317
|
candidate: {
|
|
251
318
|
buildScript: "ci:candidate",
|
|
252
319
|
checkScript: "ci:candidate-check",
|
|
320
|
+
admissionScript: "release:admit",
|
|
321
|
+
stageSummaryScript: "release:stage-summary",
|
|
253
322
|
artifactRoot: "widgets-unsigned",
|
|
254
323
|
os: ["windows-2025", "macos-15"],
|
|
255
324
|
signWindows: true,
|
|
256
325
|
signMacos: true,
|
|
257
326
|
releaseChain: {
|
|
327
|
+
evidenceVerificationScript: "release:verify-evidence",
|
|
258
328
|
windowsFinalizeScript: "release:finalize:win",
|
|
259
329
|
macosFinalizeScript: "release:finalize:mac",
|
|
260
330
|
installedQualificationScript: "release:qualify:installed",
|
|
@@ -272,26 +342,201 @@ test("rust-hybrid release chain preserves signed candidate, installed qualificat
|
|
|
272
342
|
);
|
|
273
343
|
assert.match(content, /windows-sign:[\s\S]*pnpm run release:finalize:win/);
|
|
274
344
|
assert.match(content, /macos-sign:[\s\S]*pnpm run release:finalize:mac/);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
assert.match(content, /
|
|
279
|
-
assert.
|
|
345
|
+
// Tag-last contract: every gate reads the ADMITTED, same-run outputs of
|
|
346
|
+
// the `admission` job — never the raw workflow_dispatch input directly,
|
|
347
|
+
// and never a ref/tag.
|
|
348
|
+
assert.match(content, /signed_qualification:\n description: Run protected signing, installed qualification, & evidence verification/);
|
|
349
|
+
assert.doesNotMatch(content, /inputs\.signed_qualification == true/);
|
|
350
|
+
assert.doesNotMatch(content, /startsWith\(github\.ref, 'refs\/tags\/v'\)/);
|
|
351
|
+
assert.match(content, /windows-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
|
|
352
|
+
assert.match(content, /macos-sign:\n needs: \[admission, candidate\]\n if: \$\{\{ needs\.admission\.outputs\.signed_qualification == 'true' \}\}/);
|
|
353
|
+
assert.match(content, /installed-qualification:\n needs: \[admission, candidate, windows-sign\]/);
|
|
354
|
+
assert.match(content, /installed-qualification:[\s\S]*?if: \$\{\{ always\(\) && needs\.admission\.outputs\.signed_qualification == 'true' && needs\.candidate\.result == 'success' \}\}/);
|
|
280
355
|
assert.match(content, /installed_qualification_failed/);
|
|
281
356
|
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
357
|
assert.doesNotMatch(content, /^ run: throw 'installed_qualification_failed .*detail: /m);
|
|
283
358
|
assert.match(content, /pnpm run release:qualify:installed/);
|
|
284
359
|
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.
|
|
360
|
+
assert.match(content, /publish:\n needs: \[admission, installed-qualification, macos-sign\]/);
|
|
361
|
+
assert.match(content, /publish:[\s\S]*?if: \$\{\{ needs\.admission\.outputs\.publish == 'true' && needs\.installed-qualification\.result == 'success' && needs\.macos-sign\.result == 'success' \}\}/);
|
|
362
|
+
const publishJobIndex = content.search(/^ {2}publish:\n {4}needs:/m);
|
|
363
|
+
assert.ok(publishJobIndex >= 0, "publish job must be present");
|
|
364
|
+
assert.doesNotMatch(content.slice(publishJobIndex), /inputs\.signed_qualification|inputs\.publish/);
|
|
289
365
|
assert.match(content, /contents: write/);
|
|
366
|
+
// Publish is the sole creator of the tag/release: it must not require a
|
|
367
|
+
// pre-existing tag anywhere in its gate.
|
|
368
|
+
assert.doesNotMatch(content.slice(publishJobIndex), /refs\/tags/);
|
|
369
|
+
assert.match(content, /pnpm run release:verify-evidence/);
|
|
370
|
+
assert.match(content, /Verify aggregated release evidence[\s\S]*?pnpm run release:verify-evidence/);
|
|
290
371
|
assert.match(content, /pnpm run release:publish:qualified/);
|
|
291
372
|
assert.match(content, /CLOUDFLARE_API_TOKEN: \$\{\{ secrets\.CLOUDFLARE_API_TOKEN \}\}/);
|
|
292
|
-
|
|
373
|
+
// Ownership scan binds to the source revision input before admission emits
|
|
374
|
+
// that same revision for every downstream job.
|
|
375
|
+
assert.match(content, /git rev-parse HEAD.*inputs\.source_revision \|\| github\.sha/);
|
|
293
376
|
assert.doesNotMatch(content, /RIGHT_GIT_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
|
|
377
|
+
// candidate, windows-sign, macos-sign, installed-qualification and publish all
|
|
378
|
+
// check out the admitted revision with full history; admission checks out
|
|
379
|
+
// ${{ inputs.source_revision }} (it IS the admission job — there is no
|
|
380
|
+
// needs.admission yet). Full history is load-bearing, not cosmetic: see
|
|
381
|
+
// "every job checking out an admitted revision fetches full history".
|
|
382
|
+
assert.equal((content.match(/ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+fetch-depth: 0\n\s+persist-credentials: false/g) ?? []).length, 5, "candidate, windows-sign, macos-sign, installed-qualification & publish should all pin the admitted revision with full history");
|
|
383
|
+
assert.doesNotMatch(content, /ref: \$\{\{ needs\.admission\.outputs\.source_revision \}\}\n\s+persist-credentials: false/, "no admitted-revision checkout may be shallow");
|
|
384
|
+
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
385
|
assert.doesNotMatch(content.slice(0, content.indexOf(" windows-sign:")), /secrets\.|id-token: write|contents: write/);
|
|
386
|
+
// Downstream download-artifact names must match exactly what windows-sign/
|
|
387
|
+
// macos-sign uploaded (see the multi-architecture test) — this is the
|
|
388
|
+
// regression guard for the artifact-name mismatch bug.
|
|
389
|
+
const windowsSignedArtifact = "widgets-${{ github.run_id }}-${{ github.run_attempt }}-signed-windows-x86_64-${{ needs.admission.outputs.version }}-${{ needs.admission.outputs.source_revision }}";
|
|
390
|
+
const macosSignedArtifact = "widgets-${{ github.run_id }}-${{ github.run_attempt }}-signed-macos-arm64-${{ needs.admission.outputs.version }}-${{ needs.admission.outputs.source_revision }}";
|
|
391
|
+
assert.equal((content.match(new RegExp(windowsSignedArtifact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")) ?? []).length, 2, "windows-sign upload + installed-qualification/publish download must share one artifact name");
|
|
392
|
+
assert.ok(content.includes(macosSignedArtifact), "publish must download the exact macOS artifact macos-sign uploaded");
|
|
393
|
+
assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
|
|
394
|
+
assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Part E regression guard: every `run: <pm> run <script>` line in a
|
|
398
|
+
// rendered full release-chain workflow must name a script the manifest
|
|
399
|
+
// actually declares. This is the general form of the `pnpm run undefined`
|
|
400
|
+
// class of bug — it would also catch a typo'd or stale script reference
|
|
401
|
+
// even when it resolves to a real (but wrong) string, which the fail-closed
|
|
402
|
+
// undefined/null check in buildRenderContext cannot.
|
|
403
|
+
test("every 'run <script>' in a full release-chain workflow names a script the manifest declares", () => {
|
|
404
|
+
const manifest = {
|
|
405
|
+
...FULL_MANIFEST,
|
|
406
|
+
lanes: ["ci", "release-candidate"],
|
|
407
|
+
profile: "rust-hybrid",
|
|
408
|
+
gate: { path: "scripts/gate.sh" },
|
|
409
|
+
candidate: {
|
|
410
|
+
buildScript: "ci:candidate",
|
|
411
|
+
checkScript: "ci:candidate-check",
|
|
412
|
+
admissionScript: "release:admit",
|
|
413
|
+
stageSummaryScript: "release:stage-summary",
|
|
414
|
+
artifactRoot: "widgets-unsigned",
|
|
415
|
+
os: ["windows-2025", "macos-15"],
|
|
416
|
+
signWindows: true,
|
|
417
|
+
signMacos: true,
|
|
418
|
+
releaseChain: {
|
|
419
|
+
evidenceVerificationScript: "release:verify-evidence",
|
|
420
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
421
|
+
macosFinalizeScript: "release:finalize:mac",
|
|
422
|
+
installedQualificationScript: "release:qualify:installed",
|
|
423
|
+
publishScript: "release:publish:qualified",
|
|
424
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
425
|
+
publishSecrets: ["CLOUDFLARE_API_TOKEN"],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
|
|
429
|
+
templateVersion: "1.4.0",
|
|
430
|
+
};
|
|
431
|
+
const content = renderTemplate(
|
|
432
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
433
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
434
|
+
);
|
|
435
|
+
const declaredScripts = new Set([
|
|
436
|
+
manifest.candidate.buildScript,
|
|
437
|
+
manifest.candidate.checkScript,
|
|
438
|
+
manifest.candidate.admissionScript,
|
|
439
|
+
manifest.candidate.stageSummaryScript,
|
|
440
|
+
manifest.candidate.releaseChain.evidenceVerificationScript,
|
|
441
|
+
manifest.candidate.releaseChain.windowsFinalizeScript,
|
|
442
|
+
manifest.candidate.releaseChain.macosFinalizeScript,
|
|
443
|
+
manifest.candidate.releaseChain.installedQualificationScript,
|
|
444
|
+
manifest.candidate.releaseChain.publishScript,
|
|
445
|
+
]);
|
|
446
|
+
const runLines = content.match(/run: (?:pnpm|npm|yarn) run \S+/g) ?? [];
|
|
447
|
+
assert.ok(runLines.length > 0, "expected at least one package-script run line");
|
|
448
|
+
for (const line of runLines) {
|
|
449
|
+
const script = line.split(" run ").pop();
|
|
450
|
+
assert.ok(declaredScripts.has(script), `rendered step runs undeclared script: ${line}`);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Downstream-consumer gap fix: the evidence verifier can only prove a stage
|
|
455
|
+
// ran and succeeded if that stage produces a stage summary AND the publish
|
|
456
|
+
// job actually downloads it. This test proves all four authoritative
|
|
457
|
+
// stages (candidate, windows-sign, macos-sign, installed-qualification)
|
|
458
|
+
// produce a summary, that installed-qualification wraps itself in
|
|
459
|
+
// init/finalize like every other stage, that publish downloads the
|
|
460
|
+
// complete same-run set into distinct subdirectories (no collisions
|
|
461
|
+
// between candidate's two platform legs), and that evidence is bound to
|
|
462
|
+
// this exact run (not merely version+source_revision) via RIGHT_GIT_RUN_ID/
|
|
463
|
+
// RIGHT_GIT_RUN_ATTEMPT on both the verification and publish steps.
|
|
464
|
+
test("release chain: installed-qualification has a stage summary and publish downloads the complete same-run evidence set", () => {
|
|
465
|
+
const manifest = {
|
|
466
|
+
...FULL_MANIFEST,
|
|
467
|
+
lanes: ["ci", "release-candidate"],
|
|
468
|
+
profile: "rust-hybrid",
|
|
469
|
+
gate: { path: "scripts/gate.sh" },
|
|
470
|
+
candidate: {
|
|
471
|
+
buildScript: "ci:candidate",
|
|
472
|
+
checkScript: "ci:candidate-check",
|
|
473
|
+
admissionScript: "release:admit",
|
|
474
|
+
stageSummaryScript: "release:stage-summary",
|
|
475
|
+
artifactRoot: "widgets-unsigned",
|
|
476
|
+
os: ["windows-2025", "macos-15"],
|
|
477
|
+
signWindows: true,
|
|
478
|
+
signMacos: true,
|
|
479
|
+
releaseChain: {
|
|
480
|
+
evidenceVerificationScript: "release:verify-evidence",
|
|
481
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
482
|
+
macosFinalizeScript: "release:finalize:mac",
|
|
483
|
+
installedQualificationScript: "release:qualify:installed",
|
|
484
|
+
publishScript: "release:publish:qualified",
|
|
485
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
486
|
+
publishSecrets: ["CLOUDFLARE_API_TOKEN"],
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
|
|
490
|
+
templateVersion: "1.4.0",
|
|
491
|
+
};
|
|
492
|
+
const content = renderTemplate(
|
|
493
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
494
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
495
|
+
);
|
|
496
|
+
const installedQualificationJob = content.slice(
|
|
497
|
+
content.search(/^ {2}installed-qualification:\n {4}needs:/m),
|
|
498
|
+
content.search(/^ {2}publish:\n {4}needs:/m),
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Point 1: installed-qualification wraps itself in stage-summary
|
|
502
|
+
// init/finalize like candidate/windows-sign/macos-sign, with the correct
|
|
503
|
+
// authoritative stage identifier, and finalize runs unconditionally.
|
|
504
|
+
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/);
|
|
505
|
+
assert.match(installedQualificationJob, /RIGHT_GIT_STAGE: installed-qualification/);
|
|
506
|
+
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/);
|
|
507
|
+
assert.equal((installedQualificationJob.match(/RIGHT_GIT_STAGE: installed-qualification/g) ?? []).length, 2, "both init and finalize must carry the installed-qualification stage id");
|
|
508
|
+
assert.match(installedQualificationJob, /name: Upload installed-qualification stage summary\n\s+if: \$\{\{ always\(\) \}\}/);
|
|
509
|
+
|
|
510
|
+
const publishJob = content.slice(content.search(/^ {2}publish:\n {4}needs:/m));
|
|
511
|
+
|
|
512
|
+
// Point 2: publish downloads the complete same-run evidence set — both
|
|
513
|
+
// candidate legs (windows + macos) in distinct subdirectories, plus
|
|
514
|
+
// windows-sign, macos-sign, and installed-qualification summaries.
|
|
515
|
+
assert.match(publishJob, /Download same-run candidate \(Windows\) stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/candidate-windows/);
|
|
516
|
+
assert.match(publishJob, /Download same-run candidate \(macOS\) stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/candidate-macos/);
|
|
517
|
+
assert.match(publishJob, /Download same-run Windows signing stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/windows-sign/);
|
|
518
|
+
assert.match(publishJob, /Download same-run macOS signing stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/macos-sign/);
|
|
519
|
+
assert.match(publishJob, /Download same-run installed-qualification stage summary[\s\S]*?path: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/installed-qualification/);
|
|
520
|
+
// Distinct subdirectories: no two downloads share a path.
|
|
521
|
+
const downloadPaths = [...publishJob.matchAll(/path: (\$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries\/\S+)/g)].map((m) => m[1]);
|
|
522
|
+
assert.equal(downloadPaths.length, 5);
|
|
523
|
+
assert.equal(new Set(downloadPaths).size, 5, "every stage-summary download must use a distinct subdirectory");
|
|
524
|
+
|
|
525
|
+
// Point 3: both the evidence-verification step and the publish step carry
|
|
526
|
+
// the full run-identity contract (RIGHT_GIT_STAGE_SUMMARY_ROOT, run id,
|
|
527
|
+
// run attempt) alongside version/source_revision — evidence is bound to
|
|
528
|
+
// THIS run, not merely reusable across runs of the same version+SHA.
|
|
529
|
+
const verifyStep = publishJob.slice(publishJob.indexOf("Verify aggregated release evidence"), publishJob.indexOf("Publish qualified signed release"));
|
|
530
|
+
assert.match(verifyStep, /RIGHT_GIT_STAGE_SUMMARY_ROOT: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries/);
|
|
531
|
+
assert.match(verifyStep, /RIGHT_GIT_RUN_ID: \$\{\{ github\.run_id \}\}/);
|
|
532
|
+
assert.match(verifyStep, /RIGHT_GIT_RUN_ATTEMPT: \$\{\{ github\.run_attempt \}\}/);
|
|
533
|
+
const publishStep = publishJob.slice(publishJob.indexOf("Publish qualified signed release"));
|
|
534
|
+
assert.match(publishStep, /RIGHT_GIT_STAGE_SUMMARY_ROOT: \$\{\{ runner\.temp \}\}\/right-git-release\/stage-summaries/);
|
|
535
|
+
assert.match(publishStep, /RIGHT_GIT_RUN_ID: \$\{\{ github\.run_id \}\}/);
|
|
536
|
+
assert.match(publishStep, /RIGHT_GIT_RUN_ATTEMPT: \$\{\{ github\.run_attempt \}\}/);
|
|
537
|
+
|
|
538
|
+
assert.doesNotMatch(content, /run: (?:pnpm|npm|yarn) run undefined\b/);
|
|
539
|
+
assert.doesNotMatch(content, /\$\{\{\s*github\.output\s*\}\}/);
|
|
295
540
|
});
|
|
296
541
|
|
|
297
542
|
test("render determinism: same manifest renders byte-identical output every time", () => {
|
|
@@ -406,3 +651,172 @@ test("renderTemplate normalizes an empty final release-chain fragment to one new
|
|
|
406
651
|
assert.equal(out, "jobs:\n");
|
|
407
652
|
assert.equal((out.match(/\n/g) ?? []).length, 1);
|
|
408
653
|
});
|
|
654
|
+
|
|
655
|
+
// Tag-last has no tag to check out, so every stage checks out one exact admitted
|
|
656
|
+
// commit SHA and HEAD is always detached. @rightkit/release only exempts a
|
|
657
|
+
// detached HEAD when it can prove the commit is reachable from the dispatched
|
|
658
|
+
// remote branch — and proving reachability needs history plus the
|
|
659
|
+
// remote-tracking ref. A default shallow checkout has neither, so the signing,
|
|
660
|
+
// qualification, and publish jobs would be refused. Every job that checks out an
|
|
661
|
+
// admitted revision must therefore ask for full history.
|
|
662
|
+
test("every job checking out an admitted revision fetches full history", () => {
|
|
663
|
+
const manifest = {
|
|
664
|
+
...FULL_MANIFEST,
|
|
665
|
+
lanes: ["ci", "release-candidate"],
|
|
666
|
+
profile: "rust-hybrid",
|
|
667
|
+
gate: { path: "scripts/gate.sh" },
|
|
668
|
+
candidate: {
|
|
669
|
+
buildScript: "ci:candidate",
|
|
670
|
+
checkScript: "ci:candidate-check",
|
|
671
|
+
admissionScript: "release:admission",
|
|
672
|
+
stageSummaryScript: "release:stage-summary",
|
|
673
|
+
artifactRoot: "widgets-unsigned",
|
|
674
|
+
os: ["windows-2025", "macos-15"],
|
|
675
|
+
signWindows: true,
|
|
676
|
+
signMacos: true,
|
|
677
|
+
releaseChain: {
|
|
678
|
+
evidenceVerificationScript: "release:verify:evidence",
|
|
679
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
680
|
+
macosFinalizeScript: "release:finalize:mac",
|
|
681
|
+
installedQualificationScript: "release:qualify:installed",
|
|
682
|
+
publishScript: "release:publish:qualified",
|
|
683
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
684
|
+
publishSecrets: ["CLOUDFLARE_API_TOKEN"],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
|
|
688
|
+
templateVersion: "1.9.0",
|
|
689
|
+
};
|
|
690
|
+
const content = renderTemplate(
|
|
691
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
692
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
693
|
+
);
|
|
694
|
+
const checkouts = content
|
|
695
|
+
.split(/^ {6}- uses: actions\/checkout@/m)
|
|
696
|
+
.slice(1)
|
|
697
|
+
.map((step) => step.split(/^ {6}- /m)[0]);
|
|
698
|
+
assert.ok(checkouts.length >= 6, `expected every stage to check out; found ${checkouts.length}`);
|
|
699
|
+
for (const step of checkouts) {
|
|
700
|
+
if (!/ref: \$\{\{ (needs\.admission\.outputs\.source_revision|inputs\.source_revision) \}\}/.test(step)) continue;
|
|
701
|
+
assert.match(step, /fetch-depth: 0/, `a checkout of an admitted revision must fetch full history:\n${step}`);
|
|
702
|
+
}
|
|
703
|
+
// The four downstream stages specifically — these were the ones shipping shallow.
|
|
704
|
+
for (const job of ["windows-sign", "macos-sign", "installed-qualification", "publish"]) {
|
|
705
|
+
const body = content.split(new RegExp(`^ ${job}:$`, "m"))[1] ?? "";
|
|
706
|
+
const checkout = body.split(/^ {6}- uses: actions\/checkout@/m)[1]?.split(/^ {6}- /m)[0] ?? "";
|
|
707
|
+
assert.match(checkout, /fetch-depth: 0/, `${job} must check out with full history`);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Rule 3 (release workflows have a dry run): a boolean dry_run input, runnable
|
|
712
|
+
// from a branch, must thread through admission so packaging/signing/qualification
|
|
713
|
+
// run while publication is skipped and artifacts carry a dry-run suffix.
|
|
714
|
+
test("release-candidate template exposes a dry_run input threaded through admission", () => {
|
|
715
|
+
const manifest = {
|
|
716
|
+
...FULL_MANIFEST,
|
|
717
|
+
lanes: ["ci", "release-candidate"],
|
|
718
|
+
profile: "rust-hybrid",
|
|
719
|
+
gate: { path: "scripts/gate.sh" },
|
|
720
|
+
candidate: {
|
|
721
|
+
buildScript: "ci:candidate",
|
|
722
|
+
checkScript: "ci:candidate-check",
|
|
723
|
+
admissionScript: "release:admission",
|
|
724
|
+
stageSummaryScript: "release:stage-summary",
|
|
725
|
+
artifactRoot: "widgets-unsigned",
|
|
726
|
+
os: ["windows-2025", "macos-15"],
|
|
727
|
+
signWindows: true,
|
|
728
|
+
signMacos: true,
|
|
729
|
+
},
|
|
730
|
+
matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
|
|
731
|
+
templateVersion: "1.9.0",
|
|
732
|
+
};
|
|
733
|
+
const content = renderTemplate(
|
|
734
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
735
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
736
|
+
);
|
|
737
|
+
assert.match(content, /^ dry_run:\n description: .*\n required: false\n type: boolean\n default: false$/m);
|
|
738
|
+
assert.match(content, /dry_run: \$\{\{ steps\.admit\.outputs\.dry_run \}\}/);
|
|
739
|
+
assert.match(content, /artifact_suffix: \$\{\{ steps\.admit\.outputs\.artifact_suffix \}\}/);
|
|
740
|
+
assert.match(content, /RIGHT_GIT_DRY_RUN: \$\{\{ inputs\.dry_run \}\}/);
|
|
741
|
+
// The envelope only pins refs/heads/main and first-attempt for a real dispatch.
|
|
742
|
+
assert.match(content, /if \[\[ "\$\{\{ inputs\.dry_run \}\}" == "true" \]\]; then\n \[\[ "\$GITHUB_REF" == refs\/heads\/\* \]\]/);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Correction 11: Windows signing, macOS signing, installed qualification and
|
|
746
|
+
// publication all shared one `release` environment, so no signing/publication
|
|
747
|
+
// grant separation existed. The split is opt-in per product because the secrets
|
|
748
|
+
// live in the existing environment until an operator moves them.
|
|
749
|
+
function environmentManifest(environments) {
|
|
750
|
+
const manifest = {
|
|
751
|
+
schemaVersion: 1,
|
|
752
|
+
compile: "github-actions-only",
|
|
753
|
+
lanes: ["ci", "release-candidate"],
|
|
754
|
+
packageManager: "pnpm@11.24.0",
|
|
755
|
+
profile: "rust-hybrid",
|
|
756
|
+
gate: { path: "scripts/gate.sh" },
|
|
757
|
+
candidate: {
|
|
758
|
+
buildScript: "ci:candidate",
|
|
759
|
+
checkScript: "ci:candidate-check",
|
|
760
|
+
admissionScript: "release:admit",
|
|
761
|
+
stageSummaryScript: "release:stage-summary",
|
|
762
|
+
artifactRoot: "widgets-unsigned",
|
|
763
|
+
os: ["windows-2025", "macos-15"],
|
|
764
|
+
targets: [
|
|
765
|
+
{ os: "windows-2025", platform: "windows", architecture: "x86_64" },
|
|
766
|
+
{ os: "macos-15", platform: "macos", architecture: "arm64" },
|
|
767
|
+
],
|
|
768
|
+
signWindows: true,
|
|
769
|
+
signMacos: true,
|
|
770
|
+
releaseChain: {
|
|
771
|
+
windowsFinalizeScript: "release:finalize-windows",
|
|
772
|
+
macosFinalizeScript: "release:finalize-macos",
|
|
773
|
+
installedQualificationScript: "release:qualify-installed",
|
|
774
|
+
publishScript: "release:publish-qualified",
|
|
775
|
+
qualificationRunnerVariable: "WIDGETS_QUALIFICATION_RUNNER",
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
matrix: { os: ["ubuntu-24.04", "windows-2025"], node: ["22.23.2"] },
|
|
779
|
+
templateVersion: "1.8.0",
|
|
780
|
+
};
|
|
781
|
+
if (environments) manifest.candidate.releaseChain.environments = environments;
|
|
782
|
+
return manifest;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function renderRelease(manifest) {
|
|
786
|
+
return renderTemplate(
|
|
787
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
788
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
test("release environments default to the single release environment", () => {
|
|
793
|
+
const content = renderRelease(environmentManifest(null));
|
|
794
|
+
assert.ok((content.match(/environment: release$/gm) ?? []).length >= 2);
|
|
795
|
+
assert.doesNotMatch(content, /environment: signing-authorized/);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("signing and publication split into distinct protected environments", () => {
|
|
799
|
+
const content = renderRelease(environmentManifest({
|
|
800
|
+
signing: "signing-authorized",
|
|
801
|
+
publication: "release-authorized",
|
|
802
|
+
qualification: "qualification",
|
|
803
|
+
}));
|
|
804
|
+
assert.equal((content.match(/environment: signing-authorized$/gm) ?? []).length, 2);
|
|
805
|
+
assert.equal((content.match(/environment: release-authorized$/gm) ?? []).length, 1);
|
|
806
|
+
// A job that may sign must never also be the job that publishes.
|
|
807
|
+
assert.doesNotMatch(content, /environment: release$/m);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("an unsafe or unknown release environment is rejected", () => {
|
|
811
|
+
assert.equal(validateManifest(environmentManifest({ signing: "bad name!" })).valid, false);
|
|
812
|
+
assert.equal(validateManifest(environmentManifest({ promotion: "x" })).valid, false);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test("ci.yml ownership check cannot be removed by deleting optional repo metadata", () => {
|
|
816
|
+
const manifest = { ...structuredClone(FULL_MANIFEST), ownership: { roots: [".", "engine"] } };
|
|
817
|
+
const template = fs.readFileSync(path.join(templatesDir, "ci.yml"), "utf8");
|
|
818
|
+
const withStep = renderTemplate(template, buildRenderContext(manifest, "/repos/coderight"));
|
|
819
|
+
assert.match(withStep, /RightKit ownership \(protected registry roots\)/);
|
|
820
|
+
const without = renderTemplate(template, buildRenderContext(structuredClone(FULL_MANIFEST), "/repos/widgets"));
|
|
821
|
+
assert.match(without, /RightKit ownership \(protected registry roots\)/);
|
|
822
|
+
});
|
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",
|
package/templates/ci.yml
CHANGED
|
@@ -64,6 +64,8 @@ jobs:
|
|
|
64
64
|
node-version: ${{ matrix.node }}
|
|
65
65
|
package-manager-cache: false
|
|
66
66
|
${PACKAGE_MANAGER_SETUP}
|
|
67
|
+
- run: ${INSTALL_COMMAND}
|
|
68
|
+
${OWNERSHIP_STEP}
|
|
67
69
|
- uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
|
|
68
70
|
if: steps.rust-changes.outputs.required == 'true'
|
|
69
71
|
with:
|