@rightkit/git 0.2.8 → 0.2.9

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
@@ -38,6 +38,6 @@ Node builtins only; no runtime dependencies.
38
38
 
39
39
  ## Profiles
40
40
 
41
- `profile` defaults to `node`. `rust-hybrid` requires `ci`, accepts optional `release-candidate`, installs pinned Node plus Rust, then runs product-owned package scripts. Candidate configuration is `{ buildScript, checkScript, artifactRoot, os, signWindows?, signMacos?, releaseChain? }`; `os` selects supported release hosts from main matrix. RightGit always uploads an unsigned handoff. `signWindows: true` adds Azure OIDC signing. `signMacos: true` adds Developer ID signing plus notarization from protected environment secrets.
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, targets?, signWindows?, signMacos?, releaseChain? }`. Legacy `os` remains supported & derives one target per runner; explicit `targets` is `{ os, platform, architecture }[]`, independent of main CI matrix. Each target maps to native runner, Rust target triple, exact unsigned artifact, & platform signing matrix. Supported platform values are `windows`, `macos`, `linux`; architectures are `x86_64`, `arm64`. RightGit always uploads an unsigned handoff. `signWindows: true` adds Azure OIDC signing. `signMacos: true` adds Developer ID signing plus notarization from protected environment secrets.
42
42
 
43
- `releaseChain` lets RightGit own full protected DAG while product scripts own artifact semantics: `{ windowsFinalizeScript, macosFinalizeScript?, installedQualificationScript, publishScript, qualificationRunnerVariable, publishSecrets? }`. Generated ordering is unsigned candidate → platform signing/finalization → installed qualification → publication. Every protected job is tag-gated, publication needs successful installed qualification plus macOS finalization when enabled, & only publish job receives declared publication secrets plus `contents: write`.
43
+ `releaseChain` lets RightGit own full protected DAG while product scripts own artifact semantics: `{ windowsFinalizeScript, macosFinalizeScript?, installedQualificationScript, publishScript, qualificationRunnerVariable, publishSecrets? }`. Generated ordering is unsigned candidate → platform signing/finalization → installed qualification → publication. It deliberately rejects more than one Windows target or more than one macOS target because its installed-qualification/publish handoff has one canonical signed artifact per platform. Every protected job is tag-gated, publication needs successful installed qualification plus macOS finalization when enabled, & only publish job receives declared publication secrets plus `contents: write`.
package/manifest.mjs CHANGED
@@ -62,6 +62,23 @@ const TOP_LEVEL_KEYS = new Set([
62
62
  const GATE_PATH_RE = /^(?!.*\.\.)(?!.*\s)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
63
63
  const PACKAGE_SCRIPT_RE = /^[A-Za-z0-9][A-Za-z0-9:_-]*$/;
64
64
  const GITHUB_VARIABLE_RE = /^[A-Z][A-Z0-9_]*$/;
65
+ const RELEASE_PLATFORMS = new Set(["windows", "macos", "linux"]);
66
+ const RELEASE_ARCHITECTURES = new Set(["x86_64", "arm64"]);
67
+ const NATIVE_RUNNER_TARGETS = new Map([
68
+ ["windows-2025", ["windows", "x86_64"]],
69
+ ["windows-11-arm", ["windows", "arm64"]],
70
+ ["macos-15", ["macos", "arm64"]],
71
+ ["macos-15-intel", ["macos", "x86_64"]],
72
+ ]);
73
+
74
+ function inferredTarget(os) {
75
+ const platform = /^windows-/i.test(os) ? "windows" : /^macos-/i.test(os) ? "macos" : "linux";
76
+ return { os, platform, architecture: platform === "macos" ? "arm64" : "x86_64" };
77
+ }
78
+
79
+ function candidateTargets(candidate) {
80
+ return Array.isArray(candidate?.targets) ? candidate.targets : (candidate?.os ?? []).map(inferredTarget);
81
+ }
65
82
 
66
83
  /**
67
84
  * Validate a parsed manifest object against the right-git manifest contract.
@@ -142,7 +159,7 @@ export function validateManifest(manifest) {
142
159
  errors.push("candidate must be an object");
143
160
  } else {
144
161
  for (const key of Object.keys(candidate)) {
145
- if (!["buildScript", "checkScript", "artifactRoot", "os", "signWindows", "signMacos", "releaseChain"].includes(key)) errors.push(`unknown candidate field: ${key}`);
162
+ if (!["buildScript", "checkScript", "artifactRoot", "os", "targets", "signWindows", "signMacos", "releaseChain"].includes(key)) errors.push(`unknown candidate field: ${key}`);
146
163
  }
147
164
  for (const key of ["buildScript", "checkScript"]) {
148
165
  if (typeof candidate[key] !== "string" || !PACKAGE_SCRIPT_RE.test(candidate[key])) {
@@ -154,20 +171,52 @@ export function validateManifest(manifest) {
154
171
  }
155
172
  if (!Array.isArray(candidate.os) || candidate.os.length === 0 || candidate.os.some((value) => typeof value !== "string" || !SAFE_MATRIX_VALUE_RE.test(value))) {
156
173
  errors.push("candidate.os must be a non-empty array of safe runner labels");
157
- } else if (candidate.os.some((value) => !manifest.matrix?.os?.includes(value))) {
174
+ } else if (candidate.targets === undefined && candidate.os.some((value) => !manifest.matrix?.os?.includes(value))) {
158
175
  errors.push("candidate.os entries must also exist in matrix.os");
159
176
  }
177
+ if (candidate.targets !== undefined) {
178
+ if (!Array.isArray(candidate.targets) || candidate.targets.length === 0) {
179
+ errors.push("candidate.targets must be a non-empty array");
180
+ } else {
181
+ const targetKeys = new Set();
182
+ for (const target of candidate.targets) {
183
+ if (target === null || typeof target !== "object" || Array.isArray(target)) {
184
+ errors.push("candidate.targets entries must be objects");
185
+ continue;
186
+ }
187
+ if (Object.keys(target).some((key) => !["os", "platform", "architecture"].includes(key))) errors.push("candidate.targets entries may only contain os, platform, architecture");
188
+ if (typeof target.os !== "string" || !SAFE_MATRIX_VALUE_RE.test(target.os)) errors.push("candidate.targets[].os must be a safe runner label");
189
+ if (!RELEASE_PLATFORMS.has(target.platform)) errors.push("candidate.targets[].platform must be windows, macos, or linux");
190
+ if (!RELEASE_ARCHITECTURES.has(target.architecture)) errors.push("candidate.targets[].architecture must be x86_64 or arm64");
191
+ const nativeTarget = NATIVE_RUNNER_TARGETS.get(target.os);
192
+ if (nativeTarget && (target.platform !== nativeTarget[0] || target.architecture !== nativeTarget[1])) {
193
+ errors.push(`candidate.targets[] must declare ${target.os} as ${nativeTarget[0]}/${nativeTarget[1]}`);
194
+ }
195
+ if ((target.platform === "windows" && !/^windows-/i.test(target.os)) || (target.platform === "macos" && !/^macos-/i.test(target.os))) {
196
+ errors.push("candidate.targets[].os must match its Windows or macOS platform");
197
+ }
198
+ const targetKey = `${target.os}|${target.platform}|${target.architecture}`;
199
+ if (targetKeys.has(targetKey)) errors.push("candidate.targets entries must be unique");
200
+ targetKeys.add(targetKey);
201
+ }
202
+ const targetOs = new Set(candidate.targets.map((target) => target?.os));
203
+ if (candidate.os.some((os) => !targetOs.has(os)) || targetOs.size !== new Set(candidate.os).size || candidate.os.length !== new Set(candidate.os).size) {
204
+ errors.push("candidate.os must contain exactly candidate.targets runner labels");
205
+ }
206
+ }
207
+ }
208
+ const targets = candidateTargets(candidate);
160
209
  if (candidate.signWindows !== undefined && typeof candidate.signWindows !== "boolean") {
161
210
  errors.push("candidate.signWindows must be a boolean");
162
211
  }
163
- if (candidate.signWindows === true && candidate.os.filter((value) => /^windows-/i.test(value)).length !== 1) {
164
- errors.push("candidate.signWindows requires exactly one Windows runner in candidate.os");
212
+ if (candidate.signWindows === true && targets.filter((target) => target?.platform === "windows").length === 0) {
213
+ errors.push("candidate.signWindows requires at least one Windows target");
165
214
  }
166
215
  if (candidate.signMacos !== undefined && typeof candidate.signMacos !== "boolean") {
167
216
  errors.push("candidate.signMacos must be a boolean");
168
217
  }
169
- if (candidate.signMacos === true && candidate.os.filter((value) => /^macos-/i.test(value)).length !== 1) {
170
- errors.push("candidate.signMacos requires exactly one macOS runner in candidate.os");
218
+ if (candidate.signMacos === true && targets.filter((target) => target?.platform === "macos").length === 0) {
219
+ errors.push("candidate.signMacos requires at least one macOS target");
171
220
  }
172
221
  if ((candidate.signWindows === true || candidate.signMacos === true) && manifest.matrix?.node?.length !== 1) {
173
222
  errors.push("candidate signing requires exactly one matrix.node version");
@@ -196,6 +245,9 @@ export function validateManifest(manifest) {
196
245
  errors.push("candidate.releaseChain.publishSecrets must be unique uppercase GitHub secret names");
197
246
  }
198
247
  if (candidate.signWindows !== true) errors.push("candidate.releaseChain requires candidate.signWindows=true");
248
+ if (targets.filter((target) => target?.platform === "windows").length !== 1 || targets.filter((target) => target?.platform === "macos").length > 1) {
249
+ errors.push("candidate.releaseChain requires at most one target per signing platform; multi-target aggregation is not supported");
250
+ }
199
251
  if (candidate.signMacos === true && !chain.macosFinalizeScript) errors.push("candidate.releaseChain with candidate.signMacos=true requires macosFinalizeScript");
200
252
  if (chain.macosFinalizeScript && candidate.signMacos !== true) errors.push("candidate.releaseChain.macosFinalizeScript requires candidate.signMacos=true");
201
253
  }
@@ -324,10 +376,8 @@ function yamlFlowStringList(values) {
324
376
 
325
377
  function candidateMatrix(manifest) {
326
378
  const entries = [];
327
- for (const os of manifest.candidate?.os ?? manifest.matrix.os) {
328
- const platform = /^windows-/i.test(os) ? "windows" : /^macos-/i.test(os) ? "macos" : "linux";
329
- const architecture = platform === "macos" ? "arm64" : "x86_64";
330
- for (const node of manifest.matrix.node) entries.push({ os, node, platform, architecture });
379
+ for (const target of candidateTargets(manifest.candidate ?? { os: manifest.matrix.os })) {
380
+ for (const node of manifest.matrix.node) entries.push({ ...target, node, targetTriple: targetTriple(target) });
331
381
  }
332
382
  return [
333
383
  " include:",
@@ -336,6 +386,31 @@ function candidateMatrix(manifest) {
336
386
  ` node: ${JSON.stringify(entry.node)}`,
337
387
  ` platform: ${JSON.stringify(entry.platform)}`,
338
388
  ` architecture: ${JSON.stringify(entry.architecture)}`,
389
+ ` targetTriple: ${JSON.stringify(entry.targetTriple)}`,
390
+ ]),
391
+ ].join("\n");
392
+ }
393
+
394
+ function targetTriple(target) {
395
+ const rustArchitecture = target.architecture === "arm64" ? "aarch64" : target.architecture;
396
+ if (target.platform === "windows") return `${rustArchitecture}-pc-windows-msvc`;
397
+ if (target.platform === "macos") return `${rustArchitecture}-apple-darwin`;
398
+ return `${rustArchitecture}-unknown-linux-gnu`;
399
+ }
400
+
401
+ function signingMatrix(manifest, platform) {
402
+ const node = String(manifest.matrix.node[0]);
403
+ const entries = candidateTargets(manifest.candidate)
404
+ .filter((target) => target.platform === platform)
405
+ .map((target) => ({ ...target, node, targetTriple: targetTriple(target) }));
406
+ return [
407
+ " include:",
408
+ ...entries.flatMap((entry) => [
409
+ ` - os: ${JSON.stringify(entry.os)}`,
410
+ ` platform: ${JSON.stringify(entry.platform)}`,
411
+ ` architecture: ${JSON.stringify(entry.architecture)}`,
412
+ ` targetTriple: ${JSON.stringify(entry.targetTriple)}`,
413
+ ` node: ${JSON.stringify(entry.node)}`,
339
414
  ]),
340
415
  ].join("\n");
341
416
  }
@@ -350,7 +425,6 @@ const WINDOWS_SDK_BUILD_TOOLS_SHA256 = "180DEB372659029864C10A0C04787833234D64AA
350
425
 
351
426
  function windowsSigningJob(manifest, pm, artifactPrefix) {
352
427
  if (manifest.candidate?.signWindows !== true) return "";
353
- const windowsRunner = manifest.candidate.os.find((value) => /^windows-/i.test(value));
354
428
  const nodeVersion = String(manifest.matrix.node[0]);
355
429
  const releaseChain = manifest.candidate.releaseChain;
356
430
  const finalizeCommand = releaseChain
@@ -370,7 +444,11 @@ function windowsSigningJob(manifest, pm, artifactPrefix) {
370
444
  needs: [candidate]
371
445
  if: \${{ startsWith(github.ref, 'refs/tags/v') }}
372
446
  environment: release
373
- runs-on: ${windowsRunner}
447
+ strategy:
448
+ fail-fast: false
449
+ matrix:
450
+ ${signingMatrix(manifest, "windows")}
451
+ runs-on: \${{ matrix.os }}
374
452
  permissions:
375
453
  contents: read
376
454
  id-token: write
@@ -387,11 +465,12 @@ ${pm.setup}
387
465
  - uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
388
466
  with:
389
467
  toolchain: 1.98.0
468
+ targets: \${{ matrix.targetTriple }}
390
469
  - run: ${pm.installCommand}
391
470
  - name: Download exact unsigned Windows candidate
392
471
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
393
472
  with:
394
- name: ${artifactPrefix}-unsigned-${windowsRunner}-${nodeVersion}
473
+ name: ${artifactPrefix}-unsigned-\${{ matrix.platform }}-\${{ matrix.architecture }}-${nodeVersion}
395
474
  path: \${{ runner.temp }}/unsigned-windows-candidate
396
475
  - name: Authenticate to Azure with GitHub OIDC
397
476
  uses: azure/login@${AZURE_LOGIN_SHA} # v3
@@ -419,13 +498,15 @@ ${pm.setup}
419
498
  run: ${finalizeCommand}
420
499
  env:
421
500
  ${finalizeEnvironment}
501
+ RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
502
+ RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
422
503
  AZURE_ARTIFACT_SIGNING_ENDPOINT: \${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
423
504
  AZURE_ARTIFACT_SIGNING_ACCOUNT: \${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
424
505
  AZURE_ARTIFACT_SIGNING_PROFILE: \${{ vars.AZURE_ARTIFACT_SIGNING_PROFILE }}
425
506
  - name: Upload signed Windows candidate
426
507
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
427
508
  with:
428
- name: ${artifactPrefix}-signed-windows-${nodeVersion}
509
+ name: ${artifactPrefix}-signed-windows-\${{ matrix.architecture }}-${nodeVersion}
429
510
  path: |
430
511
  ${uploadPath}
431
512
  if-no-files-found: error
@@ -434,7 +515,6 @@ ${uploadPath}
434
515
 
435
516
  function macosSigningJob(manifest, pm, artifactPrefix) {
436
517
  if (manifest.candidate?.signMacos !== true) return "";
437
- const macosRunner = manifest.candidate.os.find((value) => /^macos-/i.test(value));
438
518
  const nodeVersion = String(manifest.matrix.node[0]);
439
519
  const releaseChain = manifest.candidate.releaseChain;
440
520
  const finalizeCommand = releaseChain
@@ -454,7 +534,11 @@ function macosSigningJob(manifest, pm, artifactPrefix) {
454
534
  needs: [candidate]
455
535
  if: \${{ startsWith(github.ref, 'refs/tags/v') }}
456
536
  environment: release
457
- runs-on: ${macosRunner}
537
+ strategy:
538
+ fail-fast: false
539
+ matrix:
540
+ ${signingMatrix(manifest, "macos")}
541
+ runs-on: \${{ matrix.os }}
458
542
  permissions:
459
543
  contents: read
460
544
  steps:
@@ -470,11 +554,12 @@ ${pm.setup}
470
554
  - uses: dtolnay/rust-toolchain@f8be11a05b1d4f3fcebe6410cc16743212b999b0
471
555
  with:
472
556
  toolchain: 1.98.0
557
+ targets: \${{ matrix.targetTriple }}
473
558
  - run: ${pm.installCommand}
474
559
  - name: Download exact unsigned macOS candidate
475
560
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
476
561
  with:
477
- name: ${artifactPrefix}-unsigned-${macosRunner}-${nodeVersion}
562
+ name: ${artifactPrefix}-unsigned-\${{ matrix.platform }}-\${{ matrix.architecture }}-${nodeVersion}
478
563
  path: \${{ runner.temp }}/unsigned-macos-candidate
479
564
  - name: Install Apple signing identity in temporary keychain
480
565
  shell: bash
@@ -501,13 +586,15 @@ ${pm.setup}
501
586
  run: ${finalizeCommand}
502
587
  env:
503
588
  ${finalizeEnvironment}
589
+ RIGHT_GIT_RELEASE_PLATFORM: \${{ matrix.platform }}
590
+ RIGHT_GIT_RELEASE_ARCHITECTURE: \${{ matrix.architecture }}
504
591
  APPLE_DEVELOPER_ID: \${{ vars.APPLE_DEVELOPER_ID }}
505
592
  APPLE_API_KEY: \${{ secrets.APPLE_API_KEY }}
506
593
  APPLE_API_ISSUER: \${{ secrets.APPLE_API_ISSUER }}
507
594
  - name: Upload signed & notarized macOS candidate
508
595
  uses: actions/upload-artifact@${UPLOAD_ARTIFACT_SHA} # v4
509
596
  with:
510
- name: ${artifactPrefix}-signed-macos-${nodeVersion}
597
+ name: ${artifactPrefix}-signed-macos-\${{ matrix.architecture }}-${nodeVersion}
511
598
  path: |
512
599
  ${uploadPath}
513
600
  if-no-files-found: error
@@ -523,7 +610,10 @@ ${uploadPath}
523
610
  function releaseChainJobs(manifest, pm, artifactPrefix) {
524
611
  const chain = manifest.candidate?.releaseChain;
525
612
  if (!chain) return "";
526
- const windowsRunner = manifest.candidate.os.find((value) => /^windows-/i.test(value));
613
+ const windowsTarget = candidateTargets(manifest.candidate).find((target) => target.platform === "windows");
614
+ const windowsRunner = windowsTarget.os;
615
+ const windowsArchitecture = windowsTarget.architecture;
616
+ const macosTarget = candidateTargets(manifest.candidate).find((target) => target.platform === "macos");
527
617
  const nodeVersion = String(manifest.matrix.node[0]);
528
618
  const macosDependency = manifest.candidate.signMacos === true ? ", macos-sign" : "";
529
619
  const macosSuccess = manifest.candidate.signMacos === true ? " && needs.macos-sign.result == 'success'" : "";
@@ -531,7 +621,7 @@ function releaseChainJobs(manifest, pm, artifactPrefix) {
531
621
  ? ` - name: Download same-run signed & notarized macOS release
