@rightkit/release 0.2.68 → 0.2.69

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.
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.68",
3
+ "version": "0.2.69",
4
4
  "description": "Portable Right Suite release CLI/SDK: native-host signed installers, updater artifacts, hardening, immutable GitHub Release upload, and add-on adoption.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -15,6 +15,13 @@
15
15
  "*.py"
16
16
  ],
17
17
  "sideEffects": false,
18
+ "scripts": {
19
+ "test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
20
+ "test:registry-parity": "node registry-parity.mjs --allow-unpublished",
21
+ "prepublishOnly": "pnpm test && pnpm test:registry-parity",
22
+ "doctor:all": "node --test right-suite-contract.test.mjs",
23
+ "verify:standalone": "node standalone-clone-verify.mjs"
24
+ },
18
25
  "publishConfig": {
19
26
  "registry": "https://registry.npmjs.org/",
20
27
  "access": "public"
@@ -24,10 +31,5 @@
24
31
  "url": "git+https://github.com/bogusyogi/claude.git",
25
32
  "directory": "tools/rightkit/packages/release"
26
33
  },
27
- "scripts": {
28
- "test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
29
- "test:registry-parity": "node registry-parity.mjs --allow-unpublished",
30
- "doctor:all": "node --test right-suite-contract.test.mjs",
31
- "verify:standalone": "node standalone-clone-verify.mjs"
32
- }
33
- }
34
+ "packageManager": "pnpm@11.24.0"
35
+ }
@@ -0,0 +1,140 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ const workspace = path.resolve(
8
+ process.env.RIGHT_SUITE_CONTRACT_WORKSPACE
9
+ ?? new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"),
10
+ );
11
+
12
+ const packageRoots = [
13
+ "coderight/apps/coderight-tauri",
14
+ "cutright/apps/studio",
15
+ "genright",
16
+ "heardright/tauri-app-next",
17
+ "mailright",
18
+ "membrane/apps/membrane-hub",
19
+ "scraperight",
20
+ "viewright",
21
+ "voiceright",
22
+ ];
23
+
24
+ const releaseConfigRoots = [
25
+ ...packageRoots,
26
+ "tools/screenright",
27
+ ];
28
+
29
+ const rightGitRepos = ["cutright", "legion", "membrane"];
30
+ const privateRepos = ["rightsites", "sellright", "workright"];
31
+
32
+ function executableSource(source) {
33
+ return source
34
+ .replace(/\/\*[\s\S]*?\*\//g, "")
35
+ .split("\n")
36
+ .filter((line) => !/^\s*(?:\/\/|#)/.test(line))
37
+ .join("\n");
38
+ }
39
+
40
+ function pipelineScripts(root) {
41
+ const repo = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8", windowsHide: true }).trim();
42
+ const prefix = path.relative(repo, root).replaceAll(path.sep, "/");
43
+ const tracked = execFileSync("git", ["-C", repo, "ls-files", "-z"], { encoding: "utf8", windowsHide: true })
44
+ .split("\0")
45
+ .filter(Boolean);
46
+ return tracked
47
+ .filter((entry) => {
48
+ const local = prefix ? entry.slice(prefix.length + 1) : entry;
49
+ if (prefix && !entry.startsWith(`${prefix}/`)) return false;
50
+ return (
51
+ local.startsWith("scripts/")
52
+ || local === "package.ps1"
53
+ || local === "package.sh"
54
+ ) && /\.(?:mjs|cjs|js|ts|ps1|sh|py)$/i.test(local)
55
+ && !/(?:^|[.-])test(?:[.-]|$)/i.test(path.basename(local));
56
+ })
57
+ .map((entry) => path.join(repo, entry))
58
+ .filter((entry) => existsSync(entry));
59
+ }
60
+
61
+ test("product package pipelines use installed RightKit commands", () => {
62
+ const violations = [];
63
+ for (const root of packageRoots) {
64
+ const manifestPath = path.join(workspace, root, "package.json");
65
+ if (!existsSync(manifestPath)) continue;
66
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
67
+ for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
68
+ if (!/^(?:build|package|publish|release|deps|verify:rust|test:rust|lint:rust)/.test(name)) continue;
69
+ if (/(?:\.\.\/[\w./-]*)?tools\/right-release|tools\\right-release/i.test(command)) violations.push(`${root} ${name}: retired workspace-local right-release source`);
70
+ if (/(?:^|&&|\|\||;)\s*cargo\s+/i.test(command)) violations.push(`${root} ${name}: bare cargo`);
71
+ if (/(?:^|&&|\|\||;)\s*npx\s+/i.test(command)) violations.push(`${root} ${name}: npx`);
72
+ if (/(?:^|&&|\|\||;)\s*npm\s+(?:ci|install)\b/i.test(command)) violations.push(`${root} ${name}: npm install`);
73
+ }
74
+ }
75
+ assert.deepEqual(violations, []);
76
+ });
77
+
78
+ test("release configs resolve Cargo output through RightKit", () => {
79
+ const violations = [];
80
+ for (const root of releaseConfigRoots) {
81
+ const configPath = path.join(workspace, root, "right-release.config.mjs");
82
+ if (!existsSync(configPath)) continue;
83
+ const source = executableSource(readFileSync(configPath, "utf8"));
84
+ const hardcodedTargetRoot = /(?:const|let|var)\s+\w+\s*=\s*["'`]src-tauri[\\/]target/i.test(source);
85
+ const hardcodedTargetArtifact = /(?:file|signature)\s*:\s*["'`]src-tauri[\\/]target/i.test(source);
86
+ if (!hardcodedTargetRoot && !hardcodedTargetArtifact) continue;
87
+ if (!/@rightkit\/release\/cargo-target\.mjs|\.\/scripts\/lib\/target-root\.mjs/.test(source)) violations.push(`${root}: no RightKit target resolver`);
88
+ if (!/resolve(?:TargetRoot|WindowsReleaseDir)\s*\(/.test(source)) violations.push(`${root}: resolver not invoked`);
89
+ if (hardcodedTargetRoot) violations.push(`${root}: hardcoded target root`);
90
+ if (hardcodedTargetArtifact) violations.push(`${root}: hardcoded target artifact`);
91
+ }
92
+ assert.deepEqual(violations, []);
93
+ });
94
+
95
+ test("product pipeline children use managed Rust commands", () => {
96
+ const violations = [];
97
+ for (const root of packageRoots) {
98
+ for (const file of pipelineScripts(path.join(workspace, root))) {
99
+ const source = executableSource(readFileSync(file, "utf8"));
100
+ const label = path.relative(workspace, file);
101
+ if (/(?:spawnSync|spawn|execFileSync|execFile)\s*\(\s*["'`](?:cargo|rustc|rustdoc)["'`]/.test(source)) violations.push(`${label}: unmanaged JS child`);
102
+ if (/subprocess\.(?:run|Popen)\s*\(\s*\[\s*["'](?:cargo|rustc|rustdoc)["']/.test(source)) violations.push(`${label}: unmanaged Python child`);
103
+ if (/(?:^|\n)\s*(?:&\s*)?(?:cargo|rustc|rustdoc)\s+(?:build|check|clippy|metadata|test|-vV)\b/m.test(source)) violations.push(`${label}: unmanaged shell command`);
104
+ }
105
+ }
106
+ assert.deepEqual(violations, []);
107
+ });
108
+
109
+ test("hosted workflows are generated by right-git", () => {
110
+ const violations = [];
111
+ for (const root of rightGitRepos) {
112
+ const workflowRoot = path.join(workspace, root, ".github", "workflows");
113
+ if (!existsSync(workflowRoot)) continue;
114
+ for (const entry of readdirSync(workflowRoot, { withFileTypes: true })) {
115
+ if (!entry.isFile() || !/\.ya?ml$/i.test(entry.name)) continue;
116
+ const source = readFileSync(path.join(workflowRoot, entry.name), "utf8");
117
+ if (!/^# Managed by right-git\b/.test(source)) violations.push(`${root}/.github/workflows/${entry.name}: not right-git managed`);
118
+ }
119
+ }
120
+ for (const root of privateRepos) {
121
+ const workflowRoot = path.join(workspace, root, ".github", "workflows");
122
+ const workflows = existsSync(workflowRoot)
123
+ ? readdirSync(workflowRoot).filter((entry) => /\.ya?ml$/i.test(entry))
124
+ : [];
125
+ if (workflows.length) violations.push(`${root}: private repo has ${workflows.join(", ")}`);
126
+ }
127
+ assert.deepEqual(violations, []);
128
+ });
129
+
130
+ test("workspace memory pipelines route Rust through RightKit", () => {
131
+ for (const relative of [
132
+ "tools/pipelines/memory/dispatch/gate.py",
133
+ "tools/pipelines/memory/install-cortex.py",
134
+ "tools/pipelines/memory/verify-source-owner.py",
135
+ ]) {
136
+ const source = executableSource(readFileSync(path.join(workspace, relative), "utf8"));
137
+ assert.doesNotMatch(source, /(?:^|&&|\|\||;)\s*cargo\s+(?:metadata|build|check|clippy|test)\b/m, `${relative} invokes bare Cargo shell command`);
138
+ assert.doesNotMatch(source, /subprocess\.run\(\s*\[\s*["']cargo["']/m, `${relative} invokes bare Cargo subprocess`);
139
+ }
140
+ });
@@ -21,7 +21,7 @@ test('registry parity distinguishes an unpublished version from registry failure
21
21
  assert.deepEqual(await verifyRegistryParity({ allowUnpublished: true, fetchImpl: notFound }), {
22
22
  status: 'UNPUBLISHED',
23
23
  name: '@rightkit/release',
24
- version: '0.2.68',
24
+ version: '0.2.69',
25
25
  });
26
26
  await assert.rejects(
27
27
  verifyRegistryParity({ fetchImpl: notFound }),
package/release.mjs CHANGED
@@ -29,7 +29,8 @@ const RIGHTKIT_ALLOWED = buildAllowedVersions(
29
29
  );
30
30
  const RIGHTKIT_CARGO_ALLOWED = buildAllowedVersions(RIGHTKIT_VERSIONS.cargo, RIGHTKIT_VERSIONS.stagedCargo);
31
31
  const FORBIDDEN_RIGHTKIT_SPEC = /^(?:git|file|link|workspace):|github|github\.com/i;
32
- const WINDOWS_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-nsis-v1";
32
+ const WINDOWS_NSIS_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-nsis-v1";
33
+ const WINDOWS_PORTABLE_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-portable-v1";
33
34
  const DEFAULT_NOTARIZATION_TIMEOUT_MS = 30 * 60 * 1000;
34
35
 
35
36
  const args = process.argv.slice(2);
@@ -91,7 +92,7 @@ if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<m
91
92
 
92
93
  const root = path.dirname(configPath);
93
94
  const workdir = path.resolve(root, config.workdir ?? ".");
94
- await validateRightKitPackageContract(root, config.app);
95
+ await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
95
96
  validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
96
97
  const target = config.targets?.[opts.platform];
97
98
  if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
@@ -101,14 +102,16 @@ const legalContract = config.legal
101
102
  if (target.signed !== true) {
102
103
  fail(`${config.app ?? "app"} ${opts.platform} must declare signed: true; unsigned release targets are forbidden`);
103
104
  }
104
- if (opts.platform === "win" && !target.sign?.files?.length) {
105
- fail(`${config.app ?? "app"} win must declare sign.files for the signed release pipeline`);
106
- }
107
105
  if (opts.platform === "win") {
108
- if (target.signingContract !== WINDOWS_SIGNING_CONTRACT) fail(`${config.app ?? "app"} win must declare signingContract: ${WINDOWS_SIGNING_CONTRACT}`);
106
+ const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
107
+ const isPortable = target.signingContract === WINDOWS_PORTABLE_SIGNING_CONTRACT;
108
+ if (!isNsis && !isPortable) fail(`${config.app ?? "app"} win must declare signingContract: ${WINDOWS_NSIS_SIGNING_CONTRACT} or ${WINDOWS_PORTABLE_SIGNING_CONTRACT}`);
109
109
  if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} win must declare prePackage for the raw EXE build`);
110
- if (target.sign?.prePackageFiles?.length !== 1) fail(`${config.app ?? "app"} win must declare exactly one sign.prePackageFiles raw EXE`);
111
- if (target.sign.prePackageFiles.some((file) => target.sign.files.includes(file))) fail(`${config.app ?? "app"} win raw EXE and installer signing files must be distinct`);
110
+ if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} win must declare at least one sign.prePackageFiles raw EXE`);
111
+ if (isNsis && target.sign.prePackageFiles.length !== 1) fail(`${config.app ?? "app"} win NSIS must declare exactly one sign.prePackageFiles raw EXE`);
112
+ if (isNsis && !target.sign?.files?.length) fail(`${config.app ?? "app"} win NSIS must declare sign.files for installer signing`);
113
+ if (isNsis && target.sign.prePackageFiles.some((file) => target.sign.files.includes(file))) fail(`${config.app ?? "app"} win raw EXE and installer signing files must be distinct`);
114
+ if (isPortable && target.sign?.files?.length) fail(`${config.app ?? "app"} win portable release must sign every executable through sign.prePackageFiles and omit sign.files`);
112
115
  }
113
116
  if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
114
117
  /**
@@ -206,7 +209,8 @@ if (opts.platform === "win") {
206
209
  // bundling, and doing that to signed bytes invalidates Authenticode — the exact
207
210
  // defect that shipped invalid binaries. Idempotent, so an app that still patches
208
211
  // in its own prePackage script simply reports alreadyPatched here.
209
- bundleMarkerReceipts = opts.dryRun ? [] : rawFiles.map((file) => patchTauriBundleType(file, { bundle: target.bundleMarker ?? "nsis" }));
212
+ const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
213
+ bundleMarkerReceipts = !isNsis || opts.dryRun ? [] : rawFiles.map((file) => patchTauriBundleType(file, { bundle: target.bundleMarker ?? "nsis" }));
210
214
  for (const receipt of bundleMarkerReceipts) {
211
215
  console.log(`right-release: bundle marker ${receipt.bundle} ${receipt.alreadyPatched ? "already applied" : "applied"} at offset ${receipt.offset} in ${path.basename(receipt.file)}`);
212
216
  }
@@ -219,7 +223,7 @@ for (const rel of target.artifacts ?? []) {
219
223
  await mustExist(path.resolve(root, rel), `missing release artifact: ${rel}`);
220
224
  }
221
225
 
222
- if (opts.platform === "win" && target.sign?.files?.length) {
226
+ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT) {
223
227
  const files = target.sign.files.map((p) => path.resolve(root, p));
224
228
  for (const file of files) await mustExist(file, `missing signing artifact: ${file}`);
225
229
  // Before the installer earns its own signature, prove it carries the bytes we
@@ -360,16 +364,16 @@ function commandTimeoutMs(command) {
360
364
  : undefined;
361
365
  }
362
366
 
363
- async function validateRightKitPackageContract(root, appName) {
367
+ async function validateRightKitPackageContract(root, appName, hostedWorkflows) {
364
368
  const packageJsonPath = path.join(root, "package.json");
365
369
  const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
366
370
  const scripts = pkg.scripts ?? {};
367
371
  assertQaBackdoorContract(root, scripts);
368
372
  const workflowDir = path.join(root, ".github", "workflows");
369
373
  if (existsSync(workflowDir)) {
370
- const workflows = await readdir(workflowDir);
374
+ const workflows = (await readdir(workflowDir)).sort();
371
375
  if (workflows.length) {
372
- fail(`${appName ?? pkg.name ?? "app"} hosted workflow files are forbidden: ${workflows.join(", ")}`);
376
+ await validateHostedWorkflows(root, appName ?? pkg.name ?? "app", workflows, hostedWorkflows);
373
377
  }
374
378
  }
375
379
  if (pkg.packageManager !== RIGHTKIT_VERSIONS.packageManager) {
@@ -394,6 +398,33 @@ async function validateRightKitPackageContract(root, appName) {
394
398
  }
395
399
  }
396
400
 
401
+ async function validateHostedWorkflows(root, appName, workflows, policy) {
402
+ if (policy !== "right-git-ci-only") {
403
+ fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
404
+ }
405
+ if (workflows.length !== 1 || workflows[0] !== "ci.yml") {
406
+ fail(`${appName} right-git-ci-only policy permits only ci.yml, got: ${workflows.join(", ")}`);
407
+ }
408
+ const manifestPath = path.join(root, ".rightgit.json");
409
+ let manifest;
410
+ try {
411
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
412
+ } catch {
413
+ fail(`${appName} right-git-ci-only policy requires a valid .rightgit.json`);
414
+ }
415
+ if (manifest?.schemaVersion !== 1 || JSON.stringify(manifest?.lanes) !== JSON.stringify(["ci"])) {
416
+ fail(`${appName} right-git-ci-only policy requires .rightgit.json lanes=["ci"]`);
417
+ }
418
+ const source = await readFile(path.join(root, ".github", "workflows", "ci.yml"), "utf8");
419
+ if (!source.startsWith("# Managed by right-git")) {
420
+ fail(`${appName} ci.yml is not marked as right-git managed`);
421
+ }
422
+ if (/^\s*(?:actions|attestations|contents|id-token|packages|security-events):\s*write\s*$/mi.test(source)
423
+ || /\bsecrets\s*\./i.test(source)) {
424
+ fail(`${appName} right-git managed ci.yml requests release-capable permissions or secrets`);
425
+ }
426
+ }
427
+
397
428
  function buildAllowedVersions(published = {}, staged = {}, legacy = {}) {
398
429
  const allowed = new Map();
399
430
  for (const [name, value] of [
@@ -423,7 +454,7 @@ function expandEnv(value) {
423
454
 
424
455
  async function signWindows(files, phase, root) {
425
456
  const args = [SIGN_WINDOWS];
426
- const receipt = path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `windows-${phase}.json`);
457
+ const receipt = windowsReceiptPath(root, phase);
427
458
  if (opts.dryRun) args.push("--dry-run");
428
459
  else {
429
460
  if (existsSync(receipt)) unlinkSync(receipt);
@@ -435,6 +466,8 @@ async function signWindows(files, phase, root) {
435
466
  }
436
467
 
437
468
  function windowsReceiptPath(root, phase) {
469
+ const configured = phase === "raw-exe" ? target.sign?.receipt : target.sign?.installerReceipt;
470
+ if (configured) return path.resolve(root, configured);
438
471
  return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `windows-${phase}.json`);
439
472
  }
440
473
 
package/release.test.mjs CHANGED
@@ -16,7 +16,7 @@ function git(cwd, ...args) {
16
16
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
17
17
  }
18
18
 
19
- function fixture({ signed = true, publish = false, signingContract = "windows-raw-exe-authenticode-before-nsis-v1", prePackageFiles = ["raw.exe"], packageJson, packageCommand, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win" } = {}) {
19
+ function fixture({ signed = true, publish = false, signingContract = "windows-raw-exe-authenticode-before-nsis-v1", prePackageFiles = ["raw.exe"], signFiles = ["fixture.exe"], packageJson, packageCommand, cargoManifest, cargoConfig, repoCargoConfig, siblingCargoManifest, buildInputs = defaultBuildInputs, platform = "win", hostedWorkflows } = {}) {
20
20
  const fixtureRoot = mkdtempSync(path.join(os.tmpdir(), "right-release-test-"));
21
21
  const dir = repoCargoConfig ? path.join(fixtureRoot, "apps", "fixture") : fixtureRoot;
22
22
  mkdirSync(dir, { recursive: true });
@@ -45,6 +45,7 @@ function fixture({ signed = true, publish = false, signingContract = "windows-ra
45
45
  app: "fixture",
46
46
  packageManager: "pnpm",
47
47
  checks: [],
48
+ ...(hostedWorkflows ? { hostedWorkflows } : {}),
48
49
  targets: {
49
50
  [platform]: {
50
51
  ...(buildInputs ? { buildInputs } : {}),
@@ -52,7 +53,7 @@ function fixture({ signed = true, publish = false, signingContract = "windows-ra
52
53
  package: packageCommand ?? { cmd: "node", args: ["-e", "process.exit(0)"] },
53
54
  ...(publish ? { publish: { cmd: "node", args: ["publish-update.mjs"] } } : {}),
54
55
  artifacts: [],
55
- ...(platform === "win" ? { signingContract, prePackage: { cmd: "node", args: ["-e", "process.exit(0)"] }, sign: { prePackageFiles, files: ["fixture.exe"] } } : {}),
56
+ ...(platform === "win" ? { signingContract, prePackage: { cmd: "node", args: ["-e", "process.exit(0)"] }, sign: { prePackageFiles, files: signFiles } } : {}),
56
57
  updater: { artifacts: [{ file: "fixture.exe", signature: "fixture.exe.sig", platform: "windows-x86_64", key: "fixture/fixture.exe" }] },
57
58
  hardening: [],
58
59
  },
@@ -240,6 +241,32 @@ test("rejects Windows targets with missing or ambiguous signing configuration",
240
241
  }
241
242
  });
242
243
 
244
+ test("accepts portable Windows releases with multiple pre-package executables and no installer", () => {
245
+ const result = run(fixture({
246
+ signingContract: "windows-raw-exe-authenticode-before-portable-v1",
247
+ prePackageFiles: ["legion.exe", "legion-hook.exe", "legion-mcp.exe"],
248
+ signFiles: [],
249
+ }), "--tier=patch");
250
+ assert.equal(result.status, 0, result.stderr);
251
+ assert.match(result.stdout, /raw-exe/);
252
+ assert.doesNotMatch(result.stdout, /bundle marker/);
253
+ });
254
+
255
+ test("uses a configured signing receipt path for portable architecture isolation", () => {
256
+ const source = readFileSync(release, "utf8");
257
+ assert.match(source, /phase === "raw-exe" \? target\.sign\?\.receipt : target\.sign\?\.installerReceipt/);
258
+ assert.match(source, /if \(configured\) return path\.resolve\(root, configured\)/);
259
+ });
260
+
261
+ test("rejects portable Windows releases that declare a second installer signing phase", () => {
262
+ const result = run(fixture({
263
+ signingContract: "windows-raw-exe-authenticode-before-portable-v1",
264
+ prePackageFiles: ["legion.exe"],
265
+ }), "--tier=patch");
266
+ assert.notEqual(result.status, 0);
267
+ assert.match(result.stderr, /portable.*omit sign\.files/i);
268
+ });
269
+
243
270
  test("rejects Git, path, or link RightKit app dependencies before release work starts", () => {
244
271
  const result = run(
245
272
  fixture({
@@ -273,6 +300,35 @@ test("rejects hosted workflow files before release work starts", () => {
273
300
  assert.match(result.stderr, /hosted workflow.*forbidden/i);
274
301
  });
275
302
 
303
+ test("accepts one right-git managed CI workflow without granting hosted release authority", () => {
304
+ const config = fixture({ hostedWorkflows: "right-git-ci-only" });
305
+ const root = path.dirname(config);
306
+ const workflowDir = path.join(root, ".github", "workflows");
307
+ mkdirSync(workflowDir, { recursive: true });
308
+ writeFileSync(path.join(root, ".rightgit.json"), JSON.stringify({ schemaVersion: 1, lanes: ["ci"] }));
309
+ writeFileSync(path.join(workflowDir, "ci.yml"), "# Managed by right-git — do not hand-edit.\npermissions:\n contents: read\njobs: {}\n");
310
+ git(root, "add", ".");
311
+ git(root, "commit", "-m", "add managed ci");
312
+
313
+ const result = run(config, "--tier=patch");
314
+ assert.equal(result.status, 0, result.stderr);
315
+ });
316
+
317
+ test("rejects a self-labeled right-git workflow with release-capable authority", () => {
318
+ const config = fixture({ hostedWorkflows: "right-git-ci-only" });
319
+ const root = path.dirname(config);
320
+ const workflowDir = path.join(root, ".github", "workflows");
321
+ mkdirSync(workflowDir, { recursive: true });
322
+ writeFileSync(path.join(root, ".rightgit.json"), JSON.stringify({ schemaVersion: 1, lanes: ["ci"] }));
323
+ writeFileSync(path.join(workflowDir, "ci.yml"), "# Managed by right-git\npermissions:\n id-token: write\njobs: {}\n");
324
+ git(root, "add", ".");
325
+ git(root, "commit", "-m", "add unsafe ci");
326
+
327
+ const result = run(config, "--tier=patch");
328
+ assert.notEqual(result.status, 0);
329
+ assert.match(result.stderr, /release-capable permissions or secrets/i);
330
+ });
331
+
276
332
  test("doctor accepts an exact crates.io RightKit pin with benign Cargo config", () => {
277
333
  const config = fixture({
278
334
  platform: hostPlatform,
@@ -489,7 +489,7 @@ test("RightKit exposes one current version manifest", () => {
489
489
  "@rightkit/logs": "0.1.4",
490
490
  "@rightkit/platform-ui": "0.1.1",
491
491
  "@rightkit/qa": "0.2.1",
492
- "@rightkit/release": "0.2.68",
492
+ "@rightkit/release": "0.2.69",
493
493
  "@rightkit/tauri": "0.1.1",
494
494
  "@rightkit/updates": "0.2.4",
495
495
  });
@@ -498,7 +498,7 @@ test("RightKit exposes one current version manifest", () => {
498
498
  "@rightkit/hooks": ["0.1.0"],
499
499
  "@rightkit/legal": ["0.3.0"],
500
500
  "@rightkit/license": ["0.1.6"],
501
- "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61", "0.2.62", "0.2.63", "0.2.64", "0.2.65", "0.2.66", "0.2.67"],
501
+ "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61", "0.2.62", "0.2.63", "0.2.64", "0.2.65", "0.2.66", "0.2.67", "0.2.68"],
502
502
  "@rightkit/qa": ["0.1.0", "0.2.0"],
503
503
  });
504
504
  assert.ok(
@@ -717,8 +717,12 @@ for (const app of apps) {
717
717
  }
718
718
 
719
719
  for (const app of macOnlyApps) {
720
- test(`${app.key} follows the macOS-only AppKit Right Release contract`, async () => {
720
+ test(`${app.key} follows the macOS-only AppKit Right Release contract`, async (context) => {
721
721
  const root = path.join(workspace, app.root);
722
+ if (!existsSync(path.join(root, "package.json"))) {
723
+ context.skip(`${app.key} is not present in this checkout`);
724
+ return;
725
+ }
722
726
  const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
723
727
  assert.equal(pkg.scripts["release:doctor"], "right-release doctor");
724
728
  assert.equal(pkg.scripts["release:build:mac"], "right-release build --platform mac");
@@ -22,7 +22,7 @@
22
22
  "@rightkit/logs": "0.1.4",
23
23
  "@rightkit/platform-ui": "0.1.1",
24
24
  "@rightkit/qa": "0.2.1",
25
- "@rightkit/release": "0.2.68",
25
+ "@rightkit/release": "0.2.69",
26
26
  "@rightkit/tauri": "0.1.1",
27
27
  "@rightkit/updates": "0.2.4"
28
28
  },
@@ -63,7 +63,8 @@
63
63
  "0.2.64",
64
64
  "0.2.65",
65
65
  "0.2.66",
66
- "0.2.67"
66
+ "0.2.67",
67
+ "0.2.68"
67
68
  ],
68
69
  "@rightkit/qa": [
69
70
  "0.1.0",