@rightkit/git 0.2.4 → 0.2.6

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,174 @@ 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
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
337
+ with:
338
+ node-version: ${nodeVersion}
339
+ package-manager-cache: false
340
+ ${pm.setup}
341
+ - uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
342
+ with:
343
+ toolchain: 1.98.0
344
+ - run: ${pm.installCommand}
345
+ - name: Download exact unsigned Windows candidate
346
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
347
+ with:
348
+ name: ${artifactPrefix}-unsigned-${windowsRunner}-${nodeVersion}
349
+ path: \${{ runner.temp }}/unsigned-windows-candidate
350
+ - name: Authenticate to Azure with GitHub OIDC
351
+ uses: azure/login@${AZURE_LOGIN_SHA} # v3
352
+ with:
353
+ client-id: \${{ vars.AZURE_CLIENT_ID }}
354
+ tenant-id: \${{ vars.AZURE_TENANT_ID }}
355
+ subscription-id: \${{ vars.AZURE_SUBSCRIPTION_ID }}
356
+ - name: Install pinned Artifact Signing client
357
+ shell: pwsh
358
+ run: |
359
+ $ErrorActionPreference = 'Stop'
360
+ $tools = Join-Path $env:RUNNER_TEMP 'artifact-signing-tools'
361
+ New-Item -ItemType Directory -Force -Path $tools | Out-Null
362
+ $client = Join-Path $tools 'client.zip'
363
+ $sdk = Join-Path $tools 'sdk.zip'
364
+ 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
365
+ 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
366
+ if ((Get-FileHash $client -Algorithm SHA256).Hash -ne '${ARTIFACT_SIGNING_CLIENT_SHA256}') { throw 'Artifact Signing client hash mismatch' }
367
+ if ((Get-FileHash $sdk -Algorithm SHA256).Hash -ne '${WINDOWS_SDK_BUILD_TOOLS_SHA256}') { throw 'Windows SDK Build Tools hash mismatch' }
368
+ Expand-Archive $client (Join-Path $tools 'client')
369
+ Expand-Archive $sdk (Join-Path $tools 'sdk')
370
+ "AZURE_ARTIFACT_SIGNING_DLIB_PATH=$tools\\client\\bin\\x64\\Azure.CodeSigning.Dlib.dll" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
371
+ "AZURE_SIGNTOOL_PATH=$tools\\sdk\\bin\\10.0.26100.0\\x64\\signtool.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
372
+ - name: Sign & finalize Windows candidate
373
+ run: pnpm exec right-release build --platform win --skip-checks
374
+ env:
375
+ LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-windows-candidate
376
+ LEGION_SOURCE_REVISION: \${{ github.sha }}
377
+ AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
378
+ AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
379
+ AZURE_ARTIFACT_SIGNING_PROFILE: \${{ vars.AZURE_ARTIFACT_SIGNING_PROFILE }}
380
+ - name: Upload signed Windows candidate
381
+ uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
382
+ with:
383
+ name: ${artifactPrefix}-signed-windows-${nodeVersion}
384
+ path: |
385
+ dist/releases/windows/**
386
+ .right-release/receipts/windows-*.json
387
+ if-no-files-found: error
388
+ `.trimEnd();
389
+ }
390
+
391
+ function macosSigningJob(manifest, pm, artifactPrefix) {
392
+ if (manifest.candidate?.signMacos !== true) return "";
393
+ const macosRunner = manifest.candidate.os.find((value) => /^macos-/i.test(value));
394
+ const nodeVersion = String(manifest.matrix.node[0]);
395
+ return `
396
+ macos-sign:
397
+ needs: [candidate]
398
+ environment: release
399
+ runs-on: ${macosRunner}
400
+ permissions:
401
+ contents: read
402
+ steps:
403
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
404
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
405
+ with:
406
+ node-version: ${nodeVersion}
407
+ package-manager-cache: false
408
+ ${pm.setup}
409
+ - uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
410
+ with:
411
+ toolchain: 1.98.0
412
+ - run: ${pm.installCommand}
413
+ - name: Download exact unsigned macOS candidate
414
+ uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
415
+ with:
416
+ name: ${artifactPrefix}-unsigned-${macosRunner}-${nodeVersion}
417
+ path: \${{ runner.temp }}/unsigned-macos-candidate
418
+ - name: Install Apple signing identity in temporary keychain
419
+ shell: bash
420
+ env:
421
+ APPLE_CERTIFICATE_BASE64: \${{ secrets.APPLE_CERTIFICATE_BASE64 }}
422
+ APPLE_CERTIFICATE_PASSWORD: \${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
423
+ APPLE_KEYCHAIN_PASSWORD: \${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
424
+ APPLE_API_KEY_BASE64: \${{ secrets.APPLE_API_KEY_BASE64 }}
425
+ run: |
426
+ set -euo pipefail
427
+ certificate_path="$RUNNER_TEMP/apple-signing.p12"
428
+ key_path="$RUNNER_TEMP/AuthKey.p8"
429
+ keychain_path="$RUNNER_TEMP/apple-signing.keychain-db"
430
+ printf '%s' "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$certificate_path"
431
+ printf '%s' "$APPLE_API_KEY_BASE64" | base64 --decode > "$key_path"
432
+ security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
433
+ security set-keychain-settings -lut 21600 "$keychain_path"
434
+ security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
435
+ security import "$certificate_path" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
436
+ security set-key-partition-list -S apple-tool:,apple: -k "$APPLE_KEYCHAIN_PASSWORD" "$keychain_path"
437
+ security list-keychain -d user -s "$keychain_path"
438
+ printf 'APPLE_API_KEY_PATH=%s\\n' "$key_path" >> "$GITHUB_ENV"
439
+ - name: Sign, notarize & finalize macOS candidate
440
+ run: pnpm exec right-release build --platform mac --skip-checks
441
+ env:
442
+ LEGION_UNSIGNED_CANDIDATE_ROOT: \${{ runner.temp }}/unsigned-macos-candidate
443
+ LEGION_SOURCE_REVISION: \${{ github.sha }}
444
+ APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
445
+ APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
446
+ APPLE_API_ISSUER: \${{ secrets.APPLE_API_ISSUER }}
447
+ - name: Upload signed & notarized macOS candidate
448
+ uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
449
+ with:
450
+ name: ${artifactPrefix}-signed-macos-${nodeVersion}
451
+ path: |
452
+ dist/releases/mac/**
453
+ .right-release/receipts/macos-*.json
454
+ if-no-files-found: error
455
+ - name: Remove Apple signing material
456
+ if: \${{ always() }}
457
+ shell: bash
458
+ run: |
459
+ security delete-keychain "$RUNNER_TEMP/apple-signing.keychain-db" 2>/dev/null || true
460
+ rm -f "$RUNNER_TEMP/apple-signing.p12" "$RUNNER_TEMP/AuthKey.p8"
461
+ `.trimEnd();
462
+ }
463
+
281
464
  /**
282
465
  * Derive one project name from the target repo's package.json, falling back
283
466
  * to the repo directory's basename. Never throws.
@@ -321,6 +504,9 @@ export function buildRenderContext(manifest, repoPath) {
321
504
  CANDIDATE_CHECK_SCRIPT: manifest.candidate?.checkScript,
322
505
  CANDIDATE_ARTIFACT_ROOT: manifest.candidate?.artifactRoot,
323
506
  CANDIDATE_OS_MATRIX: yamlFlowStringList(manifest.candidate?.os ?? manifest.matrix.os),
507
+ CANDIDATE_MATRIX: candidateMatrix(manifest),
508
+ WINDOWS_SIGNING_JOB: windowsSigningJob(manifest, pm, artifactPrefix),
509
+ MACOS_SIGNING_JOB: macosSigningJob(manifest, pm, artifactPrefix),
324
510
  };
325
511
 
326
512
  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.4",
3
+ "version": "0.2.6",
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
@@ -46,6 +46,7 @@ test("rust-hybrid ci renders its pinned package manager and gate", () => {
46
46
  const manifest = { ...FULL_MANIFEST, lanes: ["ci"], profile: "rust-hybrid", gate: { path: "scripts/gate.sh" }, templateVersion: "1.1.0" };
47
47
  const content = renderTemplate(fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/ci.yml"), "utf8"), buildRenderContext(manifest, "/repos/widgets"));
48
48
  assert.match(content, /uses: pnpm\/action-setup@[a-f0-9]{40}/);
49
+ assert.match(content, /^name: widgets-ci$/m);
49
50
  assert.match(content, /version: 11\.24\.0/);
50
51
  assert.match(content, /toolchain: 1\.98\.0/);
51
52
  assert.match(content, /bash \.\/scripts\/gate\.sh/);
@@ -67,11 +68,60 @@ test("rust-hybrid release candidate renders product scripts, artifact handoff, a
67
68
  assert.match(content, /pnpm run ci:candidate/);
68
69
  assert.match(content, /pnpm run ci:candidate-check/);
69
70
  assert.match(content, /RIGHT_GIT_ARTIFACT_ROOT/);
70
- 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 \}\}/);
71
74
  assert.match(content, /actions\/upload-artifact@[a-f0-9]{40}/);
72
75
  assert.doesNotMatch(content, /secrets\.|AZURE_|APPLE_|certificate|private.key/i);
73
76
  });
74
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.doesNotMatch(content, /secrets\.|client-secret|private.key/i);
99
+ });
100
+
101
+ test("rust-hybrid release candidate can opt into protected macOS signing & notarization", () => {
102
+ const manifest = {
103
+ ...FULL_MANIFEST,
104
+ lanes: ["ci", "release-candidate"],
105
+ profile: "rust-hybrid",
106
+ gate: { path: "scripts/gate.sh" },
107
+ candidate: { buildScript: "ci:candidate", checkScript: "ci:candidate-check", artifactRoot: "widgets-unsigned", os: ["macos-15"], signMacos: true },
108
+ matrix: { os: ["ubuntu-24.04", "macos-15"], node: ["22.23.2"] },
109
+ templateVersion: "1.3.0",
110
+ };
111
+ const content = renderTemplate(
112
+ fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
113
+ buildRenderContext(manifest, "/repos/widgets"),
114
+ );
115
+ assert.match(content, /macos-sign:/);
116
+ assert.match(content, /environment: release/);
117
+ assert.match(content, /secrets\.APPLE_CERTIFICATE_BASE64/);
118
+ assert.match(content, /temporary keychain/i);
119
+ assert.match(content, /right-release build --platform mac --skip-checks/);
120
+ assert.match(content, /secrets\.APPLE_API_KEY_BASE64/);
121
+ assert.match(content, /security delete-keychain/);
122
+ assert.doesNotMatch(content, /pull_request_target/);
123
+ });
124
+
75
125
  test("render determinism: same manifest renders byte-identical output every time", () => {
76
126
  const first = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
77
127
  const second = renderAllLanes(FULL_MANIFEST, "/repos/widgets");
@@ -1,6 +1,6 @@
1
1
  # Managed by right-git — do not hand-edit. Run `right-git sync` to update.
2
2
  # Template: profiles/rust-hybrid/ci.yml | Template version: ${TEMPLATE_VERSION}
3
- name: legion-ci
3
+ name: ${ARTIFACT_PREFIX}-ci
4
4
  on:
5
5
  pull_request:
6
6
  push:
@@ -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}