@rightkit/git 0.2.7 → 0.2.8
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 +161 -11
- package/manifest.schema.json +18 -1
- package/manifest.test.mjs +19 -0
- package/package.json +1 -1
- package/render.test.mjs +46 -0
- package/templates/profiles/rust-hybrid/release-candidate.yml +1 -0
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? }`; `os` selects supported release hosts from main matrix. RightGit always uploads an unsigned handoff. `signWindows: true` adds Azure OIDC signing. `signMacos: true` adds Developer ID signing plus notarization from protected environment secrets.
|
|
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?, releaseChain? }`; `os` selects supported release hosts from main matrix. 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. 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,7 @@ 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_]*$/;
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Validate a parsed manifest object against the right-git manifest contract.
|
|
@@ -141,7 +142,7 @@ export function validateManifest(manifest) {
|
|
|
141
142
|
errors.push("candidate must be an object");
|
|
142
143
|
} else {
|
|
143
144
|
for (const key of Object.keys(candidate)) {
|
|
144
|
-
if (!["buildScript", "checkScript", "artifactRoot", "os", "signWindows", "signMacos"].includes(key)) errors.push(`unknown candidate field: ${key}`);
|
|
145
|
+
if (!["buildScript", "checkScript", "artifactRoot", "os", "signWindows", "signMacos", "releaseChain"].includes(key)) errors.push(`unknown candidate field: ${key}`);
|
|
145
146
|
}
|
|
146
147
|
for (const key of ["buildScript", "checkScript"]) {
|
|
147
148
|
if (typeof candidate[key] !== "string" || !PACKAGE_SCRIPT_RE.test(candidate[key])) {
|
|
@@ -171,6 +172,34 @@ export function validateManifest(manifest) {
|
|
|
171
172
|
if ((candidate.signWindows === true || candidate.signMacos === true) && manifest.matrix?.node?.length !== 1) {
|
|
172
173
|
errors.push("candidate signing requires exactly one matrix.node version");
|
|
173
174
|
}
|
|
175
|
+
if (candidate.releaseChain !== undefined) {
|
|
176
|
+
const chain = candidate.releaseChain;
|
|
177
|
+
const allowedChainKeys = ["windowsFinalizeScript", "macosFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable", "publishSecrets"];
|
|
178
|
+
if (chain === null || typeof chain !== "object" || Array.isArray(chain)) {
|
|
179
|
+
errors.push("candidate.releaseChain must be an object");
|
|
180
|
+
} else {
|
|
181
|
+
for (const key of Object.keys(chain)) {
|
|
182
|
+
if (!allowedChainKeys.includes(key)) errors.push(`unknown candidate.releaseChain field: ${key}`);
|
|
183
|
+
}
|
|
184
|
+
for (const key of ["windowsFinalizeScript", "installedQualificationScript", "publishScript"]) {
|
|
185
|
+
if (typeof chain[key] !== "string" || !PACKAGE_SCRIPT_RE.test(chain[key])) {
|
|
186
|
+
errors.push(`candidate.releaseChain.${key} must be one safe package script name`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (chain.macosFinalizeScript !== undefined && (typeof chain.macosFinalizeScript !== "string" || !PACKAGE_SCRIPT_RE.test(chain.macosFinalizeScript))) {
|
|
190
|
+
errors.push("candidate.releaseChain.macosFinalizeScript must be one safe package script name");
|
|
191
|
+
}
|
|
192
|
+
if (typeof chain.qualificationRunnerVariable !== "string" || !GITHUB_VARIABLE_RE.test(chain.qualificationRunnerVariable)) {
|
|
193
|
+
errors.push("candidate.releaseChain.qualificationRunnerVariable must be one uppercase GitHub variable name");
|
|
194
|
+
}
|
|
195
|
+
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)))) {
|
|
196
|
+
errors.push("candidate.releaseChain.publishSecrets must be unique uppercase GitHub secret names");
|
|
197
|
+
}
|
|
198
|
+
if (candidate.signWindows !== true) errors.push("candidate.releaseChain requires candidate.signWindows=true");
|
|
199
|
+
if (candidate.signMacos === true && !chain.macosFinalizeScript) errors.push("candidate.releaseChain with candidate.signMacos=true requires macosFinalizeScript");
|
|
200
|
+
if (chain.macosFinalizeScript && candidate.signMacos !== true) errors.push("candidate.releaseChain.macosFinalizeScript requires candidate.signMacos=true");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
if (profile === "rust-hybrid" && lanes.includes("release-candidate") && candidate === undefined) {
|
|
@@ -323,9 +352,23 @@ function windowsSigningJob(manifest, pm, artifactPrefix) {
|
|
|
323
352
|
if (manifest.candidate?.signWindows !== true) return "";
|
|
324
353
|
const windowsRunner = manifest.candidate.os.find((value) => /^windows-/i.test(value));
|
|
325
354
|
const nodeVersion = String(manifest.matrix.node[0]);
|
|
355
|
+
const releaseChain = manifest.candidate.releaseChain;
|
|
356
|
+
const finalizeCommand = releaseChain
|
|
357
|
+
? `${pm.runPrefix} ${releaseChain.windowsFinalizeScript}`
|
|
358
|
+
: "pnpm exec right-release build --platform win --skip-checks";
|
|
359
|
+
const finalizeEnvironment = releaseChain
|
|
360
|
+
? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
|
|
361
|
+
RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/finalized-windows
|
|
362
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
|
|
363
|
+
: ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
|
|
364
|
+
LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
|
|
365
|
+
const uploadPath = releaseChain
|
|
366
|
+
? " ${{ runner.temp }}/finalized-windows/**"
|
|
367
|
+
: " dist/releases/windows/**\n .right-release/receipts/windows-*.json";
|
|
326
368
|
return `
|
|
327
369
|
windows-sign:
|
|
328
370
|
needs: [candidate]
|
|
371
|
+
if: \${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
329
372
|
environment: release
|
|
330
373
|
runs-on: ${windowsRunner}
|
|
331
374
|
permissions:
|
|
@@ -373,10 +416,9 @@ ${pm.setup}
|
|
|
373
416
|
"AZURE_ARTIFACT_SIGNING_DLIB_PATH=$tools\\client\\bin\\x64\\Azure.CodeSigning.Dlib.dll" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
374
417
|
"AZURE_SIGNTOOL_PATH=$tools\\sdk\\bin\\10.0.26100.0\\x64\\signtool.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
375
418
|
- name: Sign & finalize Windows candidate
|
|
376
|
-
run:
|
|
419
|
+
run: ${finalizeCommand}
|
|
377
420
|
env:
|
|
378
|
-
|
|
379
|
-
LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
|
|
421
|
+
${finalizeEnvironment}
|
|
380
422
|
AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
|
|
381
423
|
AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
|
|
382
424
|
AZURE_ARTIFACT_SIGNING_PROFILE: \${{ vars.AZURE_ARTIFACT_SIGNING_PROFILE }}
|
|
@@ -385,8 +427,7 @@ ${pm.setup}
|
|
|
385
427
|
with:
|
|
386
428
|
name: ${artifactPrefix}-signed-windows-${nodeVersion}
|
|
387
429
|
path: |
|
|
388
|
-
|
|
389
|
-
.right-release/receipts/windows-*.json
|
|
430
|
+
${uploadPath}
|
|
390
431
|
if-no-files-found: error
|
|
391
432
|
`.trimEnd();
|
|
392
433
|
}
|
|
@@ -395,9 +436,23 @@ function macosSigningJob(manifest, pm, artifactPrefix) {
|
|
|
395
436
|
if (manifest.candidate?.signMacos !== true) return "";
|
|
396
437
|
const macosRunner = manifest.candidate.os.find((value) => /^macos-/i.test(value));
|
|
397
438
|
const nodeVersion = String(manifest.matrix.node[0]);
|
|
439
|
+
const releaseChain = manifest.candidate.releaseChain;
|
|
440
|
+
const finalizeCommand = releaseChain
|
|
441
|
+
? `${pm.runPrefix} ${releaseChain.macosFinalizeScript}`
|
|
442
|
+
: "pnpm exec right-release build --platform mac --skip-checks";
|
|
443
|
+
const finalizeEnvironment = releaseChain
|
|
444
|
+
? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
|
|
445
|
+
RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/finalized-macos
|
|
446
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
|
|
447
|
+
: ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
|
|
448
|
+
LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
|
|
449
|
+
const uploadPath = releaseChain
|
|
450
|
+
? " ${{ runner.temp }}/finalized-macos/**"
|
|
451
|
+
: " dist/releases/mac/**\n .right-release/receipts/macos-*.json";
|
|
398
452
|
return `
|
|
399
453
|
macos-sign:
|
|
400
454
|
needs: [candidate]
|
|
455
|
+
if: \${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
401
456
|
environment: release
|
|
402
457
|
runs-on: ${macosRunner}
|
|
403
458
|
permissions:
|
|
@@ -443,10 +498,9 @@ ${pm.setup}
|
|
|
443
498
|
security list-keychain -d user -s "$keychain_path"
|
|
444
499
|
printf 'APPLE_API_KEY_PATH=%s\\n' "$key_path" >> "$GITHUB_ENV"
|
|
445
500
|
- name: Sign, notarize & finalize macOS candidate
|
|
446
|
-
run:
|
|
501
|
+
run: ${finalizeCommand}
|
|
447
502
|
env:
|
|
448
|
-
|
|
449
|
-
LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
|
|
503
|
+
${finalizeEnvironment}
|
|
450
504
|
APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
|
|
451
505
|
APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
|
|
452
506
|
APPLE_API_ISSUER: \${{ secrets.APPLE_API_ISSUER }}
|
|
@@ -455,8 +509,7 @@ ${pm.setup}
|
|
|
455
509
|
with:
|
|
456
510
|
name: ${artifactPrefix}-signed-macos-${nodeVersion}
|
|
457
511
|
path: |
|
|
458
|
-
|
|
459
|
-
.right-release/receipts/macos-*.json
|
|
512
|
+
${uploadPath}
|
|
460
513
|
if-no-files-found: error
|
|
461
514
|
- name: Remove Apple signing material
|
|
462
515
|
if: \${{ always() }}
|
|
@@ -467,6 +520,102 @@ ${pm.setup}
|
|
|
467
520
|
`.trimEnd();
|
|
468
521
|
}
|
|
469
522
|
|
|
523
|
+
function releaseChainJobs(manifest, pm, artifactPrefix) {
|
|
524
|
+
const chain = manifest.candidate?.releaseChain;
|
|
525
|
+
if (!chain) return "";
|
|
526
|
+
const windowsRunner = manifest.candidate.os.find((value) => /^windows-/i.test(value));
|
|
527
|
+
const nodeVersion = String(manifest.matrix.node[0]);
|
|
528
|
+
const macosDependency = manifest.candidate.signMacos === true ? ", macos-sign" : "";
|
|
529
|
+
const macosSuccess = manifest.candidate.signMacos === true ? " && needs.macos-sign.result == 'success'" : "";
|
|
530
|
+
const macosDownload = manifest.candidate.signMacos === true
|
|
531
|
+
? ` - name: Download same-run signed & notarized macOS release
|
|
532
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
533
|
+
with:
|
|
534
|
+
name: ${artifactPrefix}-signed-macos-${nodeVersion}
|
|
535
|
+
path: \${{ runner.temp }}/right-git-release/macos`
|
|
536
|
+
: "";
|
|
537
|
+
const publishSecretEnvironment = (chain.publishSecrets ?? [])
|
|
538
|
+
.map((name) => ` ${name}: \${{ secrets.${name} }}`)
|
|
539
|
+
.join("\n");
|
|
540
|
+
return `
|
|
541
|
+
installed-qualification:
|
|
542
|
+
needs: [candidate, windows-sign]
|
|
543
|
+
if: \${{ always() && startsWith(github.ref, 'refs/tags/v') && needs.candidate.result == 'success' }}
|
|
544
|
+
environment: release
|
|
545
|
+
runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
|
|
546
|
+
steps:
|
|
547
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
548
|
+
- name: Resolve checked-out source revision
|
|
549
|
+
shell: pwsh
|
|
550
|
+
run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
|
|
551
|
+
- name: Fail closed when protected finalization is absent
|
|
552
|
+
if: \${{ needs.windows-sign.result != 'success' }}
|
|
553
|
+
shell: pwsh
|
|
554
|
+
run: throw 'installed_qualification_failed { reason: prerequisites_absent, detail: "same-run Windows signing/finalization did not succeed" }'
|
|
555
|
+
- name: Download same-run finalized Windows release
|
|
556
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
557
|
+
with:
|
|
558
|
+
name: ${artifactPrefix}-signed-windows-${nodeVersion}
|
|
559
|
+
path: \${{ runner.temp }}/right-git-release/windows
|
|
560
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
561
|
+
with:
|
|
562
|
+
node-version: ${nodeVersion}
|
|
563
|
+
package-manager-cache: false
|
|
564
|
+
${pm.setup}
|
|
565
|
+
- run: ${pm.installCommand}
|
|
566
|
+
- name: Run installed-artifact qualification
|
|
567
|
+
run: ${pm.runPrefix} ${chain.installedQualificationScript}
|
|
568
|
+
env:
|
|
569
|
+
RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
|
|
570
|
+
RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
|
|
571
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
|
|
572
|
+
- name: Upload installed qualification evidence
|
|
573
|
+
uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
|
|
574
|
+
with:
|
|
575
|
+
name: ${artifactPrefix}-installed-qualification-${nodeVersion}
|
|
576
|
+
path: \${{ runner.temp }}/right-git-release/qualification/**
|
|
577
|
+
if-no-files-found: error
|
|
578
|
+
publish:
|
|
579
|
+
needs: [installed-qualification${macosDependency}]
|
|
580
|
+
if: \${{ startsWith(github.ref, 'refs/tags/v') && needs.installed-qualification.result == 'success'${macosSuccess} }}
|
|
581
|
+
environment: release
|
|
582
|
+
runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
|
|
583
|
+
permissions:
|
|
584
|
+
contents: write
|
|
585
|
+
steps:
|
|
586
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
587
|
+
- name: Resolve checked-out source revision
|
|
588
|
+
shell: pwsh
|
|
589
|
+
run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
|
|
590
|
+
- name: Download same-run finalized Windows release
|
|
591
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
592
|
+
with:
|
|
593
|
+
name: ${artifactPrefix}-signed-windows-${nodeVersion}
|
|
594
|
+
path: \${{ runner.temp }}/right-git-release/windows
|
|
595
|
+
${macosDownload}
|
|
596
|
+
- name: Download same-run installed qualification evidence
|
|
597
|
+
uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
|
|
598
|
+
with:
|
|
599
|
+
name: ${artifactPrefix}-installed-qualification-${nodeVersion}
|
|
600
|
+
path: \${{ runner.temp }}/right-git-release/qualification
|
|
601
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
602
|
+
with:
|
|
603
|
+
node-version: ${nodeVersion}
|
|
604
|
+
package-manager-cache: false
|
|
605
|
+
${pm.setup}
|
|
606
|
+
- run: ${pm.installCommand}
|
|
607
|
+
- name: Publish qualified signed release
|
|
608
|
+
run: ${pm.runPrefix} ${chain.publishScript}
|
|
609
|
+
env:
|
|
610
|
+
GH_TOKEN: \${{ github.token }}
|
|
611
|
+
RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
|
|
612
|
+
RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/right-git-release/macos
|
|
613
|
+
RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
|
|
614
|
+
RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
|
|
615
|
+
${publishSecretEnvironment}
|
|
616
|
+
`.trimEnd();
|
|
617
|
+
}
|
|
618
|
+
|
|
470
619
|
/**
|
|
471
620
|
* Derive one project name from the target repo's package.json, falling back
|
|
472
621
|
* to the repo directory's basename. Never throws.
|
|
@@ -513,6 +662,7 @@ export function buildRenderContext(manifest, repoPath) {
|
|
|
513
662
|
CANDIDATE_MATRIX: candidateMatrix(manifest),
|
|
514
663
|
WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
|
|
515
664
|
MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
|
|
665
|
+
RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
|
|
516
666
|
};
|
|
517
667
|
|
|
518
668
|
if (manifest.publish?.npm) {
|
package/manifest.schema.json
CHANGED
|
@@ -96,7 +96,24 @@
|
|
|
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
98
|
"signWindows": { "type": "boolean" },
|
|
99
|
-
"signMacos": { "type": "boolean" }
|
|
99
|
+
"signMacos": { "type": "boolean" },
|
|
100
|
+
"releaseChain": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"required": ["windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
|
|
103
|
+
"additionalProperties": false,
|
|
104
|
+
"properties": {
|
|
105
|
+
"windowsFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
106
|
+
"macosFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
107
|
+
"installedQualificationScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
108
|
+
"publishScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
|
|
109
|
+
"qualificationRunnerVariable": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" },
|
|
110
|
+
"publishSecrets": {
|
|
111
|
+
"type": "array",
|
|
112
|
+
"uniqueItems": true,
|
|
113
|
+
"items": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
}
|
package/manifest.test.mjs
CHANGED
|
@@ -41,6 +41,25 @@ 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);
|
|
44
63
|
});
|
|
45
64
|
|
|
46
65
|
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,50 @@ 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 release chain preserves signed candidate, installed qualification, then publish", () => {
|
|
134
|
+
const manifest = {
|
|
135
|
+
...FULL_MANIFEST,
|
|
136
|
+
lanes: ["ci", "release-candidate"],
|
|
137
|
+
profile: "rust-hybrid",
|
|
138
|
+
gate: { path: "scripts/gate.sh" },
|
|
139
|
+
candidate: {
|
|
140
|
+
buildScript: "ci:candidate",
|
|
141
|
+
checkScript: "ci:candidate-check",
|
|
142
|
+
artifactRoot: "widgets-unsigned",
|
|
143
|
+
os: ["windows-2025", "macos-15"],
|
|
144
|
+
signWindows: true,
|
|
145
|
+
signMacos: true,
|
|
146
|
+
releaseChain: {
|
|
147
|
+
windowsFinalizeScript: "release:finalize:win",
|
|
148
|
+
macosFinalizeScript: "release:finalize:mac",
|
|
149
|
+
installedQualificationScript: "release:qualify:installed",
|
|
150
|
+
publishScript: "release:publish:qualified",
|
|
151
|
+
qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL",
|
|
152
|
+
publishSecrets: ["CLOUDFLARE_API_TOKEN"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
matrix: { os: ["ubuntu-24.04", "windows-2025", "macos-15"], node: ["22.23.2"] },
|
|
156
|
+
templateVersion: "1.4.0",
|
|
157
|
+
};
|
|
158
|
+
const content = renderTemplate(
|
|
159
|
+
fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
|
|
160
|
+
buildRenderContext(manifest, "/repos/widgets"),
|
|
161
|
+
);
|
|
162
|
+
assert.match(content, /windows-sign:[\s\S]*pnpm run release:finalize:win/);
|
|
163
|
+
assert.match(content, /macos-sign:[\s\S]*pnpm run release:finalize:mac/);
|
|
164
|
+
assert.match(content, /installed-qualification:\n needs: \[candidate, windows-sign\]/);
|
|
165
|
+
assert.match(content, /installed_qualification_failed/);
|
|
166
|
+
assert.match(content, /pnpm run release:qualify:installed/);
|
|
167
|
+
assert.match(content, /publish:\n needs: \[installed-qualification, macos-sign\]/);
|
|
168
|
+
assert.match(content, /needs\.macos-sign\.result == 'success'/);
|
|
169
|
+
assert.match(content, /contents: write/);
|
|
170
|
+
assert.match(content, /pnpm run release:publish:qualified/);
|
|
171
|
+
assert.match(content, /CLOUDFLARE_API_TOKEN: \$\{\{ secrets\.CLOUDFLARE_API_TOKEN \}\}/);
|
|
172
|
+
assert.equal((content.match(/RIGHT_GIT_SOURCE_REVISION=\$\(git rev-parse HEAD\)/g) ?? []).length, 3);
|
|
173
|
+
assert.doesNotMatch(content, /RIGHT_GIT_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
|
|
174
|
+
assert.doesNotMatch(content.slice(0, content.indexOf(" windows-sign:")), /secrets\.|id-token: write|contents: write/);
|
|
175
|
+
});
|
|
176
|
+
|
|
131
177
|
test("render determinism: same manifest renders byte-identical output every time", () => {
|
|
132
178
|
const first = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
133
179
|
const second = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|