532
622
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
533
623
  with:
534
- name: ${artifactPrefix}-signed-macos-${nodeVersion}
624
+ name: ${artifactPrefix}-signed-macos-${macosTarget.architecture}-${nodeVersion}
535
625
  path: \${{ runner.temp }}/right-git-release/macos`
536
626
  : "";
537
627
  const publishSecretEnvironment = (chain.publishSecrets ?? [])
@@ -555,7 +645,7 @@ function releaseChainJobs(manifest, pm, artifactPrefix) {
555
645
  - name: Download same-run finalized Windows release
556
646
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
557
647
  with:
558
- name: ${artifactPrefix}-signed-windows-${nodeVersion}
648
+ name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
559
649
  path: \${{ runner.temp }}/right-git-release/windows
560
650
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
561
651
  with:
@@ -590,7 +680,7 @@ ${pm.setup}
590
680
  - name: Download same-run finalized Windows release
591
681
  uses: actions/download-artifact@${DOWNLOAD_ARTIFACT_SHA} # v4.3.0
592
682
  with:
593
- name: ${artifactPrefix}-signed-windows-${nodeVersion}
683
+ name: ${artifactPrefix}-signed-windows-${windowsArchitecture}-${nodeVersion}
594
684
  path: \${{ runner.temp }}/right-git-release/windows
595
685
  ${macosDownload}
596
686
  - name: Download same-run installed qualification evidence
@@ -95,6 +95,20 @@
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
97
  "os": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
98
+ "targets": {
99
+ "type": "array",
100
+ "minItems": 1,
101
+ "items": {
102
+ "type": "object",
103
+ "required": ["os", "platform", "architecture"],
104
+ "additionalProperties": false,
105
+ "properties": {
106
+ "os": { "type": "string", "minLength": 1 },
107
+ "platform": { "enum": ["windows", "macos", "linux"] },
108
+ "architecture": { "enum": ["x86_64", "arm64"] }
109
+ }
110
+ }
111
+ },
98
112
  "signWindows": { "type": "boolean" },
99
113
  "signMacos": { "type": "boolean" },
100
114
  "releaseChain": {
package/manifest.test.mjs CHANGED
@@ -62,6 +62,35 @@ test("rust-hybrid supports ci plus a configured unsigned release-candidate lane"
62
62
  assert.equal(validateManifest({ ...releaseChain, candidate: { ...releaseChain.candidate, releaseChain: { ...releaseChain.candidate.releaseChain, publishSecrets: ["TOKEN", "TOKEN"] } } }).valid, false);
63
63
  });
64
64
 
65
+ test("candidate targets permit native multi-architecture matrices independent of normal CI runners", () => {
66
+ const multiArchitecture = {
67
+ ...VALID_MINIMAL,
68
+ profile: "rust-hybrid",
69
+ gate: { path: "scripts/gate.sh" },
70
+ lanes: ["ci", "release-candidate"],
71
+ candidate: {
72
+ buildScript: "ci:candidate",
73
+ checkScript: "ci:candidate-check",
74
+ artifactRoot: "widgets-unsigned",
75
+ os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
76
+ targets: [
77
+ { os: "windows-2025", platform: "windows", architecture: "x86_64" },
78
+ { os: "windows-11-arm", platform: "windows", architecture: "arm64" },
79
+ { os: "macos-15", platform: "macos", architecture: "arm64" },
80
+ { os: "macos-15-intel", platform: "macos", architecture: "x86_64" },
81
+ ],
82
+ signWindows: true,
83
+ signMacos: true,
84
+ },
85
+ };
86
+ assert.equal(validateManifest(multiArchitecture).valid, true, JSON.stringify(validateManifest(multiArchitecture).errors));
87
+ assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "windows-2025", platform: "windows", architecture: "riscv64" }] } }).valid, false);
88
+ assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "windows-11-arm", platform: "windows", architecture: "x86_64" }] } }).valid, false);
89
+ assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "ubuntu-24.04", platform: "windows", architecture: "x86_64" }] } }).valid, false);
90
+ assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, targets: [{ os: "${{ secrets.X }}", platform: "windows", architecture: "x86_64" }] } }).valid, false);
91
+ assert.equal(validateManifest({ ...multiArchitecture, candidate: { ...multiArchitecture.candidate, releaseChain: { windowsFinalizeScript: "release:finalize:win", macosFinalizeScript: "release:finalize:mac", installedQualificationScript: "release:qualify", publishScript: "release:publish", qualificationRunnerVariable: "QUALIFICATION_RUNNER_LABEL" } } }).valid, false);
92
+ });
93
+
65
94
  test("rejects non-object input", () => {
66
95
  for (const bad of [null, undefined, "not an object", 42, ["array"]]) {
67
96
  const { valid, errors } = validateManifest(bad);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/git",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
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
@@ -130,6 +130,46 @@ test("rust-hybrid release candidate can opt into protected macOS signing & notar
130
130
  assert.doesNotMatch(content, /pull_request_target/);
131
131
  });
132
132
 
133
+ test("rust-hybrid multi-architecture candidates bind every native target to exact artifacts and signing", () => {
134
+ const manifest = {
135
+ ...FULL_MANIFEST,
136
+ lanes: ["ci", "release-candidate"],
137
+ profile: "rust-hybrid",
138
+ gate: { path: "scripts/gate.sh" },
139
+ matrix: { os: ["ubuntu-24.04"], node: ["22.23.2"] },
140
+ candidate: {
141
+ buildScript: "ci:candidate",
142
+ checkScript: "ci:candidate-check",
143
+ artifactRoot: "widgets-unsigned",
144
+ os: ["windows-2025", "windows-11-arm", "macos-15", "macos-15-intel"],
145
+ targets: [
146
+ { os: "windows-2025", platform: "windows", architecture: "x86_64" },
147
+ { os: "windows-11-arm", platform: "windows", architecture: "arm64" },
148
+ { os: "macos-15", platform: "macos", architecture: "arm64" },
149
+ { os: "macos-15-intel", platform: "macos", architecture: "x86_64" },
150
+ ],
151
+ signWindows: true,
152
+ signMacos: true,
153
+ },
154
+ templateVersion: "1.5.0",
155
+ };
156
+ const content = renderTemplate(
157
+ fs.readFileSync(path.join(templatesDir, "profiles/rust-hybrid/release-candidate.yml"), "utf8"),
158
+ buildRenderContext(manifest, "/repos/widgets"),
159
+ );
160
+ for (const [runner, triple] of [["windows-2025", "x86_64-pc-windows-msvc"], ["windows-11-arm", "aarch64-pc-windows-msvc"], ["macos-15", "aarch64-apple-darwin"], ["macos-15-intel", "x86_64-apple-darwin"]]) {
161
+ assert.match(content, new RegExp(`os: ${JSON.stringify(runner).replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}`));
162
+ assert.match(content, new RegExp(`targetTriple: ${JSON.stringify(triple).replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}`));
163
+ }
164
+ assert.match(content, /targets: \$\{\{ matrix\.targetTriple \}\}/);
165
+ assert.match(content, /windows-sign:[\s\S]*matrix:[\s\S]*windows-11-arm/);
166
+ assert.match(content, /macos-sign:[\s\S]*matrix:[\s\S]*macos-15-intel/);
167
+ assert.match(content, /name: widgets-unsigned-\$\{\{ matrix\.platform \}\}-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
168
+ assert.match(content, /name: widgets-signed-windows-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
169
+ assert.match(content, /name: widgets-signed-macos-\$\{\{ matrix\.architecture \}\}-22\.23\.2/);
170
+ assert.equal((content.match(/RIGHT_GIT_RELEASE_ARCHITECTURE: \$\{\{ matrix\.architecture \}\}/g) ?? []).length, 4);
171
+ });
172
+
133
173
  test("rust-hybrid release chain preserves signed candidate, installed qualification, then publish", () => {
134
174
  const manifest = {
135
175
  ...FULL_MANIFEST,
@@ -34,6 +34,7 @@ ${PACKAGE_MANAGER_SETUP}
34
34
  with:
35
35
  toolchain: 1.98.0
36
36
  components: rustfmt, clippy
37
+ targets: ${{ matrix.targetTriple }}
37
38
  - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
38
39
  with:
39
40
  workspaces: engine
@@ -53,7 +54,7 @@ ${PACKAGE_MANAGER_SETUP}
53
54
  - name: Upload unsigned candidate handoff
54
55
  uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
55
56
  with:
56
- name: ${ARTIFACT_PREFIX}-unsigned-${{ matrix.os }}-${{ matrix.node }}
57
+ name: ${ARTIFACT_PREFIX}-unsigned-${{ matrix.platform }}-${{ matrix.architecture }}-${{ matrix.node }}
57
58
  path: ${{ runner.temp }}/${CANDIDATE_ARTIFACT_ROOT}/**
58
59
  if-no-files-found: error
59
60
  ${WINDOWS_SIGNING_JOB}