@rightkit/git 0.2.23 → 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/manifest.mjs CHANGED
@@ -188,9 +188,17 @@ export function validateManifest(manifest) {
188
188
  errors.push("candidate must be an object");
189
189
  } else {
190
190
  for (const key of Object.keys(candidate)) {
191
- 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}`);
192
192
  }
193
- 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"]) {
194
202
  if (typeof candidate[key] !== "string" || !PACKAGE_SCRIPT_RE.test(candidate[key])) {
195
203
  errors.push(`candidate.${key} must be one safe package script name`);
196
204
  }
@@ -252,14 +260,18 @@ export function validateManifest(manifest) {
252
260
  }
253
261
  if (candidate.releaseChain !== undefined) {
254
262
  const chain = candidate.releaseChain;
255
- 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"];
256
268
  if (chain === null || typeof chain !== "object" || Array.isArray(chain)) {
257
269
  errors.push("candidate.releaseChain must be an object");
258
270
  } else {
259
271
  for (const key of Object.keys(chain)) {
260
272
  if (!allowedChainKeys.includes(key)) errors.push(`unknown candidate.releaseChain field: ${key}`);
261
273
  }
262
- for (const key of ["windowsFinalizeScript", "installedQualificationScript", "publishScript"]) {
274
+ for (const key of ["evidenceVerificationScript", "windowsFinalizeScript", "installedQualificationScript", "publishScript"]) {
263
275
  if (typeof chain[key] !== "string" || !PACKAGE_SCRIPT_RE.test(chain[key])) {
264
276
  errors.push(`candidate.releaseChain.${key} must be one safe package script name`);
265
277
  }
@@ -462,7 +474,7 @@ function windowsSigningJob(manifest, pm, artifactPrefix) {
462
474
  const finalizeEnvironment = releaseChain
463
475
  ? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
464
476
  RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/finalized-windows
465
- RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
477
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}`
466
478
  : ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
467
479
  LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
468
480
  const uploadPath = releaseChain
@@ -470,8 +482,8 @@ function windowsSigningJob(manifest, pm, artifactPrefix) {
470
482
  : " dist/releases/windows/**\n .right-release/receipts/windows-*.json";
471
483
  return `
472
484
  windows-sign:
473
- needs: [candidate]
474
- 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' }}
475
487
  environment: release
476
488
  strategy:
477
489
  fail-fast: false
@@ -483,9 +495,9 @@ ${signingMatrix(manifest, "windows")}
483
495
  id-token: write
484
496
  steps:
485
497
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
486
- - name: Resolve checked-out source revision
487
- shell: pwsh
488
- 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
489
501
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
490
502
  with:
491
503
  node-version: ${nodeVersion}
@@ -495,10 +507,24 @@ ${pm.setup}
495
507
  with:
496
508
  targets: \${{ matrix.targetTriple }}
497
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 }}
498
524
  - name: Download exact unsigned Windows candidate
499
525
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
500
526
  with:
501
- 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 }}
502
528
  path: \${{ runner.temp }}/unsigned-windows-candidate
503
529
  - name: Authenticate to Azure with GitHub OIDC
504
530
  uses: azure/login@${AZURE_LOGIN_SHA} # v3
@@ -531,13 +557,38 @@ ${finalizeEnvironment}
531
557
  AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
532
558
  AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
533
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 }}
534
577
  - name: Upload signed Windows candidate
578
+ if: \${{ always() }}
535
579
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
536
580
  with:
537
- 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 }}
538
582
  path: |
539
583
  ${uploadPath}
540
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
541
592
  `.trimEnd();
542
593
  }
543
594
 
@@ -551,7 +602,7 @@ function macosSigningJob(manifest, pm, artifactPrefix) {
551
602
  const finalizeEnvironment = releaseChain
552
603
  ? ` RIGHT_GIT_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
553
604
  RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/finalized-macos
554
- RIGHT_GIT_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`
605
+ RIGHT_GIT_SOURCE_REVISION: \${{ needs.admission.outputs.source_revision }}`
555
606
  : ` LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
556
607
  LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}`;
557
608
  const uploadPath = releaseChain
@@ -559,8 +610,8 @@ function macosSigningJob(manifest, pm, artifactPrefix) {
559
610
  : " dist/releases/mac/**\n .right-release/receipts/macos-*.json";
560
611
  return `
561
612
  macos-sign:
562
- needs: [candidate]
563
- 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' }}
564
615
  environment: release
565
616
  strategy:
566
617
  fail-fast: false
@@ -571,9 +622,9 @@ ${signingMatrix(manifest, "macos")}
571
622
  contents: read
572
623
  steps:
573
624
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
574
- - name: Resolve checked-out source revision
575
- shell: bash
576
- 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
577
628
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
578
629
  with:
579
630
  node-version: ${nodeVersion}
@@ -583,10 +634,24 @@ ${pm.setup}
583
634
  with:
584
635
  targets: \${{ matrix.targetTriple }}
585
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 }}
586
651
  - name: Download exact unsigned macOS candidate
