@rightkit/git 0.2.7 → 0.2.9
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/README.md +4 -2
- package/manifest.mjs +268 -28
- package/manifest.schema.json +32 -1
- package/manifest.test.mjs +48 -0
- package/package.json +1 -1
- package/render.test.mjs +86 -0
- package/templates/profiles/rust-hybrid/release-candidate.yml +3 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Sync records exact prior bytes, refuses unmanaged same-name workflows, & preflig
|
|
|
23
23
|
|
|
24
24
|
## Credential posture (read before enabling package-manager publishing)
|
|
25
25
|
|
|
26
|
-
- **Signing credentials stay environment-scoped.** Opt-in Windows candidates use GitHub OIDC with Azure Artifact Signing. Opt-in macOS candidates use a temporary runner keychain plus Apple API-key notarization. npm publishing uses short-lived OIDC.
|
|
26
|
+
- **Signing credentials stay environment-scoped.** Opt-in Windows candidates use GitHub OIDC with Azure Artifact Signing. Opt-in macOS candidates use a temporary runner keychain plus Apple API-key notarization. Signing jobs run only for `v*` tag refs; manual dispatch produces unsigned candidates only. npm publishing uses short-lived OIDC.
|
|
27
27
|
- **Exception, disclosed:** the optional, opt-in `publish-package-managers` lane *does* require two owner-provisioned, long-lived tokens in GitHub Actions `secrets.*` (`HOMEBREW_TAP_TOKEN`, `WINGET_CREATE_GITHUB_TOKEN`) to open tap/manifest PRs. These are real bearer tokens, public-repo-scoped by the visibility gate but not zero-exposure. The "no signing credential in CI" invariant is specifically about code-signing material; it does not mean "no credential of any kind."
|
|
28
28
|
|
|
29
29
|
Manifest input is treated as untrusted: every field flowing into rendered YAML is charset-allowlisted, so a manifest cannot inject `${{ secrets.* }}` or shell/expression syntax.
|
|
@@ -38,4 +38,6 @@ Node builtins only; no runtime dependencies.
|
|
|
38
38
|
|
|
39
39
|
## Profiles
|
|
40
40
|
|
|
41
|
-
`profile` defaults to `node`. `rust-hybrid` requires `ci`, accepts optional `release-candidate`, installs pinned Node plus Rust, then runs product-owned package scripts. Candidate configuration is `{ buildScript, checkScript, artifactRoot, os, signWindows?, signMacos? }
|
|
41
|
+
`profile` defaults to `node`. `rust-hybrid` requires `ci`, accepts optional `release-candidate`, installs pinned Node plus Rust, then runs product-owned package scripts. Candidate configuration is `{ buildScript, checkScript, artifactRoot, os, targets?, signWindows?, signMacos?, releaseChain? }`. Legacy `os` remains supported & derives one target per runner; explicit `targets` is `{ os, platform, architecture }[]`, independent of main CI matrix. Each target maps to native runner, Rust target triple, exact unsigned artifact, & platform signing matrix. Supported platform values are `windows`, `macos`, `linux`; architectures are `x86_64`, `arm64`. RightGit always uploads an unsigned handoff. `signWindows: true` adds Azure OIDC signing. `signMacos: true` adds Developer ID signing plus notarization from protected environment secrets.
|
|
42
|
+
|
|
43
|
+
`releaseChain` lets RightGit own full protected DAG while product scripts own artifact semantics: `{ windowsFinalizeScript, macosFinalizeScript?, installedQualificationScript, publishScript, qualificationRunnerVariable, publishSecrets? }`. Generated ordering is unsigned candidate → platform signing/finalization → installed qualification → publication. It deliberately rejects more than one Windows target or more than one macOS target because its installed-qualification/publish handoff has one canonical signed artifact per platform. Every protected job is tag-gated, publication needs successful installed qualification plus macOS finalization when enabled, & only publish job receives declared publication secrets plus `contents: write`.
|
package/manifest.mjs
CHANGED
|
@@ -61,6 +61,24 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
61
61
|
]);
|
|
62
62
|
const GATE_PATH_RE = /^(?!.*\.\.)(?!.*\s)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
63
63
|
const PACKAGE_SCRIPT_RE = /^[A-Za-z0-9][A-Za-z0-9:_-]*$/;
|
|
64
|
+
const GITHUB_VARIABLE_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
65
|
+
const RELEASE_PLATFORMS = new Set(["windows", "macos", "linux"]);
|
|
66
|
+
const RELEASE_ARCHITECTURES = new Set(["x86_64", "arm64"]);
|
|
67
|
+
const NATIVE_RUNNER_TARGETS = new Map([
|
|
68
|
+
["windows-2025", ["windows", "x86_64"]],
|
|
69
|
+
["windows-11-arm", ["windows", "arm64"]],
|
|
70
|
+
["macos-15", ["macos", "arm64"]],
|
|
71
|
+
["macos-15-intel", ["macos", "x86_64"]],
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
function inferredTarget(os) {
|
|
75
|
+
const platform = /^windows-/i.test(os) ? "windows" : /^macos-/i.test(os) ? "macos" : "linux";
|
|
76
|
+
return { os, platform, architecture: platform === "macos" ? "arm64" : "x86_64" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function candidateTargets(candidate) {
|
|
80
|
+
return Array.isArray(candidate?.targets) ? candidate.targets : (candidate?.os ?? []).map(inferredTarget);
|
|
81
|
+
}
|
|
64
82
|
|
|
65
83
|
/**
|
|
66
84
|
* Validate a parsed manifest object against the right-git manifest contract.
|
|
@@ -141,7 +159,7 @@ export function validateManifest(manifest) {
|
|
|
141
159
|
errors.push("candidate must be an object");
|
|
142
160
|
} else {
|
|
143
161
|
for (const key of Object.keys(candidate)) {
|
|
144
|
-
if (!["buildScript", "checkScript", "artifactRoot", "os", "signWindows", "signMacos"].includes(key)) errors.push(`unknown candidate field: ${key}`);
|
|
162
|
+
if (!["buildScript", "checkScript", "artifactRoot", "os", "targets", "signWindows", "signMacos", "releaseChain"].includes(key)) errors.push(`unknown candidate field: ${key}`);
|
|
145
163
|
}
|
|
146
164
|
for (const key of ["buildScript", "checkScript"]) {
|
|
147
165
|
if (typeof candidate[key] !== "string" || !PACKAGE_SCRIPT_RE.test(candidate[key])) {
|
|
@@ -153,24 +171,87 @@ export function validateManifest(manifest) {
|
|
|
153
171
|
}
|
|
154
172
|
if (!Array.isArray(candidate.os) || candidate.os.length === 0 || candidate.os.some((value) => typeof value !== "string" || !SAFE_MATRIX_VALUE_RE.test(value))) {
|
|
155
173
|
errors.push("candidate.os must be a non-empty array of safe runner labels");
|
|
156
|
-
} else if (candidate.os.some((value) => !manifest.matrix?.os?.includes(value))) {
|
|
174
|
+
} else if (candidate.targets === undefined && candidate.os.some((value) => !manifest.matrix?.os?.includes(value))) {
|
|
157
175
|
errors.push("candidate.os entries must also exist in matrix.os");
|
|
158
176
|
}
|
|
177
|
+
if (candidate.targets !== undefined) {
|
|
178
|
+
if (!Array.isArray(candidate.targets) || candidate.targets.length === 0) {
|
|
179
|
+
errors.push("candidate.targets must be a non-empty array");
|
|
180
|
+
} else {
|
|
181
|
+
const targetKeys = new Set();
|
|
182
|
+
for (const target of candidate.targets) {
|
|
183
|
+
if (target === null || typeof target !== "object" || Array.isArray(target)) {
|
|
184
|
+
errors.push("candidate.targets entries must be objects");
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(target).some((key) => !["os", "platform", "architecture"].includes(key))) errors.push("candidate.targets entries may only contain os, platform, architecture");
|
|
188
|
+
if (typeof target.os !== "string" || !SAFE_MATRIX_VALUE_RE.test(target.os)) errors.push("candidate.targets[].os must be a safe runner label");
|
|
189
|
+
if (!RELEASE_PLATFORMS.has(target.platform)) errors.push("candidate.targets[].platform must be windows, macos, or linux");
|
|
190
|
+
if (!RELEASE_ARCHITECTURES.has(target.architecture)) errors.push("candidate.targets[].architecture must be x86_64 or arm64");
|
|
191
|
+
const nativeTarget = NATIVE_RUNNER_TARGETS.get(target.os);
|
|
192
|
+
if (nativeTarget && (target.platform !== nativeTarget[0] || target.architecture !== nativeTarget[1])) {
|
|
193
|
+
errors.push(`candidate.targets[] must declare ${target.os} as ${nativeTarget[0]}/${nativeTarget[1]}`);
|
|
194
|
+
}
|
|
195
|
+
if ((target.platform === "windows" && !/^windows-/i.test(target.os)) || (target.platform === "macos" && !/^macos-/i.test(target.os))) {
|
|
196
|
+
errors.push("candidate.targets[].os must match its Windows or macOS platform");
|
|
197
|
+
}
|
|
198
|
+
const targetKey = `${target.os}|${target.platform}|${target.architecture}`;
|
|
199
|
+
if (targetKeys.has(targetKey)) errors.push("candidate.targets entries must be unique");
|
|
200
|
+
targetKeys.add(targetKey);
|
|
201
|
+
}
|
|
202
|
+
const targetOs = new Set(candidate.targets.map((target) => target?.os));
|
|
203
|
+
if (candidate.os.some((os) => !targetOs.has(os)) || targetOs.size !== new Set(candidate.os).size || candidate.os.length !== new Set(candidate.os).size) {
|
|
204
|
+
errors.push("candidate.os must contain exactly candidate.targets runner labels");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const targets = candidateTargets(candidate);
|
|
159
209
|
if (candidate.signWindows !== undefined && typeof candidate.signWindows !== "boolean") {
|
|
160
210
|
errors.push("candidate.signWindows must be a boolean");
|
|
161
211
|
}
|
|
162
|
-
if (candidate.signWindows === true &&
|
|
163
|
-
errors.push("candidate.signWindows requires
|
|
212
|
+
if (candidate.signWindows === true && targets.filter((target) => target?.platform === "windows").length === 0) {
|
|
213
|
+
errors.push("candidate.signWindows requires at least one Windows target");
|
|
164
214
|
}
|
|
165
215
|
if (candidate.signMacos !== undefined && typeof candidate.signMacos !== "boolean") {
|
|
166
216
|
errors.push("candidate.signMacos must be a boolean");
|
|
167
217
|
}
|
|
168
|
-
if (candidate.signMacos === true &&
|
|
169
|
-
errors.push("candidate.signMacos requires
|
|
218
|
+
if (candidate.signMacos === true && targets.filter((target) => target?.platform === "macos").length === 0) {
|
|
219
|
+
errors.push("candidate.signMacos requires at least one macOS target");
|
|
170
220
|
}
|
|
171
221
|
if ((candidate.signWindows === true || candidate.signMacos === true) && manifest.matrix?.node?.length !== 1) {
|
|
172
222
|
errors.push("candidate signing requires exactly one matrix.node version");
|
|
173
223
|
}
|
|
224
|
+
if (candidate.releaseChain !== undefined) {
|
|
225
|
+
const chain = candidate.releaseChain;
|
|
226
|
+
const allowedChainKeys = ["windowsFinalizeScript", "macosFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable", "publishSecrets"];
|
|
227
|
+
if (chain === null || typeof chain !== "object" || Array.isArray(chain)) {
|
|
228
|
+
errors.push("candidate.releaseChain must be an object");
|
|
229
|
+
} else {
|
|
230
|
+
for (const key of Object.keys(chain)) {
|
|
231
|
+
if (!allowedChainKeys.includes(key)) errors.push(`unknown candidate.releaseChain field: ${key}`);
|
|
232
|
+
}
|
|
233
|
+
for (const key of ["windowsFinalizeScript", "installedQualificationScript", "publishScript"]) {
|
|
234
|
+
if (typeof chain[key] !== "string" || !PACKAGE_SCRIPT_RE.test(chain[key])) {
|
|
235
|
+
errors.push(`candidate.releaseChain.${key} must be one safe package script name`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (chain.macosFinalizeScript !== undefined && (typeof chain.macosFinalizeScript !== "string" || !PACKAGE_SCRIPT_RE.test(chain.macosFinalizeScript))) {
|
|
239
|
+
errors.push("candidate.releaseChain.macosFinalizeScript must be one safe package script name");
|
|
240
|
+
}
|
|
241
|
+
if (typeof chain.qualificationRunnerVariable !== "string" || !GITHUB_VARIABLE_RE.test(chain.qualificationRunnerVariable)) {
|
|
242
|
+
errors.push("candidate.releaseChain.qualificationRunnerVariable must be one uppercase GitHub variable name");
|
|
243
|
+
}
|
|
244
|
+
if (chain.publishSecrets !== undefined && (!Array.isArray(chain.publishSecrets) || new Set(chain.publishSecrets).size !== chain.publishSecrets.length || chain.publishSecrets.some((value) => typeof value !== "string" || !GITHUB_VARIABLE_RE.test(value)))) {
|
|
245
|
+
errors.push("candidate.releaseChain.publishSecrets must be unique uppercase GitHub secret names");
|
|
246
|
+
}
|
|
247
|
+
if (candidate.signWindows !== true) errors.push("candidate.releaseChain requires candidate.signWindows=true");
|
|
248
|
+
if (targets.filter((target) => target?.platform === "windows").length !== 1 || targets.filter((target) => target?.platform === "macos").length > 1) {
|
|
249
|
+
errors.push("candidate.releaseChain requires at most one target per signing platform; multi-target aggregation is not supported");
|
|
250
|
+
}
|
|
251
|
+
if (candidate.signMacos === true && !chain.macosFinalizeScript) errors.push("candidate.releaseChain with candidate.signMacos=true requires macosFinalizeScript");
|
|
252
|
+
if (chain.macosFinalizeScript && candidate.signMacos !== true) errors.push("candidate.releaseChain.macosFinalizeScript requires candidate.signMacos=true");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
174
255
|
}
|
|
175
256
|
}
|
|
176
257
|
if (profile === "rust-hybrid" && lanes.includes("release-candidate") && candidate === undefined) {
|
|
@@ -295,10 +376,8 @@ function yamlFlowStringList(values) {
|
|
|
295
376
|
|
|
296
377
|
function candidateMatrix(manifest) {
|
|
297
378
|
const entries = [];
|
|
298
|
-
for (const
|
|
299
|
-
const
|
|
300
|
-
const architecture = platform === "macos" ? "arm64" : "x86_64";
|
|
301
|
-
for (const node of manifest.matrix.node) entries.push({ os, node, platform, architecture });
|
|
379
|
+
for (const target of candidateTargets(manifest.candidate ?? { os: manifest.matrix.os })) {
|
|
380
|
+
for (const node of manifest.matrix.node) entries.push({ ...target, node, targetTriple: targetTriple(target) });
|
|
302
381
|
}
|
|
303
382
|
return [
|
|
304
383
|
" include:",
|
|
@@ -307,6 +386,31 @@ function candidateMatrix(manifest) {
|
|
|
307
386
|
` node: ${JSON.stringify(entry.node)}`,
|
|
308
387
|
` platform: ${JSON.stringify(entry.platform)}`,
|
|
309
388
|
` architecture: ${JSON.stringify(entry.architecture)}`,
|
|
389
|
+
` targetTriple: ${JSON.stringify(entry.targetTriple)}`,
|
|
390
|
+
]),
|
|
391
|
+
].join("\n");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function targetTriple(target) {
|
|
395
|
+
const rustArchitecture = target.architecture === "arm64" ? "aarch64" : target.architecture;
|
|
396
|
+
if (target.platform === "windows") return `${rustArchitecture}-pc-windows-msvc`;
|
|
397
|
+
if (target.platform === "macos") return `${rustArchitecture}-apple-darwin`;
|
|
398
|
+
return `${rustArchitecture}-unknown-linux-gnu`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function signingMatrix(manifest, platform) {
|
|
402
|
+
const node = String(manifest.matrix.node[0]);
|
|
403
|
+
const entries = candidateTargets(manifest.candidate)
|
|
404
|
+
.filter((target) => target.platform === platform)
|
|
405
|
+
.map((target) => ({ ...target, node, targetTriple: targetTriple(target) }));
|
|
406
|
+
return [
|
|
407
|
+
" include:",
|
|
408
|
+
...entries.flatMap((entry) => [
|
|
409
|
+
` - os: ${JSON.stringify(entry.os)}`,
|
|
410
|
+
` platform: ${JSON.stringify(entry.platform)}`,
|
|
411
|
+
` architecture: ${JSON.stringify(entry.architecture)}`,
|
|
412
|
+
` targetTriple: ${JSON.stringify(entry.targetTriple)}`,
|
|
413
|
+
` node: ${JSON.stringify(entry.node)}`,
|
|
310
414
|
]),
|
|
311
415
|
].join("\n");
|
|
312
416
|
}
|
|
@@ -321,13 +425,30 @@ const WINDOWS_SDK_BUILD_TOOLS_SHA256 = "180DEB372659029864C10A0C04787833234D64AA
|
|
|
321
425
|
|
|
322
426
|
function windowsSigningJob(manifest, pm, artifactPrefix) {
|
|
323
427
|
if (manifest.candidate?.signWindows !== true) return "";
|
|
324
|
-
const windowsRunner = manifest.candidate.os.find((value) => /^windows-/i.test(value));
|
|
325
428
|
const nodeVersion = String(manifest.matrix.node[0]);
|
|
429
|
+
const releaseChain = manifest.candidate.releaseChain;
|
|
430
|
+
const finalizeCommand = releaseChain
|
|
431
|
+
? `${pm.runPrefix} ${releaseChain.windowsFinalizeScript}`
|
|
432
|
+
: "pnpm exec right-release build --platform win --skip-checks";
|
|
433
|
+
const finalizeEnvironment = releaseChain
|
|
434
|
+
? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
|
|
435
|
+
RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/finalized-windows
|
|
436
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
|
|
437
|
+
: ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
|
|
438
|
+
LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
|
|
439
|
+
const uploadPath = releaseChain
|
|
440
|
+
? " ${{ runner.temp }}/finalized-windows/**"
|
|
441
|
+
: " dist/releases/windows/**\n .right-release/receipts/windows-*.json";
|
|
326
442
|
return `
|
|
327
443
|
windows-sign:
|
|
328
444
|
needs: [candidate]
|
|
445
|
+
if: \${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
329
446
|
environment: release
|
|
330
|
-
|
|
447
|
+
strategy:
|
|
448
|
+
fail-fast: false
|
|
449
|
+
matrix:
|
|
450
|
+
${signingMatrix(manifest, "windows")}
|
|
451
|
+
runs-on: \${{ matrix.os }}
|
|
331
452
|
permissions:
|
|
332
453
|
contents: read
|
|
333
454
|
id-token: write
|
|
@@ -344,11 +465,12 @@ ${pm.setup}
|
|
|
344
465
|
- uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
|
|
345
466
|
with:
|
|
346
467
|
toolchain: 1.98.0
|
|
468
|
+
targets: \${{ matrix.targetTriple }}
|
|
347
469
|
- run: ${pm.installCommand}
|
|
348
470
|
- name: Download exact unsigned Windows candidate
|
|
349
471
|
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
350
472
|
with:
|
|
351
|
-
name: ${artifactPrefix}-unsigned
|
|
473
|
+
name: ${artifactPrefix}-unsigned-\${{ matrix.platform }}-\${{ matrix.architecture }}-${nodeVersion}
|
|
352
474
|
path: \${{ runner.temp }}/unsigned-windows-candidate
|
|
353
475
|
- name: Authenticate to Azure with GitHub OIDC
|
|
354
476
|
uses: azure/login@${AZURE_LOGIN_SHA} # v3
|
|
@@ -373,33 +495,50 @@ ${pm.setup}
|
|
|
373
495
|
"AZURE_ARTIFACT_SIGNING_DLIB_PATH=$tools\\client\\bin\\x64\\Azure.CodeSigning.Dlib.dll" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
374
496
|
"AZURE_SIGNTOOL_PATH=$tools\\sdk\\bin\\10.0.26100.0\\x64\\signtool.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
375
497
|
- name: Sign & finalize Windows candidate
|
|
376
|
-
run:
|
|
498
|
+
run: ${finalizeCommand}
|
|
377
499
|
env:
|
|
378
|
-
|
|
379
|
-
|
|
500
|
+
${finalizeEnvironment}
|
|
501
|
+
RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
|
|
502
|
+
RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
|
|
380
503
|
AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
|
|
381
504
|
AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
|
|
382
505
|
AZURE_ARTIFACT_SIGNING_PROFILE: \${{ vars.AZURE_ARTIFACT_SIGNING_PROFILE }}
|
|
383
506
|
- name: Upload signed Windows candidate
|
|
384
507
|
uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
|
|
385
508
|
with:
|
|
386
|
-
name: ${artifactPrefix}-signed-windows-${nodeVersion}
|
|
509
|
+
name: ${artifactPrefix}-signed-windows-\${{ matrix.architecture }}-${nodeVersion}
|
|
387
510
|
path: |
|
|
388
|
-
|
|
389
|
-
.right-release/receipts/windows-*.json
|
|
511
|
+
${uploadPath}
|
|
390
512
|
if-no-files-found: error
|
|
391
513
|
`.trimEnd();
|
|
392
514
|
}
|
|
393
515
|
|
|
394
516
|
function macosSigningJob(manifest, pm, artifactPrefix) {
|
|
395
517
|
if (manifest.candidate?.signMacos !== true) return "";
|
|
396
|
-
const macosRunner = manifest.candidate.os.find((value) => /^macos-/i.test(value));
|
|
397
518
|
const nodeVersion = String(manifest.matrix.node[0]);
|
|
519
|
+
const releaseChain = manifest.candidate.releaseChain;
|
|
520
|
+
const finalizeCommand = releaseChain
|
|
521
|
+
? `${pm.runPrefix} ${releaseChain.macosFinalizeScript}`
|
|
522
|
+
: "pnpm exec right-release build --platform mac --skip-checks";
|
|
523
|
+
const finalizeEnvironment = releaseChain
|
|
524
|
+
? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
|
|
525
|
+
RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/finalized-macos
|
|
526
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
|
|
527
|
+
: ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
|
|
528
|
+
LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
|
|
529
|
+
const uploadPath = releaseChain
|
|
530
|
+
? " ${{ runner.temp }}/finalized-macos/**"
|
|
531
|
+
: " dist/releases/mac/**\n .right-release/receipts/macos-*.json";
|
|
398
532
|
return `
|
|
399
533
|
macos-sign:
|
|
400
534
|
needs: [candidate]
|
|
535
|
+
if: \${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
401
536
|
environment: release
|
|
402
|
-
|
|
537
|
+
strategy:
|
|
538
|
+
fail-fast: false
|
|
539
|
+
matrix:
|
|
540
|
+
${signingMatrix(manifest, "macos")}
|
|
541
|
+
runs-on: \${{ matrix.os }}
|
|
403
542
|
permissions:
|
|
404
543
|
contents: read
|
|
405
544
|
steps:
|
|
@@ -415,11 +554,12 @@ ${pm.setup}
|
|
|
415
554
|
- uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
|
|
416
555
|
with:
|
|
417
556
|
toolchain: 1.98.0
|
|
557
|
+
targets: \${{ matrix.targetTriple }}
|
|
418
558
|
- run: ${pm.installCommand}
|
|
419
559
|
- name: Download exact unsigned macOS candidate
|
|
420
560
|
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
421
561
|
with:
|
|
422
|
-
name: ${artifactPrefix}-unsigned
|
|
562
|
+
name: ${artifactPrefix}-unsigned-\${{ matrix.platform }}-\${{ matrix.architecture }}-${nodeVersion}
|
|
423
563
|
path: \${{ runner.temp }}/unsigned-macos-candidate
|
|
424
564
|
- name: Install Apple signing identity in temporary keychain
|
|
425
565
|
shell: bash
|
|
@@ -443,20 +583,20 @@ ${pm.setup}
|
|
|
443
583
|
security list-keychain -d user -s "$keychain_path"
|
|
444
584
|
printf 'APPLE_API_KEY_PATH=%s\\n' "$key_path" >> "$GITHUB_ENV"
|
|
445
585
|
- name: Sign, notarize & finalize macOS candidate
|
|
446
|
-
run:
|
|
586
|
+
run: ${finalizeCommand}
|
|
447
587
|
env:
|
|
448
|
-
|
|
449
|
-
|
|
588
|
+
${finalizeEnvironment}
|
|
589
|
+
RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
|
|
590
|
+
RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
|
|
450
591
|
APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
|
|
451
592
|
APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
|
|
452
593
|
APPLE_API_ISSUER: \${{ secrets.APPLE_API_ISSUER }}
|
|
453
594
|
- name: Upload signed & notarized macOS candidate
|
|
454
595
|
uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
|
|
455
596
|
with:
|
|
456
|
-
name: ${artifactPrefix}-signed-macos-${nodeVersion}
|
|
597
|
+
name: ${artifactPrefix}-signed-macos-\${{ matrix.architecture }}-${nodeVersion}
|
|
457
598
|
path: |
|
|
458
|
-
|
|
459
|
-
.right-release/receipts/macos-*.json
|
|
599
|
+
${uploadPath}
|
|
460
600
|
if-no-files-found: error
|
|
461
601
|
- name: Remove Apple signing material
|
|
462
602
|
if: \${{ always() }}
|
|
@@ -467,6 +607,105 @@ ${pm.setup}
|
|
|
467
607
|
`.trimEnd();
|
|
468
608
|
}
|
|
469
609
|
|
|
610
|
+
function releaseChainJobs(manifest, pm, artifactPrefix) {
|
|
611
|
+
const chain = manifest.candidate?.releaseChain;
|
|
612
|
+
if (!chain) return "";
|
|
613
|
+
const windowsTarget = candidateTargets(manifest.candidate).find((target) => target.platform === "windows");
|
|
614
|
+
const windowsRunner = windowsTarget.os;
|
|
615
|
+
const windowsArchitecture = windowsTarget.architecture;
|
|
616
|
+
const macosTarget = candidateTargets(manifest.candidate).find((target) => target.platform === "macos");
|
|
617
|
+
const nodeVersion = String(manifest.matrix.node[0]);
|
|
618
|
+
const macosDependency = manifest.candidate.signMacos === true ? ", macos-sign" : "";
|
|
619
|
+
const macosSuccess = manifest.candidate.signMacos === true ? " && needs.macos-sign.result == 'success'" : "";
|
|
620
|
+
const macosDownload = manifest.candidate.signMacos === true
|
|
621
|
+
? ` - name: Download same-run signed & notarized macOS release
|
|
622
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
623
|
+
with:
|
|
624
|
+
name: ${artifactPrefix}-signed-macos-${macosTarget.architecture}-${nodeVersion}
|
|
625
|
+
path: \${{ runner.temp }}/right-git-release/macos`
|
|
626
|
+
: "";
|
|
627
|
+
const publishSecretEnvironment = (chain.publishSecrets ?? [])
|
|
628
|
+
.map((name) => ` ${name}: \${{ secrets.${name} }}`)
|
|
629
|
+
.join("\n");
|
|
630
|
+
return `
|
|
631
|
+
installed-qualification:
|
|
632
|
+
needs: [candidate, windows-sign]
|
|
633
|
+
if: \${{ always() && startsWith(github.ref, 'refs/tags/v') && needs.candidate.result == 'success' }}
|
|
634
|
+
environment: release
|
|
635
|
+
runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
|
|
636
|
+
steps:
|
|
637
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
638
|
+
- name: Resolve checked-out source revision
|
|
639
|
+
shell: pwsh
|
|
640
|
+
run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
|
|
641
|
+
- name: Fail closed when protected finalization is absent
|
|
642
|
+
if: \${{ needs.windows-sign.result != 'success' }}
|
|
643
|
+
shell: pwsh
|
|
644
|
+
run: throw 'installed_qualification_failed { reason: prerequisites_absent, detail: "same-run Windows signing/finalization did not succeed" }'
|
|
645
|
+
- name: Download same-run finalized Windows release
|
|
646
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
647
|
+
with:
|
|
648
|
+
name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
|
|
649
|
+
path: \${{ runner.temp }}/right-git-release/windows
|
|
650
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
651
|
+
with:
|
|
652
|
+
node-version: ${nodeVersion}
|
|
653
|
+
package-manager-cache: false
|
|
654
|
+
${pm.setup}
|
|
655
|
+
- run: ${pm.installCommand}
|
|
656
|
+
- name: Run installed-artifact qualification
|
|
657
|
+
run: ${pm.runPrefix} ${chain.installedQualificationScript}
|
|
658
|
+
env:
|
|
659
|
+
RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
|
|
660
|
+
RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
|
|
661
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
|
|
662
|
+
- name: Upload installed qualification evidence
|
|
663
|
+
uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
|
|
664
|
+
with:
|
|
665
|
+
name: ${artifactPrefix}-installed-qualification-${nodeVersion}
|
|
666
|
+
path: \${{ runner.temp }}/right-git-release/qualification/**
|
|
667
|
+
if-no-files-found: error
|
|
668
|
+
publish:
|
|
669
|
+
needs: [installed-qualification${macosDependency}]
|
|
670
|
+
if: \${{ startsWith(github.ref, 'refs/tags/v') && needs.installed-qualification.result == 'success'${macosSuccess} }}
|
|
671
|
+
environment: release
|
|
672
|
+
runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
|
|
673
|
+
permissions:
|
|
674
|
+
contents: write
|
|
675
|
+
steps:
|
|
676
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
677
|
+
- name: Resolve checked-out source revision
|
|
678
|
+
shell: pwsh
|
|
679
|
+
run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
|
|
680
|
+
- name: Download same-run finalized Windows release
|
|
681
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
682
|
+
with:
|
|
683
|
+
name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
|
|
684
|
+
path: \${{ runner.temp }}/right-git-release/windows
|
|
685
|
+
${macosDownload}
|
|
686
|
+
- name: Download same-run installed qualification evidence
|
|
687
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
688
|
+
with:
|
|
689
|
+
name: ${artifactPrefix}-installed-qualification-${nodeVersion}
|
|
690
|
+
path: \${{ runner.temp }}/right-git-release/qualification
|
|
691
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
692
|
+
with:
|
|
693
|
+
node-version: ${nodeVersion}
|
|
694
|
+
package-manager-cache: false
|
|
695
|
+
${pm.setup}
|
|
696
|
+
- run: ${pm.installCommand}
|
|
697
|
+
- name: Publish qualified signed release
|
|
698
|
+
run: ${pm.runPrefix} ${chain.publishScript}
|
|
699
|
+
env:
|
|
700
|
+
GH_TOKEN: \${{ github.token }}
|
|
701
|
+
RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
|
|
702
|
+
RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/right-git-release/macos
|
|
703
|
+
RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
|
|
704
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
|
|
705
|
+
${publishSecretEnvironment}
|
|
706
|
+
`.trimEnd();
|
|
707
|
+
}
|
|
708
|
+
|
|
470
709
|
/**
|
|
471
710
|
* Derive one project name from the target repo's package.json, falling back
|
|
472
711
|
* to the repo directory's basename. Never throws.
|
|
@@ -513,6 +752,7 @@ export function buildRenderContext(manifest, repoPath) {
|
|
|
513
752
|
CANDIDATE_MATRIX: candidateMatrix(manifest),
|
|
514
753
|
WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
|
|
515
754
|
MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
|
|
755
|
+
RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
|
|
516
756
|
};
|
|
517
757
|
|
|
518
758
|
if (manifest.publish?.npm) {
|
package/manifest.schema.json
CHANGED
|
@@ -95,8 +95,39 @@
|
|
|
95
95
|
"checkScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
96
96
|
"artifactRoot": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" },
|
|
97
97
|
"os": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
|
|
98
|
+
"targets": {
|
|
99
|
+
"type": "array",
|
|
100
|
+
"minItems": 1,
|
|
101
|
+
"items": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"required": ["os", "platform", "architecture"],
|
|
104
|
+
"additionalProperties": false,
|
|
105
|
+
"properties": {
|
|
106
|
+
"os": { "type": "string", "minLength": 1 },
|
|
107
|
+
"platform": { "enum": ["windows", "macos", "linux"] },
|
|
108
|
+
"architecture": { "enum": ["x86_64", "arm64"] }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
98
112
|
"signWindows": { "type": "boolean" },
|
|
99
|
-
"signMacos": { "type": "boolean" }
|
|
113
|
+
"signMacos": { "type": "boolean" },
|
|
114
|
+
"releaseChain": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"required": ["windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
|
|
117
|
+
"additionalProperties": false,
|
|
118
|
+
"properties": {
|
|
119
|
+
"windowsFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
120
|
+
"macosFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
121
|
+
"installedQualificationScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
122
|
+
"publishScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
123
|
+
"qualificationRunnerVariable": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" },
|
|
124
|
+
"publishSecrets": {
|
|
125
|
+
"type": "array",
|
|
126
|
+
"uniqueItems": true,
|
|
127
|
+
"items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
100
131
|
}
|
|
101
132
|
}
|
|
102
133
|
}
|
package/manifest.test.mjs
CHANGED
|
@@ -41,6 +41,54 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
|
|
|
41
41
|
assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, buildScript: "echo unsafe" } }).valid, false);
|
|
42
42
|
assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, artifactRoot: "../escape" } }).valid, false);
|
|
43
43
|
assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, os: ["windows-latest"] } }).valid, false);
|
|
44
|
+
|
|
45
|
+
const releaseChain = {
|
|
46
|
+
...windowsCandidate,
|
|
47
|
+
candidate: {
|
|
48
|
+
...windowsCandidate.candidate,
|
|
49
|
+
releaseChain: {
|
|
50
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
51
|
+
installedQualificationScript: "release:qualify:installed",
|
|
52
|
+
publishScript: "release:publish:qualified",
|
|
53
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
54
|
+
publishSecrets: ["CLOUDFLARE_API_TOKEN"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
assert.equal(validateManifest(releaseChain).valid, true);
|
|
59
|
+
assert.equal(validateManifest({ ...releaseChain, candidate: { ...releaseChain.candidate, signWindows: false } }).valid, false);
|
|
60
|
+
assert.equal(validateManifest({ ...releaseChain, candidate: { ...releaseChain.candidate, releaseChain: { ...releaseChain.candidate.releaseChain, publishScript: "echo unsafe" } } }).valid, false);
|
|
61
|
+
assert.equal(validateManifest({ ...releaseChain, candidate: { ...releaseChain.candidate, releaseChain: { ...releaseChain.candidate.releaseChain, qualificationRunnerVariable: "${{ secrets.X }}" } } }).valid, false);
|
|
62
|
+
assert.equal(validateManifest({ ...releaseChain, candidate: { ...releaseChain.candidate, releaseChain: { ...releaseChain.candidate.releaseChain, publishSecrets: ["TOKEN", "TOKEN"] } } }).valid, false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("candidate targets permit native multi-architecture matrices independent of normal CI runners", () => {
|
|
66
|
+
const multiArchitecture = {
|
|
67
|
+
...VALID_MINIMAL,
|
|
68
|
+
profile: "rust-hybrid",
|
|
69
|
+
gate: { path: "scripts/gate.sh" },
|
|
70
|
+
lanes: ["ci", "release-candidate"],
|
|
71
|
+
candidate: {
|
|
72
|
+
buildScript: "ci:candidate",
|
|
73
|
+
checkScript: "ci:candidate-check",
|
|
74
|
+
artifactRoot: "widgets-unsigned",
|
|
75
|
+
os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
|
|
76
|
+
targets: [
|
|
77
|
+
{ os: "windows-2025", platform: "windows", architecture: "x86_64" },
|
|
78
|
+
{ os: "windows-11-arm", platform: "windows", architecture: "arm64" },
|
|
79
|
+
{ os: "macos-15", platform: "macos", architecture: "arm64" },
|
|
80
|
+
{ os: "macos-15-intel", platform: "macos", architecture: "x86_64" },
|
|
81
|
+
],
|
|
82
|
+
signWindows: true,
|
|
83
|
+
signMacos: true,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
assert.equal(validateManifest(multiArchitecture).valid, true, JSON.stringify(validateManifest(multiArchitecture).errors));
|
|
87
|
+
assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "windows-2025", platform: "windows", architecture: "riscv64" }] } }).valid, false);
|
|
88
|
+
assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "windows-11-arm", platform: "windows", architecture: "x86_64" }] } }).valid, false);
|
|
89
|
+
assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "ubuntu-24.04", platform: "windows", architecture: "x86_64" }] } }).valid, false);
|
|
90
|
+
assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "${{ secrets.X }}", platform: "windows", architecture: "x86_64" }] } }).valid, false);
|
|
91
|
+
assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, releaseChain: { windowsFinalizeScript: "release:finalize:win", macosFinalizeScript: "release:finalize:mac", installedQualificationScript: "release:qualify", publishScript: "release:publish", qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL" } } }).valid, false);
|
|
44
92
|
});
|
|
45
93
|
|
|
46
94
|
test("rejects non-object input", () => {
|
package/package.json
CHANGED
package/render.test.mjs
CHANGED
|
@@ -90,6 +90,7 @@ test("rust-hybrid release candidate can opt into OIDC Windows signing without lo
|
|
|
90
90
|
buildRenderContext(manifest, "/repos/widgets"),
|
|
91
91
|
);
|
|
92
92
|
assert.match(content, /environment: release/);
|
|
93
|
+
assert.match(content, /windows-sign:\n needs: \[candidate\]\n if: \$\{\{ startsWith\(github\.ref, 'refs\/tags\/v'\) \}\}/);
|
|
93
94
|
assert.match(content, /id-token: write/);
|
|
94
95
|
assert.match(content, /azure\/login@[a-f0-9]{40}/);
|
|
95
96
|
assert.match(content, /vars\.AZURE_CLIENT_ID/);
|
|
@@ -116,6 +117,7 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
|
|
|
116
117
|
buildRenderContext(manifest, "/repos/widgets"),
|
|
117
118
|
);
|
|
118
119
|
assert.match(content, /macos-sign:/);
|
|
120
|
+
assert.match(content, /macos-sign:\n needs: \[candidate\]\n if: \$\{\{ startsWith\(github\.ref, 'refs\/tags\/v'\) \}\}/);
|
|
119
121
|
assert.match(content, /environment: release/);
|
|
120
122
|
assert.match(content, /secrets\.APPLE_CERTIFICATE_BASE64/);
|
|
121
123
|
assert.match(content, /temporary keychain/i);
|
|
@@ -128,6 +130,90 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
|
|
|
128
130
|
assert.doesNotMatch(content, /pull_request_target/);
|
|
129
131
|
});
|
|
130
132
|
|
|
133
|
+
test("rust-hybrid multi-architecture candidates bind every native target to exact artifacts and signing", () => {
|
|
134
|
+
const manifest = {
|
|
135
|
+
...FULL_MANIFEST,
|
|
136
|
+
lanes: ["ci", "release-candidate"],
|
|
137
|
+
profile: "rust-hybrid",
|
|
138
|
+
gate: { path: "scripts/gate.sh" },
|
|
139
|
+
matrix: { os: ["ubuntu-24.04"], node: ["22.23.2"] },
|
|
140
|
+
candidate: {
|
|
141
|
+
buildScript: "ci:candidate",
|
|
142
|
+
checkScript: "ci:candidate-check",
|
|
143
|
+
artifactRoot: "widgets-unsigned",
|
|
144
|
+
os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
|
|
145
|
+
targets: [
|
|
146
|
+
{ os: "windows-2025", platform: "windows", architecture: "x86_64" },
|
|
147
|
+
{ os: "windows-11-arm", platform: "windows", architecture: "arm64" },
|
|
148
|
+
{ os: "macos-15", platform: "macos", architecture: "arm64" },
|
|
149
|
+
{ os: "macos-15-intel", platform: "macos", architecture: "x86_64" },
|
|
150
|
+
],
|
|
151
|
+
signWindows: true,
|
|
152
|
+
signMacos: true,
|
|
153
|
+
},
|
|
154
|
+
templateVersion: "1.5.0",
|
|
155
|
+
};
|
|
156
|
+
const content = renderTemplate(
|
|
157
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
158
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
159
|
+
);
|
|
160
|
+
for (const [runner, triple] of [["windows-2025", "x86_64-pc-windows-msvc"], ["windows-11-arm", "aarch64-pc-windows-msvc"], ["macos-15", "aarch64-apple-darwin"], ["macos-15-intel", "x86_64-apple-darwin"]]) {
|
|
161
|
+
assert.match(content, new RegExp(`os: ${JSON.stringify(runner).replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}`));
|
|
162
|
+
assert.match(content, new RegExp(`targetTriple: ${JSON.stringify(triple).replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}`));
|
|
163
|
+
}
|
|
164
|
+
assert.match(content, /targets: \$\{\{ matrix\.targetTriple \}\}/);
|
|
165
|
+
assert.match(content, /windows-sign:[\s\S]*matrix:[\s\S]*windows-11-arm/);
|
|
166
|
+
assert.match(content, /macos-sign:[\s\S]*matrix:[\s\S]*macos-15-intel/);
|
|
167
|
+
assert.match(content, /name: widgets-unsigned-\$\{\{ matrix\.platform \}\}-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
|
|
168
|
+
assert.match(content, /name: widgets-signed-windows-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
|
|
169
|
+
assert.match(content, /name: widgets-signed-macos-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
|
|
170
|
+
assert.equal((content.match(/RIGHT_GIT_RELEASE_ARCHITECTURE: \$\{\{ matrix\.architecture \}\}/g) ?? []).length, 4);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("rust-hybrid release chain preserves signed candidate, installed qualification, then publish", () => {
|
|
174
|
+
const manifest = {
|
|
175
|
+
...FULL_MANIFEST,
|
|
176
|
+
lanes: ["ci", "release-candidate"],
|
|
177
|
+
profile: "rust-hybrid",
|
|
178
|
+
gate: { path: "scripts/gate.sh" },
|
|
179
|
+
candidate: {
|
|
180
|
+
buildScript: "ci:candidate",
|
|
181
|
+
checkScript: "ci:candidate-check",
|
|
182
|
+
artifactRoot: "widgets-unsigned",
|
|
183
|
+
os: ["windows-2025", "macos-15"],
|
|
184
|
+
signWindows: true,
|
|
185
|
+
signMacos: true,
|
|
186
|
+
releaseChain: {
|
|
187
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
188
|
+
macosFinalizeScript: "release:finalize:mac",
|
|
189
|
+
installedQualificationScript: "release:qualify:installed",
|
|
190
|
+
publishScript: "release:publish:qualified",
|
|
191
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
192
|
+
publishSecrets: ["CLOUDFLARE_API_TOKEN"],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
|
|
196
|
+
templateVersion: "1.4.0",
|
|
197
|
+
};
|
|
198
|
+
const content = renderTemplate(
|
|
199
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
200
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
201
|
+
);
|
|
202
|
+
assert.match(content, /windows-sign:[\s\S]*pnpm run release:finalize:win/);
|
|
203
|
+
assert.match(content, /macos-sign:[\s\S]*pnpm run release:finalize:mac/);
|
|
204
|
+
assert.match(content, /installed-qualification:\n needs: \[candidate, windows-sign\]/);
|
|
205
|
+
assert.match(content, /installed_qualification_failed/);
|
|
206
|
+
assert.match(content, /pnpm run release:qualify:installed/);
|
|
207
|
+
assert.match(content, /publish:\n needs: \[installed-qualification, macos-sign\]/);
|
|
208
|
+
assert.match(content, /needs\.macos-sign\.result == 'success'/);
|
|
209
|
+
assert.match(content, /contents: write/);
|
|
210
|
+
assert.match(content, /pnpm run release:publish:qualified/);
|
|
211
|
+
assert.match(content, /CLOUDFLARE_API_TOKEN: \$\{\{ secrets\.CLOUDFLARE_API_TOKEN \}\}/);
|
|
212
|
+
assert.equal((content.match(/RIGHT_GIT_SOURCE_REVISION=\$\(git rev-parse HEAD\)/g) ?? []).length, 3);
|
|
213
|
+
assert.doesNotMatch(content, /RIGHT_GIT_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
|
|
214
|
+
assert.doesNotMatch(content.slice(0, content.indexOf(" windows-sign:")), /secrets\.|id-token: write|contents: write/);
|
|
215
|
+
});
|
|
216
|
+
|
|
131
217
|
test("render determinism: same manifest renders byte-identical output every time", () => {
|
|
132
218
|
const first = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
133
219
|
const second = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
@@ -34,6 +34,7 @@ ${PACKAGE_MANAGER_SETUP}
|
|
|
34
34
|
with:
|
|
35
35
|
toolchain: 1.98.0
|
|
36
36
|
components: rustfmt, clippy
|
|
37
|
+
targets: ${{ matrix.targetTriple }}
|
|
37
38
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
38
39
|
with:
|
|
39
40
|
workspaces: engine
|
|
@@ -53,8 +54,9 @@ ${PACKAGE_MANAGER_SETUP}
|
|
|
53
54
|
- name: Upload unsigned candidate handoff
|
|
54
55
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
55
56
|
with:
|
|
56
|
-
name: ${ARTIFACT_PREFIX}-unsigned-${{ matrix.
|
|
57
|
+
name: ${ARTIFACT_PREFIX}-unsigned-${{ matrix.platform }}-${{ matrix.architecture }}-${{ matrix.node }}
|
|
57
58
|
path: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}/**
|
|
58
59
|
if-no-files-found: error
|
|
59
60
|
${WINDOWS_SIGNING_JOB}
|
|
60
61
|
${MACOS_SIGNING_JOB}
|
|
62
|
+
${RELEASE_CHAIN_JOBS}
|