@rightkit/git 0.2.6 → 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 +167 -11
- package/manifest.schema.json +18 -1
- package/manifest.test.mjs +19 -0
- package/package.json +1 -1
- package/render.test.mjs +52 -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:
|
|
@@ -333,6 +376,9 @@ function windowsSigningJob(manifest, pm, artifactPrefix) {
|
|
|
333
376
|
id-token: write
|
|
334
377
|
steps:
|
|
335
378
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
379
|
+
- name: Resolve checked-out source revision
|
|
380
|
+
shell: pwsh
|
|
381
|
+
run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
|
|
336
382
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
337
383
|
with:
|
|
338
384
|
node-version: ${nodeVersion}
|
|
@@ -370,10 +416,9 @@ ${pm.setup}
|
|
|
370
416
|
"AZURE_ARTIFACT_SIGNING_DLIB_PATH=$tools\\client\\bin\\x64\\Azure.CodeSigning.Dlib.dll" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
371
417
|
"AZURE_SIGNTOOL_PATH=$tools\\sdk\\bin\\10.0.26100.0\\x64\\signtool.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
372
418
|
- name: Sign & finalize Windows candidate
|
|
373
|
-
run:
|
|
419
|
+
run: ${finalizeCommand}
|
|
374
420
|
env:
|
|
375
|
-
|
|
376
|
-
LEGION_SOURCE_REVISION: \${{ github.sha }}
|
|
421
|
+
${finalizeEnvironment}
|
|
377
422
|
AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
|
|
378
423
|
AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
|
|
379
424
|
AZURE_ARTIFACT_SIGNING_PROFILE: \${{ vars.AZURE_ARTIFACT_SIGNING_PROFILE }}
|
|
@@ -382,8 +427,7 @@ ${pm.setup}
|
|
|
382
427
|
with:
|
|
383
428
|
name: ${artifactPrefix}-signed-windows-${nodeVersion}
|
|
384
429
|
path: |
|
|
385
|
-
|
|
386
|
-
.right-release/receipts/windows-*.json
|
|
430
|
+
${uploadPath}
|
|
387
431
|
if-no-files-found: error
|
|
388
432
|
`.trimEnd();
|
|
389
433
|
}
|
|
@@ -392,15 +436,32 @@ function macosSigningJob(manifest, pm, artifactPrefix) {
|
|
|
392
436
|
if (manifest.candidate?.signMacos !== true) return "";
|
|
393
437
|
const macosRunner = manifest.candidate.os.find((value) => /^macos-/i.test(value));
|
|
394
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";
|
|
395
452
|
return `
|
|
396
453
|
macos-sign:
|
|
397
454
|
needs: [candidate]
|
|
455
|
+
if: \${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
398
456
|
environment: release
|
|
399
457
|
runs-on: ${macosRunner}
|
|
400
458
|
permissions:
|
|
401
459
|
contents: read
|
|
402
460
|
steps:
|
|
403
461
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
462
|
+
- name: Resolve checked-out source revision
|
|
463
|
+
shell: bash
|
|
464
|
+
run: printf 'RIGHT_GIT_SOURCE_REVISION=%s\\n' "$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
|
404
465
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
405
466
|
with:
|
|
406
467
|
node-version: ${nodeVersion}
|
|
@@ -437,10 +498,9 @@ ${pm.setup}
|
|
|
437
498
|
security list-keychain -d user -s "$keychain_path"
|
|
438
499
|
printf 'APPLE_API_KEY_PATH=%s\\n' "$key_path" >> "$GITHUB_ENV"
|
|
439
500
|
- name: Sign, notarize & finalize macOS candidate
|
|
440
|
-
run:
|
|
501
|
+
run: ${finalizeCommand}
|
|
441
502
|
env:
|
|
442
|
-
|
|
443
|
-
LEGION_SOURCE_REVISION: \${{ github.sha }}
|
|
503
|
+
${finalizeEnvironment}
|
|
444
504
|
APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
|
|
445
505
|
APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
|
|
446
506
|
APPLE_API_ISSUER: \${{ secrets.APPLE_API_ISSUER }}
|
|
@@ -449,8 +509,7 @@ ${pm.setup}
|
|
|
449
509
|
with:
|
|
450
510
|
name: ${artifactPrefix}-signed-macos-${nodeVersion}
|
|
451
511
|
path: |
|
|
452
|
-
|
|
453
|
-
.right-release/receipts/macos-*.json
|
|
512
|
+
${uploadPath}
|
|
454
513
|
if-no-files-found: error
|
|
455
514
|
- name: Remove Apple signing material
|
|
456
515
|
if: \${{ always() }}
|
|
@@ -461,6 +520,102 @@ ${pm.setup}
|
|
|
461
520
|
`.trimEnd();
|
|
462
521
|
}
|
|
463
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
|
+
|
|
464
619
|
/**
|
|
465
620
|
* Derive one project name from the target repo's package.json, falling back
|
|
466
621
|
* to the repo directory's basename. Never throws.
|
|
@@ -507,6 +662,7 @@ export function buildRenderContext(manifest, repoPath) {
|
|
|
507
662
|
CANDIDATE_MATRIX: candidateMatrix(manifest),
|
|
508
663
|
WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
|
|
509
664
|
MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
|
|
665
|
+
RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
|
|
510
666
|
};
|
|
511
667
|
|
|
512
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,11 +90,15 @@ 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/);
|
|
96
97
|
assert.match(content, /right-release build --platform win --skip-checks/);
|
|
97
98
|
assert.match(content, /Artifact Signing client hash mismatch/);
|
|
99
|
+
assert.match(content, /RIGHT_GIT_SOURCE_REVISION=\$\(git rev-parse HEAD\)/);
|
|
100
|
+
assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
|
|
101
|
+
assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
|
|
98
102
|
assert.doesNotMatch(content, /secrets\.|client-secret|private.key/i);
|
|
99
103
|
});
|
|
100
104
|
|
|
@@ -113,15 +117,63 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
|
|
|
113
117
|
buildRenderContext(manifest, "/repos/widgets"),
|
|
114
118
|
);
|
|
115
119
|
assert.match(content, /macos-sign:/);
|
|
120
|
+
assert.match(content, /macos-sign:\n needs: \[candidate\]\n if: \$\{\{ startsWith\(github\.ref, 'refs\/tags\/v'\) \}\}/);
|
|
116
121
|
assert.match(content, /environment: release/);
|
|
117
122
|
assert.match(content, /secrets\.APPLE_CERTIFICATE_BASE64/);
|
|
118
123
|
assert.match(content, /temporary keychain/i);
|
|
119
124
|
assert.match(content, /right-release build --platform mac --skip-checks/);
|
|
120
125
|
assert.match(content, /secrets\.APPLE_API_KEY_BASE64/);
|
|
126
|
+
assert.match(content, /RIGHT_GIT_SOURCE_REVISION=%s\\n.*git rev-parse HEAD/);
|
|
127
|
+
assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
|
|
128
|
+
assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
|
|
121
129
|
assert.match(content, /security delete-keychain/);
|
|
122
130
|
assert.doesNotMatch(content, /pull_request_target/);
|
|
123
131
|
});
|
|
124
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
|
+
|
|
125
177
|
test("render determinism: same manifest renders byte-identical output every time", () => {
|
|
126
178
|
const first = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|
|
127
179
|
const second = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
|