587
652
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
588
653
  with:
589
- 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 }}
590
655
  path: \${{ runner.temp }}/unsigned-macos-candidate
591
656
  - name: Install Apple signing identity in temporary keychain
592
657
  shell: bash
@@ -618,13 +683,38 @@ ${finalizeEnvironment}
618
683
  APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
619
684
  APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
620
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 }}
621
703
  - name: Upload signed & notarized macOS candidate
704
+ if: \${{ always() }}
622
705
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
623
706
  with:
624
- 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 }}
625
708
  path: |
626
709
  ${uploadPath}
627
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
628
718
  - name: Remove Apple signing material
629
719
  if: \${{ always() }}
630
720
  shell: bash
@@ -641,30 +731,71 @@ function releaseChainJobs(manifest, pm, artifactPrefix) {
641
731
  const windowsRunner = windowsTarget.os;
642
732
  const windowsArchitecture = windowsTarget.architecture;
643
733
  const macosTarget = candidateTargets(manifest.candidate).find((target) => target.platform === "macos");
644
- const nodeVersion = String(manifest.matrix.node[0]);
645
- const macosDependency = manifest.candidate.signMacos === true ? ", macos-sign" : "";
646
- const macosSuccess = manifest.candidate.signMacos === true ? " && needs.macos-sign.result == 'success'" : "";
647
- 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
648
757
  ? ` - name: Download same-run signed & notarized macOS release
649
758
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
650
759
  with:
651
- name: ${artifactPrefix}-signed-macos-${macosTarget.architecture}-${nodeVersion}
760
+ name: ${macosSignedArtifact}
652
761
  path: \${{ runner.temp }}/right-git-release/macos`
653
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 }}`;
654
785
  const publishSecretEnvironment = (chain.publishSecrets ?? [])
655
786
  .map((name) => ` ${name}: \${{ secrets.${name} }}`)
656
787
  .join("\n");
657
788
  return `
658
789
  installed-qualification:
659
- needs: [candidate, windows-sign]
660
- 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' }}
661
792
  environment: release
662
793
  runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
663
794
  steps:
664
795
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
665
- - name: Resolve checked-out source revision
666
- shell: pwsh
667
- 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
668
799
  - name: Fail closed when protected finalization is absent
669
800
  if: \${{ needs.windows-sign.result != 'success' }}
670
801
  shell: pwsh
@@ -673,64 +804,127 @@ function releaseChainJobs(manifest, pm, artifactPrefix) {
673
804
  - name: Download same-run finalized Windows release
674
805
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
675
806
  with:
676
- name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
807
+ name: ${windowsSignedArtifact}
677
808
  path: \${{ runner.temp }}/right-git-release/windows
678
809
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
679
810
  with:
680
- node-version: ${nodeVersion}
811
+ node-version: ${String(manifest.matrix.node[0])}
681
812
  package-manager-cache: false
682
813
  ${pm.setup}
683
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}
684
826
  - name: Run installed-artifact qualification
685
827
  run: ${pm.runPrefix} ${chain.installedQualificationScript}
686
828
  env:
687
829
  GH_TOKEN: \${{ github.token }}
688
830
  RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
689
831
  RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
690
- 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}
691
848
  - name: Upload installed qualification evidence
849
+ if: \${{ always() }}
692
850
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
693
851
  with:
