@rightkit/git 0.2.22 → 0.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/right-git.mjs CHANGED
File without changes
package/install.mjs CHANGED
@@ -13,7 +13,7 @@ import { applyPlan, planSync } from "./sync.mjs";
13
13
 
14
14
  export const DEFAULT_MATRIX = { os: ["ubuntu-latest", "macos-latest", "windows-latest"], node: ["22.13", "24"] };
15
15
  export const DEFAULT_PACKAGE_MANAGER = "pnpm@11.24.0";
16
- export const DEFAULT_TEMPLATE_VERSION = "1.7.0";
16
+ export const DEFAULT_TEMPLATE_VERSION = "1.8.0";
17
17
 
18
18
  /**
19
19
  * opts: { lanes: string[], matrix?, packageManager?, templateVersion?,
package/manifest.mjs CHANGED
@@ -167,7 +167,7 @@ export function validateManifest(manifest) {
167
167
  errors.push("rustCache must be an object");
168
168
  } else {
169
169
  for (const key of Object.keys(rustCache)) {
170
- if (key !== "workspaces") errors.push(`unknown rustCache field: ${key}`);
170
+ if (key !== "workspaces" && key !== "requiredForGate") errors.push(`unknown rustCache field: ${key}`);
171
171
  }
172
172
  if (!Array.isArray(rustCache.workspaces) || rustCache.workspaces.length === 0) {
173
173
  errors.push("rustCache.workspaces must be a non-empty array");
@@ -176,6 +176,9 @@ export function validateManifest(manifest) {
176
176
  } else if (new Set(rustCache.workspaces).size !== rustCache.workspaces.length) {
177
177
  errors.push("rustCache.workspaces entries must be unique");
178
178
  }
179
+ if (rustCache.requiredForGate !== undefined && typeof rustCache.requiredForGate !== "boolean") {
180
+ errors.push("rustCache.requiredForGate must be a boolean");
181
+ }
179
182
  }
180
183
  }
181
184
 
@@ -185,9 +188,17 @@ export function validateManifest(manifest) {
185
188
  errors.push("candidate must be an object");
186
189
  } else {
187
190
  for (const key of Object.keys(candidate)) {
188
- if (!["buildScript", "checkScript", "artifactRoot", "os", "targets", "signWindows", "signMacos", "releaseChain"].includes(key)) errors.push(`unknown candidate field: ${key}`);
191
+ if (!["buildScript", "checkScript", "admissionScript", "stageSummaryScript", "artifactRoot", "os", "targets", "signWindows", "signMacos", "releaseChain"].includes(key)) errors.push(`unknown candidate field: ${key}`);
189
192
  }
190
- for (const key of ["buildScript", "checkScript"]) {
193
+ // admissionScript & stageSummaryScript are required for EVERY release-candidate
194
+ // lane, not just signed/full-releaseChain ones: the tag-last workflow's
195
+ // `admission` job (dispatch-envelope validation) and the `candidate` job's
196
+ // stage-summary init/finalize steps are unconditional parts of the rendered
197
+ // template, independent of candidate.signWindows/signMacos/releaseChain. If
198
+ // either script were only required inside the optional releaseChain object,
199
+ // an unsigned release-candidate manifest (no releaseChain at all) would still
200
+ // render those unconditional steps with an unresolved script name.
201
+ for (const key of ["buildScript", "checkScript", "admissionScript", "stageSummaryScript"]) {
191
202
  if (typeof candidate[key] !== "string" || !PACKAGE_SCRIPT_RE.test(candidate[key])) {
192
203
  errors.push(`candidate.${key} must be one safe package script name`);
193
204
  }
@@ -249,14 +260,18 @@ export function validateManifest(manifest) {
249
260
  }
250
261
  if (candidate.releaseChain !== undefined) {
251
262
  const chain = candidate.releaseChain;
252
- const allowedChainKeys = ["windowsFinalizeScript", "macosFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable", "publishSecrets"];
263
+ // admissionScript & stageSummaryScript live on candidate directly (see
264
+ // above) — the full signed release chain adds evidenceVerificationScript
265
+ // (runs before publish, verifying aggregated same-run evidence) plus the
266
+ // finalize/qualify/publish scripts.
267
+ const allowedChainKeys = ["evidenceVerificationScript", "windowsFinalizeScript", "macosFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable", "publishSecrets"];
253
268
  if (chain === null || typeof chain !== "object" || Array.isArray(chain)) {
254
269
  errors.push("candidate.releaseChain must be an object");
255
270
  } else {
256
271
  for (const key of Object.keys(chain)) {
257
272
  if (!allowedChainKeys.includes(key)) errors.push(`unknown candidate.releaseChain field: ${key}`);
258
273
  }
259
- for (const key of ["windowsFinalizeScript", "installedQualificationScript", "publishScript"]) {
274
+ for (const key of ["evidenceVerificationScript", "windowsFinalizeScript", "installedQualificationScript", "publishScript"]) {
260
275
  if (typeof chain[key] !== "string" || !PACKAGE_SCRIPT_RE.test(chain[key])) {
261
276
  errors.push(`candidate.releaseChain.${key} must be one safe package script name`);
262
277
  }
@@ -459,7 +474,7 @@ function windowsSigningJob(manifest, pm, artifactPrefix) {
459
474
  const finalizeEnvironment = releaseChain
460
475
  ? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
461
476
  RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/finalized-windows
462
- RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
477
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}`
463
478
  : ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
464
479
  LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
465
480
  const uploadPath = releaseChain
@@ -467,8 +482,8 @@ function windowsSigningJob(manifest, pm, artifactPrefix) {
467
482
  : " dist/releases/windows/**\n .right-release/receipts/windows-*.json";
468
483
  return `
469
484
  windows-sign:
470
- needs: [candidate]
471
- if: \${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.signed_qualification == true) }}
485
+ needs: [admission, candidate]
486
+ if: \${{ needs.admission.outputs.signed_qualification == 'true' }}
472
487
  environment: release
473
488
  strategy:
474
489
  fail-fast: false
@@ -480,9 +495,9 @@ ${signingMatrix(manifest, "windows")}
480
495
  id-token: write
481
496
  steps:
482
497
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
483
- - name: Resolve checked-out source revision
484
- shell: pwsh
485
- run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
498
+ with:
499
+ ref: \${{ needs.admission.outputs.source_revision }}
500
+ persist-credentials: false
486
501
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
487
502
  with:
488
503
  node-version: ${nodeVersion}
@@ -492,10 +507,24 @@ ${pm.setup}
492
507
  with:
493
508
  targets: \${{ matrix.targetTriple }}
494
509
  - run: ${pm.installCommand}
510
+ - name: Initialize Windows signing stage summary
511
+ run: ${pm.runPrefix} ${manifest.candidate.stageSummaryScript}
512
+ env:
513
+ RIGHT_GIT_STAGE_ACTION: init
514
+ RIGHT_GIT_STAGE_ROOT: \${{ runner.temp }}/right-git-stage-summary
515
+ RIGHT_GIT_STAGE_EVIDENCE_ROOT: \${{ runner.temp }}/finalized-windows
516
+ RIGHT_GIT_STAGE: windows-sign
517
+ RIGHT_GIT_STAGE_PRODUCER: windows-sign-\${{ matrix.architecture }}
518
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
519
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
520
+ RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
521
+ RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
522
+ RIGHT_GIT_RUN_ID: \${{ github.run_id }}
523
+ RIGHT_GIT_RUN_ATTEMPT: \${{ github.run_attempt }}
495
524
  - name: Download exact unsigned Windows candidate
496
525
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
497
526
  with:
498
- name: ${artifactPrefix}-unsigned-\${{ matrix.platform }}-\${{ matrix.architecture }}-${nodeVersion}
527
+ name: ${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-candidate-\${{ matrix.platform }}-\${{ matrix.architecture }}-\${{ needs.admission.outputs.version }}-\${{ needs.admission.outputs.source_revision }}
499
528
  path: \${{ runner.temp }}/unsigned-windows-candidate
500
529
  - name: Authenticate to Azure with GitHub OIDC
501
530
  uses: azure/login@${AZURE_LOGIN_SHA} # v3
@@ -528,13 +557,38 @@ ${finalizeEnvironment}
528
557
  AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
529
558
  AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
530
559
  AZURE_ARTIFACT_SIGNING_PROFILE: \${{ vars.AZURE_ARTIFACT_SIGNING_PROFILE }}
560
+ - name: Finalize Windows signing stage summary
561
+ if: \${{ always() }}
562
+ run: ${pm.runPrefix} ${manifest.candidate.stageSummaryScript}
563
+ env:
564
+ RIGHT_GIT_STAGE_ACTION: finalize
565
+ RIGHT_GIT_STAGE_ROOT: \${{ runner.temp }}/right-git-stage-summary
566
+ RIGHT_GIT_STAGE_EVIDENCE_ROOT: \${{ runner.temp }}/finalized-windows
567
+ RIGHT_GIT_STAGE: windows-sign
568
+ RIGHT_GIT_STAGE_PRODUCER: windows-sign-\${{ matrix.architecture }}
569
+ RIGHT_GIT_STAGE_STATUS: \${{ job.status == 'success' && 'SUCCEEDED' || 'FAILED' }}
570
+ RIGHT_GIT_STAGE_EXIT_CODE: \${{ job.status == 'success' && '0' || '1' }}
571
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
572
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
573
+ RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
574
+ RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
575
+ RIGHT_GIT_RUN_ID: \${{ github.run_id }}
576
+ RIGHT_GIT_RUN_ATTEMPT: \${{ github.run_attempt }}
531
577
  - name: Upload signed Windows candidate
578
+ if: \${{ always() }}
532
579
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
533
580
  with:
534
- name: ${artifactPrefix}-signed-windows-\${{ matrix.architecture }}-${nodeVersion}
581
+ name: ${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-signed-windows-\${{ matrix.architecture }}-\${{ needs.admission.outputs.version }}-\${{ needs.admission.outputs.source_revision }}
535
582
  path: |
536
583
  ${uploadPath}
537
584
  if-no-files-found: error
585
+ - name: Upload Windows signing stage summary
586
+ if: \${{ always() }}
587
+ uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
588
+ with:
589
+ name: ${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-summary-windows-sign-\${{ matrix.architecture }}-\${{ needs.admission.outputs.version }}-\${{ needs.admission.outputs.source_revision }}
590
+ path: \${{ runner.temp }}/right-git-stage-summary/stage-summary.json
591
+ if-no-files-found: error
538
592
  `.trimEnd();
539
593
  }
540
594
 
@@ -548,7 +602,7 @@ function macosSigningJob(manifest, pm, artifactPrefix) {
548
602
  const finalizeEnvironment = releaseChain
549
603
  ? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
550
604
  RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/finalized-macos
551
- RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
605
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}`
552
606
  : ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
553
607
  LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
554
608
  const uploadPath = releaseChain
@@ -556,8 +610,8 @@ function macosSigningJob(manifest, pm, artifactPrefix) {
556
610
  : " dist/releases/mac/**\n .right-release/receipts/macos-*.json";
557
611
  return `
558
612
  macos-sign:
559
- needs: [candidate]
560
- if: \${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.signed_qualification == true) }}
613
+ needs: [admission, candidate]
614
+ if: \${{ needs.admission.outputs.signed_qualification == 'true' }}
561
615
  environment: release
562
616
  strategy:
563
617
  fail-fast: false
@@ -568,9 +622,9 @@ ${signingMatrix(manifest, "macos")}
568
622
  contents: read
569
623
  steps:
570
624
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
571
- - name: Resolve checked-out source revision
572
- shell: bash
573
- run: printf 'RIGHT_GIT_SOURCE_REVISION=%s\\n' "$(git rev-parse HEAD)" >> "$GITHUB_ENV"
625
+ with:
626
+ ref: \${{ needs.admission.outputs.source_revision }}
627
+ persist-credentials: false
574
628
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
575
629
  with:
576
630
  node-version: ${nodeVersion}
@@ -580,10 +634,24 @@ ${pm.setup}
580
634
  with:
581
635
  targets: \${{ matrix.targetTriple }}
582
636
  - run: ${pm.installCommand}
637
+ - name: Initialize macOS signing stage summary
638
+ run: ${pm.runPrefix} ${manifest.candidate.stageSummaryScript}
639
+ env:
640
+ RIGHT_GIT_STAGE_ACTION: init
641
+ RIGHT_GIT_STAGE_ROOT: \${{ runner.temp }}/right-git-stage-summary
642
+ RIGHT_GIT_STAGE_EVIDENCE_ROOT: \${{ runner.temp }}/finalized-macos
643
+ RIGHT_GIT_STAGE: macos-sign
644
+ RIGHT_GIT_STAGE_PRODUCER: macos-sign-\${{ matrix.architecture }}
645
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
646
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
647
+ RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
648
+ RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
649
+ RIGHT_GIT_RUN_ID: \${{ github.run_id }}
650
+ RIGHT_GIT_RUN_ATTEMPT: \${{ github.run_attempt }}
583
651
  - name: Download exact unsigned macOS candidate
584
652
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
585
653
  with:
586
- name: ${artifactPrefix}-unsigned-\${{ matrix.platform }}-\${{ matrix.architecture }}-${nodeVersion}
654
+ name: ${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-candidate-\${{ matrix.platform }}-\${{ matrix.architecture }}-\${{ needs.admission.outputs.version }}-\${{ needs.admission.outputs.source_revision }}
587
655
  path: \${{ runner.temp }}/unsigned-macos-candidate
588
656
  - name: Install Apple signing identity in temporary keychain
589
657
  shell: bash
@@ -615,13 +683,38 @@ ${finalizeEnvironment}
615
683
  APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
616
684
  APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
617
685
  APPLE_API_ISSUER: \${{ secrets.APPLE_API_ISSUER }}
686
+ - name: Finalize macOS signing stage summary
687
+ if: \${{ always() }}
688
+ run: ${pm.runPrefix} ${manifest.candidate.stageSummaryScript}
689
+ env:
690
+ RIGHT_GIT_STAGE_ACTION: finalize
691
+ RIGHT_GIT_STAGE_ROOT: \${{ runner.temp }}/right-git-stage-summary
692
+ RIGHT_GIT_STAGE_EVIDENCE_ROOT: \${{ runner.temp }}/finalized-macos
693
+ RIGHT_GIT_STAGE: macos-sign
694
+ RIGHT_GIT_STAGE_PRODUCER: macos-sign-\${{ matrix.architecture }}
695
+ RIGHT_GIT_STAGE_STATUS: \${{ job.status == 'success' && 'SUCCEEDED' || 'FAILED' }}
696
+ RIGHT_GIT_STAGE_EXIT_CODE: \${{ job.status == 'success' && '0' || '1' }}
697
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
698
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
699
+ RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
700
+ RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
701
+ RIGHT_GIT_RUN_ID: \${{ github.run_id }}
702
+ RIGHT_GIT_RUN_ATTEMPT: \${{ github.run_attempt }}
618
703
  - name: Upload signed & notarized macOS candidate
704
+ if: \${{ always() }}
619
705
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
620
706
  with:
621
- name: ${artifactPrefix}-signed-macos-\${{ matrix.architecture }}-${nodeVersion}
707
+ name: ${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-signed-macos-\${{ matrix.architecture }}-\${{ needs.admission.outputs.version }}-\${{ needs.admission.outputs.source_revision }}
622
708
  path: |
623
709
  ${uploadPath}
624
710
  if-no-files-found: error
711
+ - name: Upload macOS signing stage summary
712
+ if: \${{ always() }}
713
+ uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
714
+ with:
715
+ name: ${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-summary-macos-sign-\${{ matrix.architecture }}-\${{ needs.admission.outputs.version }}-\${{ needs.admission.outputs.source_revision }}
716
+ path: \${{ runner.temp }}/right-git-stage-summary/stage-summary.json
717
+ if-no-files-found: error
625
718
  - name: Remove Apple signing material
626
719
  if: \${{ always() }}
627
720
  shell: bash
@@ -638,30 +731,71 @@ function releaseChainJobs(manifest, pm, artifactPrefix) {
638
731
  const windowsRunner = windowsTarget.os;
639
732
  const windowsArchitecture = windowsTarget.architecture;
640
733
  const macosTarget = candidateTargets(manifest.candidate).find((target) => target.platform === "macos");
641
- const nodeVersion = String(manifest.matrix.node[0]);
642
- const macosDependency = manifest.candidate.signMacos === true ? ", macos-sign" : "";
643
- const macosSuccess = manifest.candidate.signMacos === true ? " && needs.macos-sign.result == 'success'" : "";
644
- const macosDownload = manifest.candidate.signMacos === true
734
+ const signMacos = manifest.candidate.signMacos === true;
735
+ const macosDependency = signMacos ? ", macos-sign" : "";
736
+ const macosSuccess = signMacos ? " && needs.macos-sign.result == 'success'" : "";
737
+ // Run- and identity-bound: matches exactly what windows-sign/macos-sign
738
+ // uploaded (see windowsSigningJob/macosSigningJob) — no matrix context is
739
+ // available in these non-matrixed jobs, so the platform architecture is a
740
+ // literal derived from the manifest's single windows/macos target.
741
+ const VERSION = "${{ needs.admission.outputs.version }}";
742
+ const SOURCE_REVISION = "${{ needs.admission.outputs.source_revision }}";
743
+ const windowsSignedArtifact = `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-signed-windows-${windowsArchitecture}-${VERSION}-${SOURCE_REVISION}`;
744
+ const windowsSummaryArtifact = `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-summary-windows-sign-${windowsArchitecture}-${VERSION}-${SOURCE_REVISION}`;
745
+ const macosSignedArtifact = signMacos ? `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-signed-macos-${macosTarget.architecture}-${VERSION}-${SOURCE_REVISION}` : "";
746
+ const macosSummaryArtifact = signMacos ? `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-summary-macos-sign-${macosTarget.architecture}-${VERSION}-${SOURCE_REVISION}` : "";
747
+ // Candidate stage summaries are per (platform, architecture) — see the
748
+ // `Upload candidate stage summary` step in the template. releaseChain
749
+ // requires at most one target per signing platform (validateManifest), so
750
+ // exactly one candidate-windows summary and at most one candidate-macos
751
+ // summary exist for this manifest.
752
+ const candidateWindowsSummaryArtifact = `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-summary-candidate-windows-${windowsArchitecture}-${VERSION}-${SOURCE_REVISION}`;
753
+ const candidateMacosSummaryArtifact = signMacos ? `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-summary-candidate-macos-${macosTarget.architecture}-${VERSION}-${SOURCE_REVISION}` : "";
754
+ const qualificationArtifact = `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-installed-qualification-${VERSION}-${SOURCE_REVISION}`;
755
+ const qualificationSummaryArtifact = `${artifactPrefix}-\${{ github.run_id }}-\${{ github.run_attempt }}-summary-installed-qualification-${VERSION}-${SOURCE_REVISION}`;
756
+ const macosDownload = signMacos
645
757
  ? ` - name: Download same-run signed & notarized macOS release
646
758
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
647
759
  with:
648
- name: ${artifactPrefix}-signed-macos-${macosTarget.architecture}-${nodeVersion}
760
+ name: ${macosSignedArtifact}
649
761
  path: \${{ runner.temp }}/right-git-release/macos`
650
762
  : "";
763
+ const macosSummaryDownload = signMacos
764
+ ? ` - name: Download same-run macOS signing stage summary
765
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
766
+ with:
767
+ name: ${macosSummaryArtifact}
768
+ path: \${{ runner.temp }}/right-git-release/stage-summaries/macos-sign`
769
+ : "";
770
+ const candidateMacosSummaryDownload = signMacos
771
+ ? ` - name: Download same-run candidate (macOS) stage summary
772
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
773
+ with:
774
+ name: ${candidateMacosSummaryArtifact}
775
+ path: \${{ runner.temp }}/right-git-release/stage-summaries/candidate-macos`
776
+ : "";
777
+ const finalizedMacosEnv = signMacos ? `\n RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/right-git-release/macos` : "";
778
+ // Evidence must be bound to this exact run — not merely this
779
+ // version+source_revision — so a stale artifact from a different run
780
+ // cannot satisfy verification. Every job/step from installed-qualification
781
+ // onward carries RIGHT_GIT_RUN_ID/RIGHT_GIT_RUN_ATTEMPT alongside version
782
+ // and source_revision.
783
+ const runIdentityEnv = ` RIGHT_GIT_RUN_ID: \${{ github.run_id }}
784
+ RIGHT_GIT_RUN_ATTEMPT: \${{ github.run_attempt }}`;
651
785
  const publishSecretEnvironment = (chain.publishSecrets ?? [])
652
786
  .map((name) => ` ${name}: \${{ secrets.${name} }}`)
653
787
  .join("\n");
654
788
  return `
655
789
  installed-qualification:
656
- needs: [candidate, windows-sign]
657
- if: \${{ always() && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.signed_qualification == true)) && needs.candidate.result == 'success' }}
790
+ needs: [admission, candidate, windows-sign]
791
+ if: \${{ always() && needs.admission.outputs.signed_qualification == 'true' && needs.candidate.result == 'success' }}
658
792
  environment: release
659
793
  runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
660
794
  steps:
661
795
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
662
- - name: Resolve checked-out source revision
663
- shell: pwsh
664
- run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
796
+ with:
797
+ ref: \${{ needs.admission.outputs.source_revision }}
798
+ persist-credentials: false
665
799
  - name: Fail closed when protected finalization is absent
666
800
  if: \${{ needs.windows-sign.result != 'success' }}
667
801
  shell: pwsh
@@ -670,64 +804,127 @@ function releaseChainJobs(manifest, pm, artifactPrefix) {
670
804
  - name: Download same-run finalized Windows release
671
805
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
672
806
  with:
673
- name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
807
+ name: ${windowsSignedArtifact}
674
808
  path: \${{ runner.temp }}/right-git-release/windows
675
809
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
676
810
  with:
677
- node-version: ${nodeVersion}
811
+ node-version: ${String(manifest.matrix.node[0])}
678
812
  package-manager-cache: false
679
813
  ${pm.setup}
680
814
  - run: ${pm.installCommand}
815
+ - name: Initialize installed-qualification stage summary
816
+ run: ${pm.runPrefix} ${manifest.candidate.stageSummaryScript}
817
+ env:
818
+ RIGHT_GIT_STAGE_ACTION: init
819
+ RIGHT_GIT_STAGE_ROOT: \${{ runner.temp }}/right-git-stage-summary
820
+ RIGHT_GIT_STAGE_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
821
+ RIGHT_GIT_STAGE: installed-qualification
822
+ RIGHT_GIT_STAGE_PRODUCER: installed-qualification
823
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
824
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
825
+ ${runIdentityEnv}
681
826
  - name: Run installed-artifact qualification
682
827
  run: ${pm.runPrefix} ${chain.installedQualificationScript}
683
828
  env:
684
829
  GH_TOKEN: \${{ github.token }}
685
830
  RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
686
831
  RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
687
- RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
832
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
833
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
834
+ - name: Finalize installed-qualification stage summary
835
+ if: \${{ always() }}
836
+ run: ${pm.runPrefix} ${manifest.candidate.stageSummaryScript}
837
+ env:
838
+ RIGHT_GIT_STAGE_ACTION: finalize
839
+ RIGHT_GIT_STAGE_ROOT: \${{ runner.temp }}/right-git-stage-summary
840
+ RIGHT_GIT_STAGE_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
841
+ RIGHT_GIT_STAGE: installed-qualification
842
+ RIGHT_GIT_STAGE_PRODUCER: installed-qualification
843
+ RIGHT_GIT_STAGE_STATUS: \${{ job.status == 'success' && 'SUCCEEDED' || 'FAILED' }}
844
+ RIGHT_GIT_STAGE_EXIT_CODE: \${{ job.status == 'success' && '0' || '1' }}
845
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
846
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
847
+ ${runIdentityEnv}
688
848
  - name: Upload installed qualification evidence
849
+ if: \${{ always() }}
689
850
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
690
851
  with:
691
- name: ${artifactPrefix}-installed-qualification-${nodeVersion}
852
+ name: ${qualificationArtifact}
692
853
  path: \${{ runner.temp }}/right-git-release/qualification/**
693
854
  if-no-files-found: error
855
+ - name: Upload installed-qualification stage summary
856
+ if: \${{ always() }}
857
+ uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
858
+ with:
859
+ name: ${qualificationSummaryArtifact}
860
+ path: \${{ runner.temp }}/right-git-stage-summary/stage-summary.json
861
+ if-no-files-found: error
694
862
  publish:
695
- needs: [installed-qualification${macosDependency}]
696
- if: \${{ startsWith(github.ref, 'refs/tags/v') && needs.installed-qualification.result == 'success'${macosSuccess} }}
863
+ needs: [admission, installed-qualification${macosDependency}]
864
+ if: \${{ needs.admission.outputs.publish == 'true' && needs.installed-qualification.result == 'success'${macosSuccess} }}
697
865
  environment: release
698
866
  runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
699
867
  permissions:
700
868
  contents: write
701
869
  steps:
702
870
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
703
- - name: Resolve checked-out source revision
704
- shell: pwsh
705
- run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
871
+ with:
872
+ ref: \${{ needs.admission.outputs.source_revision }}
873
+ persist-credentials: false
706
874
  - name: Download same-run finalized Windows release
707
875
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
708
876
  with:
709
- name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
877
+ name: ${windowsSignedArtifact}
710
878
  path: \${{ runner.temp }}/right-git-release/windows
711
879
  ${macosDownload}
712
880
  - name: Download same-run installed qualification evidence
713
881
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
714
882
  with:
715
- name: ${artifactPrefix}-installed-qualification-${nodeVersion}
883
+ name: ${qualificationArtifact}
716
884
  path: \${{ runner.temp }}/right-git-release/qualification
885
+ - name: Download same-run candidate (Windows) stage summary
886
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
887
+ with:
888
+ name: ${candidateWindowsSummaryArtifact}
889
+ path: \${{ runner.temp }}/right-git-release/stage-summaries/candidate-windows
890
+ ${candidateMacosSummaryDownload}
891
+ - name: Download same-run Windows signing stage summary
892
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
893
+ with:
894
+ name: ${windowsSummaryArtifact}
895
+ path: \${{ runner.temp }}/right-git-release/stage-summaries/windows-sign
896
+ ${macosSummaryDownload}
897
+ - name: Download same-run installed-qualification stage summary
898
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
899
+ with:
900
+ name: ${qualificationSummaryArtifact}
901
+ path: \${{ runner.temp }}/right-git-release/stage-summaries/installed-qualification
717
902
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
718
903
  with:
719
- node-version: ${nodeVersion}
904
+ node-version: ${String(manifest.matrix.node[0])}
720
905
  package-manager-cache: false
721
906
  ${pm.setup}
722
907
  - run: ${pm.installCommand}
908
+ - name: Verify aggregated release evidence
909
+ run: ${pm.runPrefix} ${chain.evidenceVerificationScript}
910
+ env:
911
+ GH_TOKEN: \${{ github.token }}
912
+ RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows${finalizedMacosEnv}
913
+ RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
914
+ RIGHT_GIT_STAGE_SUMMARY_ROOT: \${{ runner.temp }}/right-git-release/stage-summaries
915
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
916
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
917
+ ${runIdentityEnv}
723
918
  - name: Publish qualified signed release
724
919
  run: ${pm.runPrefix} ${chain.publishScript}
725
920
  env:
726
921
  GH_TOKEN: \${{ github.token }}
727
- RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
728
- RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/right-git-release/macos
922
+ RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows${finalizedMacosEnv}
729
923
  RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
730
- RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
924
+ RIGHT_GIT_STAGE_SUMMARY_ROOT: \${{ runner.temp }}/right-git-release/stage-summaries
925
+ RIGHT_GIT_RELEASE_VERSION: \${{ needs.admission.outputs.version }}
926
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}
927
+ ${runIdentityEnv}
731
928
  ${publishSecretEnvironment}
732
929
  `.trimEnd();
733
930
  }
@@ -774,18 +971,39 @@ export function buildRenderContext(manifest, repoPath) {
774
971
  PACKAGE_MANAGER_SETUP: pm.setup,
775
972
  ARTIFACT_PREFIX: artifactPrefix,
776
973
  TEMPLATE_VERSION: manifest.templateVersion,
777
- GATE_PATH: manifest.gate?.path,
778
974
  RUST_CACHE_WORKSPACES: rustWorkspaces.map((workspace) => ` ${workspace} -> target`).join("\n"),
779
- CANDIDATE_BUILD_SCRIPT: manifest.candidate?.buildScript,
780
- CANDIDATE_CHECK_SCRIPT: manifest.candidate?.checkScript,
781
- CANDIDATE_ARTIFACT_ROOT: manifest.candidate?.artifactRoot,
782
- CANDIDATE_OS_MATRIX: yamlFlowStringList(manifest.candidate?.os ?? manifest.matrix.os),
783
- CANDIDATE_MATRIX: candidateMatrix(manifest),
975
+ RUST_REQUIRED_FOR_GATE: manifest.rustCache?.requiredForGate === true ? "true" : "false",
784
976
  WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
785
977
  MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
786
978
  RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
787
979
  };
788
980
 
981
+ // Every key below is populated ONLY when its source manifest section is
982
+ // actually present — mirroring the publish.npm/homebrew/winget pattern.
983
+ // This keeps the fail-closed check just below honest: a key that is
984
+ // legitimately inapplicable to this manifest (e.g. CANDIDATE_* for a
985
+ // non-rust-hybrid manifest, GATE_PATH for a non-rust-hybrid profile) is
986
+ // simply ABSENT from the context rather than present-with-undefined, so
987
+ // render.mjs's own `Object.hasOwn` check does the right thing if a
988
+ // template ever references a token this manifest didn't opt into.
989
+ if (manifest.gate) {
990
+ context.GATE_PATH = manifest.gate.path;
991
+ }
992
+ if (manifest.candidate) {
993
+ context.CANDIDATE_BUILD_SCRIPT = manifest.candidate.buildScript;
994
+ context.CANDIDATE_CHECK_SCRIPT = manifest.candidate.checkScript;
995
+ context.CANDIDATE_ARTIFACT_ROOT = manifest.candidate.artifactRoot;
996
+ context.CANDIDATE_OS_MATRIX = yamlFlowStringList(manifest.candidate.os ?? manifest.matrix.os);
997
+ context.CANDIDATE_MATRIX = candidateMatrix(manifest);
998
+ // admissionScript & stageSummaryScript are required candidate fields
999
+ // (see validateManifest) whenever release-candidate is configured at
1000
+ // all — the admission job and the candidate job's stage-summary steps
1001
+ // are unconditional parts of the rendered template, independent of
1002
+ // signing/releaseChain.
1003
+ context.RELEASE_ADMISSION_SCRIPT = manifest.candidate.admissionScript;
1004
+ context.RELEASE_STAGE_SUMMARY_SCRIPT = manifest.candidate.stageSummaryScript;
1005
+ }
1006
+
789
1007
  if (manifest.publish?.npm) {
790
1008
  context.NPM_PROVENANCE_FLAG = manifest.publish.npm.provenance ? " --provenance" : "";
791
1009
  }
@@ -797,5 +1015,21 @@ export function buildRenderContext(manifest, repoPath) {
797
1015
  context.WINGET_PACKAGE_IDENTIFIER = manifest.publish.winget.packageIdentifier;
798
1016
  }
799
1017
 
1018
+ // F-undefined-script fix: render.mjs's `Object.hasOwn` check only proves a
1019
+ // key EXISTS in the context — it does not prove the key resolved to a real
1020
+ // value. Every field above that is sourced from an optional manifest path
1021
+ // (candidate.*, candidate.releaseChain.* via the job-builder functions,
1022
+ // gate.path) must resolve to a real string once its guarding `if` above
1023
+ // decided the section applies; a bug in one of those guards (or a
1024
+ // mis-nested manifest field) must fail the render loudly here rather than
1025
+ // silently emit a literal "undefined" into a workflow file that GitHub
1026
+ // Actions will then try to execute as a real step (e.g. `run: pnpm run
1027
+ // undefined`).
1028
+ for (const [key, value] of Object.entries(context)) {
1029
+ if (value === undefined || value === null) {
1030
+ throw new Error(`buildRenderContext: placeholder \${${key}} resolved to ${value} — check the manifest's candidate/releaseChain/gate/publish configuration`);
1031
+ }
1032
+ }
1033
+
800
1034
  return context;
801
1035
  }
@@ -95,7 +95,8 @@
95
95
  "minItems": 1,
96
96
  "uniqueItems": true,
97
97
  "items": { "type": "string", "pattern": "^(?:\\.|(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*)$" }
98
- }
98
+ },
99
+ "requiredForGate": { "type": "boolean" }
99
100
  }
100
101
  },
101
102
  "gate": {
@@ -104,11 +105,21 @@
104
105
  },
105
106
  "candidate": {
106
107
  "type": "object",
107
- "required": ["buildScript", "checkScript", "artifactRoot", "os"],
108
+ "required": ["buildScript", "checkScript", "admissionScript", "stageSummaryScript", "artifactRoot", "os"],
108
109
  "additionalProperties": false,
109
110
  "properties": {
110
111
  "buildScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
111
112
  "checkScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
113
+ "admissionScript": {
114
+ "type": "string",
115
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$",
116
+ "description": "Runs in the unconditional `admission` job (workflow_dispatch envelope validation). Required for every release-candidate lane, signed or not."
117
+ },
118
+ "stageSummaryScript": {
119
+ "type": "string",
120
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$",
121
+ "description": "Runs (init/finalize) in the `candidate` job and, when configured, the windows-sign/macos-sign jobs. Required for every release-candidate lane, signed or not."
122
+ },
112
123
  "artifactRoot": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" },
113
124
  "os": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
114
125
  "targets": {
@@ -129,9 +140,14 @@
129
140
  "signMacos": { "type": "boolean" },
130
141
  "releaseChain": {
131
142
  "type": "object",
132
- "required": ["windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
143
+ "required": ["evidenceVerificationScript", "windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
133
144
  "additionalProperties": false,
134
145
  "properties": {
146
+ "evidenceVerificationScript": {
147
+ "type": "string",
148
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$",
149
+ "description": "Runs in the `publish` job before the publish step, verifying the aggregated same-run signed/qualification evidence."
150
+ },
135
151
  "windowsFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
136
152
  "macosFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
137
153
  "installedQualificationScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },