@rightkit/git 0.2.5 → 0.2.7

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 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
- - **No code-signing credential ever reaches CI.** Signing and notarization stay local in `@rightkit/release`. The only publish credential in the npm lane is short-lived OIDC (trusted publishing, no long-lived token).
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.
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,4 @@ 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 }`; `os` selects supported release hosts from main matrix. RightGit passes isolated `RIGHT_GIT_ARTIFACT_ROOT`, checks it, & uploads it as an unsigned handoff. It never signs, finalizes, publishes releases, or receives signing credentials.
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. Both use product-owned RightRelease finalization & never publish releases.
package/manifest.mjs CHANGED
@@ -141,7 +141,7 @@ export function validateManifest(manifest) {
141
141
  errors.push("candidate must be an object");
142
142
  } else {
143
143
  for (const key of Object.keys(candidate)) {
144
- if (!["buildScript", "checkScript", "artifactRoot", "os"].includes(key)) errors.push(`unknown candidate field: ${key}`);
144
+ if (!["buildScript", "checkScript", "artifactRoot", "os", "signWindows", "signMacos"].includes(key)) errors.push(`unknown candidate field: ${key}`);
145
145
  }
146
146
  for (const key of ["buildScript", "checkScript"]) {
147
147
  if (typeof candidate[key] !== "string" || !PACKAGE_SCRIPT_RE.test(candidate[key])) {
@@ -156,6 +156,21 @@ export function validateManifest(manifest) {
156
156
  } else if (candidate.os.some((value) => !manifest.matrix?.os?.includes(value))) {
157
157
  errors.push("candidate.os entries must also exist in matrix.os");
158
158
  }
159
+ if (candidate.signWindows !== undefined && typeof candidate.signWindows !== "boolean") {
160
+ errors.push("candidate.signWindows must be a boolean");
161
+ }
162
+ if (candidate.signWindows === true && candidate.os.filter((value) => /^windows-/i.test(value)).length !== 1) {
163
+ errors.push("candidate.signWindows requires exactly one Windows runner in candidate.os");
164
+ }
165
+ if (candidate.signMacos !== undefined && typeof candidate.signMacos !== "boolean") {
166
+ errors.push("candidate.signMacos must be a boolean");
167
+ }
168
+ if (candidate.signMacos === true && candidate.os.filter((value) => /^macos-/i.test(value)).length !== 1) {
169
+ errors.push("candidate.signMacos requires exactly one macOS runner in candidate.os");
170
+ }
171
+ if ((candidate.signWindows === true || candidate.signMacos === true) && manifest.matrix?.node?.length !== 1) {
172
+ errors.push("candidate signing requires exactly one matrix.node version");
173
+ }
159
174
  }
160
175
  }
161
176
  if (profile === "rust-hybrid" && lanes.includes("release-candidate") && candidate === undefined) {
@@ -278,6 +293,180 @@ function yamlFlowStringList(values) {
278
293
  return `[${values.map((v) => JSON.stringify(String(v))).join(", ")}]`;
279
294
  }
280
295
 
296
+ function candidateMatrix(manifest) {
297
+ const entries = [];
298
+ for (const os of manifest.candidate?.os ?? manifest.matrix.os) {
299
+ const platform = /^windows-/i.test(os) ? "windows" : /^macos-/i.test(os) ? "macos" : "linux";
300
+ const architecture = platform === "macos" ? "arm64" : "x86_64";
301
+ for (const node of manifest.matrix.node) entries.push({ os, node, platform, architecture });
302
+ }
303
+ return [
304
+ " include:",
305
+ ...entries.flatMap((entry) => [
306
+ ` - os: ${JSON.stringify(entry.os)}`,
307
+ ` node: ${JSON.stringify(entry.node)}`,
308
+ ` platform: ${JSON.stringify(entry.platform)}`,
309
+ ` architecture: ${JSON.stringify(entry.architecture)}`,
310
+ ]),
311
+ ].join("\n");
312
+ }
313
+
314
+ const AZURE_LOGIN_SHA = "7ddb5af1ef8758cf1353cf3b42f940aee27ba21c"; // v3
315
+ const DOWNLOAD_ARTIFACT_SHA = "d3f86a106a0bac45b974a628896c90dbdf5c8093"; // v4.3.0
316
+ const UPLOAD_ARTIFACT_SHA = "ea165f8d65b6e75b540449e92b4886f43607fa02"; // v4
317
+ const ARTIFACT_SIGNING_CLIENT_VERSION = "1.0.128";
318
+ const ARTIFACT_SIGNING_CLIENT_SHA256 = "74BD7D27E6CE1051409C38D9B46BC8DF0400ECD643D51FFBF2AC00869061E40B";
319
+ const WINDOWS_SDK_BUILD_TOOLS_VERSION = "10.0.26100.4188";
320
+ const WINDOWS_SDK_BUILD_TOOLS_SHA256 = "180DEB372659029864C10A0C04787833234D64AACD1D2C0661D2C00295D8E022";
321
+
322
+ function windowsSigningJob(manifest, pm, artifactPrefix) {
323
+ if (manifest.candidate?.signWindows !== true) return "";
324
+ const windowsRunner = manifest.candidate.os.find((value) => /^windows-/i.test(value));
325
+ const nodeVersion = String(manifest.matrix.node[0]);
326
+ return `
327
+ windows-sign:
328
+ needs: [candidate]
329
+ environment: release
330
+ runs-on: ${windowsRunner}
331
+ permissions:
332
+ contents: read
333
+ id-token: write
334
+ steps:
335
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
336
+ - name: Resolve checked-out source revision
337
+ shell: pwsh
338
+ run: '"RIGHT_GIT_SOURCE_REVISION=$(git rev-parse HEAD)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append'
339
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
340
+ with:
341
+ node-version: ${nodeVersion}
342
+ package-manager-cache: false
343
+ ${pm.setup}
344
+ - uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
345
+ with:
346
+ toolchain: 1.98.0
347
+ - run: ${pm.installCommand}
348
+ - name: Download exact unsigned Windows candidate
349
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
350
+ with:
351
+ name: ${artifactPrefix}-unsigned-${windowsRunner}-${nodeVersion}
352
+ path: \${{ runner.temp }}/unsigned-windows-candidate
353
+ - name: Authenticate to Azure with GitHub OIDC
354
+ uses: azure/login@${AZURE_LOGIN_SHA} # v3
355
+ with:
356
+ client-id: \${{ vars.AZURE_CLIENT_ID }}
357
+ tenant-id: \${{ vars.AZURE_TENANT_ID }}
358
+ subscription-id: \${{ vars.AZURE_SUBSCRIPTION_ID }}
359
+ - name: Install pinned Artifact Signing client
360
+ shell: pwsh
361
+ run: |
362
+ $ErrorActionPreference = 'Stop'
363
+ $tools = Join-Path $env:RUNNER_TEMP 'artifact-signing-tools'
364
+ New-Item -ItemType Directory -Force -Path $tools | Out-Null
365
+ $client = Join-Path $tools 'client.zip'
366
+ $sdk = Join-Path $tools 'sdk.zip'
367
+ Invoke-WebRequest 'https://api.nuget.org/v3-flatcontainer/microsoft.artifactsigning.client/${ARTIFACT_SIGNING_CLIENT_VERSION}/microsoft.artifactsigning.client.${ARTIFACT_SIGNING_CLIENT_VERSION}.nupkg' -OutFile $client
368
+ Invoke-WebRequest 'https://api.nuget.org/v3-flatcontainer/microsoft.windows.sdk.buildtools/${WINDOWS_SDK_BUILD_TOOLS_VERSION}/microsoft.windows.sdk.buildtools.${WINDOWS_SDK_BUILD_TOOLS_VERSION}.nupkg' -OutFile $sdk
369
+ if ((Get-FileHash $client -Algorithm SHA256).Hash -ne '${ARTIFACT_SIGNING_CLIENT_SHA256}') { throw 'Artifact Signing client hash mismatch' }
370
+ if ((Get-FileHash $sdk -Algorithm SHA256).Hash -ne '${WINDOWS_SDK_BUILD_TOOLS_SHA256}') { throw 'Windows SDK Build Tools hash mismatch' }
371
+ Expand-Archive $client (Join-Path $tools 'client')
372
+ Expand-Archive $sdk (Join-Path $tools 'sdk')
373
+ "AZURE_ARTIFACT_SIGNING_DLIB_PATH=$tools\\client\\bin\\x64\\Azure.CodeSigning.Dlib.dll" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
374
+ "AZURE_SIGNTOOL_PATH=$tools\\sdk\\bin\\10.0.26100.0\\x64\\signtool.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
375
+ - name: Sign & finalize Windows candidate
376
+ run: pnpm exec right-release build --platform win --skip-checks
377
+ env:
378
+ LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
379
+ LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
380
+ AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
381
+ AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
382
+ AZURE_ARTIFACT_SIGNING_PROFILE: \${{ vars.AZURE_ARTIFACT_SIGNING_PROFILE }}
383
+ - name: Upload signed Windows candidate
384
+ uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
385
+ with:
386
+ name: ${artifactPrefix}-signed-windows-${nodeVersion}
387
+ path: |
388
+ dist/releases/windows/**
389
+ .right-release/receipts/windows-*.json
390
+ if-no-files-found: error
391
+ `.trimEnd();
392
+ }
393
+
394
+ function macosSigningJob(manifest, pm, artifactPrefix) {
395
+ if (manifest.candidate?.signMacos !== true) return "";
396
+ const macosRunner = manifest.candidate.os.find((value) => /^macos-/i.test(value));
397
+ const nodeVersion = String(manifest.matrix.node[0]);
398
+ return `
399
+ macos-sign:
400
+ needs: [candidate]
401
+ environment: release
402
+ runs-on: ${macosRunner}
403
+ permissions:
404
+ contents: read
405
+ steps:
406
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
407
+ - name: Resolve checked-out source revision
408
+ shell: bash
409
+ run: printf 'RIGHT_GIT_SOURCE_REVISION=%s\\n' "$(git rev-parse HEAD)" >> "$GITHUB_ENV"
410
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
411
+ with:
412
+ node-version: ${nodeVersion}
413
+ package-manager-cache: false
414
+ ${pm.setup}
415
+ - uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
416
+ with:
417
+ toolchain: 1.98.0
418
+ - run: ${pm.installCommand}
419
+ - name: Download exact unsigned macOS candidate
420
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
421
+ with:
422
+ name: ${artifactPrefix}-unsigned-${macosRunner}-${nodeVersion}
423
+ path: \${{ runner.temp }}/unsigned-macos-candidate
424
+ - name: Install Apple signing identity in temporary keychain
425
+ shell: bash
426
+ env:
427
+ APPLE_CERTIFICATE_BASE64: \${{ secrets.APPLE_CERTIFICATE_BASE64 }}
428
+ APPLE_CERTIFICATE_PASSWORD: \${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
429
+ APPLE_KEYCHAIN_PASSWORD: \${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
430
+ APPLE_API_KEY_BASE64: \${{ secrets.APPLE_API_KEY_BASE64 }}
431
+ run: |
432
+ set -euo pipefail
433
+ certificate_path="$RUNNER_TEMP/apple-signing.p12"
434
+ key_path="$RUNNER_TEMP/AuthKey.p8"
435
+ keychain_path="$RUNNER_TEMP/apple-signing.keychain-db"
436
+ printf '%s' "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$certificate_path"
437
+ printf '%s' "$APPLE_API_KEY_BASE64" | base64 --decode > "$key_path"
438
+ security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
439
+ security set-keychain-settings -lut 21600 "$keychain_path"
440
+ security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
441
+ security import "$certificate_path" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
442
+ security set-key-partition-list -S apple-tool:,apple: -k "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
443
+ security list-keychain -d user -s "$keychain_path"
444
+ printf 'APPLE_API_KEY_PATH=%s\\n' "$key_path" >> "$GITHUB_ENV"
445
+ - name: Sign, notarize & finalize macOS candidate
446
+ run: pnpm exec right-release build --platform mac --skip-checks
447
+ env:
448
+ LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
449
+ LEGION_SOURCE_REVISION: \${{ env.RIGHT_GIT_SOURCE_REVISION }}
450
+ APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
451
+ APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
452
+ APPLE_API_ISSUER: \${{ secrets.APPLE_API_ISSUER }}
453
+ - name: Upload signed & notarized macOS candidate
454
+ uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
455
+ with:
456
+ name: ${artifactPrefix}-signed-macos-${nodeVersion}
457
+ path: |
458
+ dist/releases/mac/**
459
+ .right-release/receipts/macos-*.json
460
+ if-no-files-found: error
461
+ - name: Remove Apple signing material
462
+ if: \${{ always() }}
463
+ shell: bash
464
+ run: |
465
+ security delete-keychain "$RUNNER_TEMP/apple-signing.keychain-db" 2>/dev/null || true
466
+ rm -f "$RUNNER_TEMP/apple-signing.p12" "$RUNNER_TEMP/AuthKey.p8"
467
+ `.trimEnd();
468
+ }
469
+
281
470
  /**
282
471
  * Derive one project name from the target repo's package.json, falling back
283
472
  * to the repo directory's basename. Never throws.
@@ -321,6 +510,9 @@ export function buildRenderContext(manifest, repoPath) {
321
510
  CANDIDATE_CHECK_SCRIPT: manifest.candidate?.checkScript,
322
511
  CANDIDATE_ARTIFACT_ROOT: manifest.candidate?.artifactRoot,
323
512
  CANDIDATE_OS_MATRIX: yamlFlowStringList(manifest.candidate?.os ?? manifest.matrix.os),
513
+ CANDIDATE_MATRIX: candidateMatrix(manifest),
514
+ WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
515
+ MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
324
516
  };
325
517
 
326
518
  if (manifest.publish?.npm) {
@@ -94,7 +94,9 @@
94
94
  "buildScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
95
95
  "checkScript": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9:_-]*$" },
96
96
  "artifactRoot": { "type": "string", "pattern": "^(?!.*\\.\\.)(?!.*\\s)[A-Za-z0-9][A-Za-z0-9._/-]*$" },
97
- "os": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }
97
+ "os": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
98
+ "signWindows": { "type": "boolean" },
99
+ "signMacos": { "type": "boolean" }
98
100
  }
99
101
  }
100
102
  }
package/manifest.test.mjs CHANGED
@@ -29,6 +29,15 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
29
29
  candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "legion-unsigned", os: ["ubuntu-latest"] },
30
30
  };
31
31
  assert.equal(validateManifest(candidate).valid, true);
32
+ assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, signWindows: true } }).valid, false);
33
+ assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, signMacos: true } }).valid, false);
34
+ const windowsCandidate = { ...candidate, matrix: { ...candidate.matrix, os: ["ubuntu-latest", "windows-2025"] }, candidate: { ...candidate.candidate, os: ["windows-2025"], signWindows: true } };
35
+ assert.equal(validateManifest(windowsCandidate).valid, true);
36
+ assert.equal(validateManifest({ ...windowsCandidate, candidate: { ...windowsCandidate.candidate, signWindows: "yes" } }).valid, false);
37
+ const macCandidate = { ...candidate, matrix: { ...candidate.matrix, os: ["ubuntu-latest", "macos-15"] }, candidate: { ...candidate.candidate, os: ["macos-15"], signMacos: true } };
38
+ assert.equal(validateManifest(macCandidate).valid, true);
39
+ assert.equal(validateManifest({ ...macCandidate, candidate: { ...macCandidate.candidate, signMacos: "yes" } }).valid, false);
40
+ assert.equal(validateManifest({ ...macCandidate, matrix: { ...macCandidate.matrix, node: ["22", "24"] } }).valid, false);
32
41
  assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, buildScript: "echo unsafe" } }).valid, false);
33
42
  assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, artifactRoot: "../escape" } }).valid, false);
34
43
  assert.equal(validateManifest({ ...candidate, candidate: { ...candidate.candidate, os: ["windows-latest"] } }).valid, false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/git",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Public-repo-only GitHub Actions workflow layer: templates, visibility gate, sync/drift/adopt/uninstall for right-git.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/render.test.mjs CHANGED
@@ -68,11 +68,66 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
68
68
  assert.match(content, /pnpm run ci:candidate/);
69
69
  assert.match(content, /pnpm run ci:candidate-check/);
70
70
  assert.match(content, /RIGHT_GIT_ARTIFACT_ROOT/);
71
- assert.match(content, /os: \["macos-latest", "windows-latest"\]/);
71
+ assert.match(content, /os: "macos-latest"/);
72
+ assert.match(content, /platform: "windows"/);
73
+ assert.match(content, /RIGHT_GIT_RELEASE_ARCHITECTURE: \$\{\{ matrix\.architecture \}\}/);
72
74
  assert.match(content, /actions\/upload-artifact@[a-f0-9]{40}/);
73
75
  assert.doesNotMatch(content, /secrets\.|AZURE_|APPLE_|certificate|private.key/i);
74
76
  });
75
77
 
78
+ test("rust-hybrid release candidate can opt into OIDC Windows signing without long-lived secrets", () => {
79
+ const manifest = {
80
+ ...FULL_MANIFEST,
81
+ lanes: ["ci", "release-candidate"],
82
+ profile: "rust-hybrid",
83
+ gate: { path: "scripts/gate.sh" },
84
+ candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "widgets-unsigned", os: ["windows-2025"], signWindows: true },
85
+ matrix: { os: ["ubuntu-24.04", "windows-2025"], node: ["22.23.2"] },
86
+ templateVersion: "1.3.0",
87
+ };
88
+ const content = renderTemplate(
89
+ fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
90
+ buildRenderContext(manifest, "/repos/widgets"),
91
+ );
92
+ assert.match(content, /environment: release/);
93
+ assert.match(content, /id-token: write/);
94
+ assert.match(content, /azure\/login@[a-f0-9]{40}/);
95
+ assert.match(content, /vars\.AZURE_CLIENT_ID/);
96
+ assert.match(content, /right-release build --platform win --skip-checks/);
97
+ assert.match(content, /Artifact Signing client hash mismatch/);
98
+ assert.match(content, /RIGHT_GIT_SOURCE_REVISION=\$\(git rev-parse HEAD\)/);
99
+ assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
100
+ assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
101
+ assert.doesNotMatch(content, /secrets\.|client-secret|private.key/i);
102
+ });
103
+
104
+ test("rust-hybrid release candidate can opt into protected macOS signing & notarization", () => {
105
+ const manifest = {
106
+ ...FULL_MANIFEST,
107
+ lanes: ["ci", "release-candidate"],
108
+ profile: "rust-hybrid",
109
+ gate: { path: "scripts/gate.sh" },
110
+ candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "widgets-unsigned", os: ["macos-15"], signMacos: true },
111
+ matrix: { os: ["ubuntu-24.04", "macos-15"], node: ["22.23.2"] },
112
+ templateVersion: "1.3.0",
113
+ };
114
+ const content = renderTemplate(
115
+ fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
116
+ buildRenderContext(manifest, "/repos/widgets"),
117
+ );
118
+ assert.match(content, /macos-sign:/);
119
+ assert.match(content, /environment: release/);
120
+ assert.match(content, /secrets\.APPLE_CERTIFICATE_BASE64/);
121
+ assert.match(content, /temporary keychain/i);
122
+ assert.match(content, /right-release build --platform mac --skip-checks/);
123
+ assert.match(content, /secrets\.APPLE_API_KEY_BASE64/);
124
+ assert.match(content, /RIGHT_GIT_SOURCE_REVISION=%s\\n.*git rev-parse HEAD/);
125
+ assert.ok(content.includes("LEGION_SOURCE_REVISION: ${{ env.RIGHT_GIT_SOURCE_REVISION }}"));
126
+ assert.doesNotMatch(content, /LEGION_SOURCE_REVISION: \$\{\{ github\.sha \}\}/);
127
+ assert.match(content, /security delete-keychain/);
128
+ assert.doesNotMatch(content, /pull_request_target/);
129
+ });
130
+
76
131
  test("render determinism: same manifest renders byte-identical output every time", () => {
77
132
  const first = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
78
133
  const second = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
@@ -1,7 +1,7 @@
1
1
  # Managed by right-git — do not hand-edit. Run `right-git sync` to update.
2
2
  # Template: profiles/rust-hybrid/release-candidate.yml | Template version: ${TEMPLATE_VERSION}
3
- # Public CI produces unsigned candidates only. Signing credentials, finalization,
4
- # release publication, & stable bootstrap publication remain protected-host work.
3
+ # Public CI always produces unsigned candidates. Opt-in finalization signs Windows
4
+ # through Azure OIDC & macOS through environment-scoped Apple credentials.
5
5
  name: release-candidate
6
6
  on:
7
7
  workflow_dispatch:
@@ -21,8 +21,7 @@ jobs:
21
21
  strategy:
22
22
  fail-fast: false
23
23
  matrix:
24
- os: ${CANDIDATE_OS_MATRIX}
25
- node: ${NODE_MATRIX}
24
+ ${CANDIDATE_MATRIX}
26
25
  runs-on: ${{ matrix.os }}
27
26
  steps:
28
27
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -43,13 +42,19 @@ ${PACKAGE_MANAGER_SETUP}
43
42
  run: ${PM_RUN} ${CANDIDATE_BUILD_SCRIPT}
44
43
  env:
45
44
  RIGHT_GIT_ARTIFACT_ROOT: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}
45
+ RIGHT_GIT_RELEASE_PLATFORM: ${{ matrix.platform }}
46
+ RIGHT_GIT_RELEASE_ARCHITECTURE: ${{ matrix.architecture }}
46
47
  - name: Check unsigned release candidate
47
48
  run: ${PM_RUN} ${CANDIDATE_CHECK_SCRIPT}
48
49
  env:
49
50
  RIGHT_GIT_ARTIFACT_ROOT: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}
51
+ RIGHT_GIT_RELEASE_PLATFORM: ${{ matrix.platform }}
52
+ RIGHT_GIT_RELEASE_ARCHITECTURE: ${{ matrix.architecture }}
50
53
  - name: Upload unsigned candidate handoff
51
54
  uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
52
55
  with:
53
56
  name: ${ARTIFACT_PREFIX}-unsigned-${{ matrix.os }}-${{ matrix.node }}
54
57
  path: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}/**
55
58
  if-no-files-found: error
59
+ ${WINDOWS_SIGNING_JOB}
60
+ ${MACOS_SIGNING_JOB}