694
- name: ${artifactPrefix}-installed-qualification-${nodeVersion}
852
+ name: ${qualificationArtifact}
695
853
  path: \${{ runner.temp }}/right-git-release/qualification/**
696
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
697
862
  publish:
698
- needs: [installed-qualification${macosDependency}]
699
- 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} }}
700
865
  environment: release
701
866
  runs-on: \${{ vars.${chain.qualificationRunnerVariable} || '${windowsRunner}' }}
702
867
  permissions:
703
868
  contents: write
704
869
  steps:
705
870
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
706
- - name: Resolve checked-out source revision
707
- shell: pwsh
708
- 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
709
874
  - name: Download same-run finalized Windows release
710
875
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
711
876
  with:
712
- name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
877
+ name: ${windowsSignedArtifact}
713
878
  path: \${{ runner.temp }}/right-git-release/windows
714
879
  ${macosDownload}
715
880
  - name: Download same-run installed qualification evidence
716
881
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
717
882
  with:
718
- name: ${artifactPrefix}-installed-qualification-${nodeVersion}
883
+ name: ${qualificationArtifact}
719
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
720
902
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
721
903
  with:
722
- node-version: ${nodeVersion}
904
+ node-version: ${String(manifest.matrix.node[0])}
723
905
  package-manager-cache: false
724
906
  ${pm.setup}
725
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}
726
918
  - name: Publish qualified signed release
727
919
  run: ${pm.runPrefix} ${chain.publishScript}
728
920
  env:
729
921
  GH_TOKEN: \${{ github.token }}
730
- RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows
731
- RIGHT_GIT_FINALIZED_MACOS_ROOT: \${{ runner.temp }}/right-git-release/macos
922
+ RIGHT_GIT_FINALIZED_WINDOWS_ROOT: \${{ runner.temp }}/right-git-release/windows${finalizedMacosEnv}
732
923
  RIGHT_GIT_QUALIFICATION_EVIDENCE_ROOT: \${{ runner.temp }}/right-git-release/qualification
733
- 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}
734
928
  ${publishSecretEnvironment}
735
929
  `.trimEnd();
736
930
  }
@@ -777,19 +971,39 @@ export function buildRenderContext(manifest, repoPath) {
777
971
  PACKAGE_MANAGER_SETUP: pm.setup,
778
972
  ARTIFACT_PREFIX: artifactPrefix,
779
973
  TEMPLATE_VERSION: manifest.templateVersion,
780
- GATE_PATH: manifest.gate?.path,
781
974
  RUST_CACHE_WORKSPACES: rustWorkspaces.map((workspace) => ` ${workspace} -> target`).join("\n"),
782
975
  RUST_REQUIRED_FOR_GATE: manifest.rustCache?.requiredForGate === true ? "true" : "false",
783
- CANDIDATE_BUILD_SCRIPT: manifest.candidate?.buildScript,
784
- CANDIDATE_CHECK_SCRIPT: manifest.candidate?.checkScript,
785
- CANDIDATE_ARTIFACT_ROOT: manifest.candidate?.artifactRoot,
786
- CANDIDATE_OS_MATRIX: yamlFlowStringList(manifest.candidate?.os ?? manifest.matrix.os),
787
- CANDIDATE_MATRIX: candidateMatrix(manifest),
788
976
  WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
789
977
  MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
790
978
  RELEASE_CHAIN_JOBS: releaseChainJobs(manifest, pm, artifactPrefix),
791
979
  };
792
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
+
793
1007
  if (manifest.publish?.npm) {
794
1008
  context.NPM_PROVENANCE_FLAG = manifest.publish.npm.provenance ? " --provenance" : "";
795
1009
  }
@@ -801,5 +1015,21 @@ export function buildRenderContext(manifest, repoPath) {
801
1015
  context.WINGET_PACKAGE_IDENTIFIER = manifest.publish.winget.packageIdentifier;
802
1016
  }
803
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
+
804
1034
  return context;
805
1035
  }
@@ -105,11 +105,21 @@
105
105
  },
106
106
  "candidate": {
107
107
  "type": "object",
108
- "required": ["buildScript", "checkScript", "artifactRoot", "os"],
108
+ "required": ["buildScript", "checkScript", "admissionScript", "stageSummaryScript", "artifactRoot", "os"],
109
109
  "additionalProperties": false,
110
110
  "properties": {
111
111
  "buildScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
112
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
+ },
113
123
  "artifactRoot": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" },
114
124
  "os": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
115
125
  "targets": {
@@ -130,9 +140,14 @@
130
140
  "signMacos": { "type": "boolean" },
131
141
  "releaseChain": {
132
142
  "type": "object",
133
- "required": ["windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
143
+ "required": ["evidenceVerificationScript", "windowsFinalizeScript", "installedQualificationScript", "publishScript", "qualificationRunnerVariable"],
134
144
  "additionalProperties": false,
135
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
+ },
136
151
  "windowsFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
137
152
  "macosFinalizeScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
138
153
  "installedQualificationScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },