@rightkit/release 0.2.150 → 0.2.153

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.
Files changed (67) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +119 -8
  4. package/build-release.mjs +16 -2
  5. package/cli/right-release.mjs +18 -5
  6. package/committed-source.mjs +19 -1
  7. package/crate-graph.mjs +121 -0
  8. package/development-signing.mjs +8 -0
  9. package/development.mjs +46 -13
  10. package/devtools-policy.json +5 -0
  11. package/devtools-policy.mjs +119 -0
  12. package/direct-bootstrap.mjs +9 -3
  13. package/dmg-build.mjs +19 -5
  14. package/dmg-layout.mjs +2 -2
  15. package/dmg-spec.mjs +97 -0
  16. package/execution-policy.mjs +17 -1
  17. package/github-release.mjs +25 -7
  18. package/github-upload-gate.mjs +84 -0
  19. package/hardeningscan.mjs +3 -0
  20. package/inventory-check.mjs +4 -4
  21. package/mac-bundle-signing.mjs +1 -1
  22. package/mac-sign-core.mjs +97 -0
  23. package/mac-signing-matrix.mjs +186 -0
  24. package/macos-notarization.mjs +18 -2
  25. package/msix-appinstaller.mjs +4 -1
  26. package/msix-config.mjs +5 -1
  27. package/msix-repair-guard.mjs +24 -0
  28. package/msix-repair-guard.ps1 +356 -0
  29. package/notarize-macos.mjs +4 -3
  30. package/ownership-catalog-config.json +6 -7
  31. package/ownership-command.mjs +69 -8
  32. package/ownership-evidence.mjs +1 -1
  33. package/ownership-key-authority.mjs +1 -1
  34. package/ownership-key-command.mjs +15 -3
  35. package/ownership-key-elevation.mjs +11 -2
  36. package/ownership-mandatory-set.json +420 -0
  37. package/ownership-mandatory-set.mjs +273 -0
  38. package/ownership.mjs +2 -2
  39. package/package.json +15 -3
  40. package/packaged-app-identity.mjs +88 -0
  41. package/preflight.mjs +25 -3
  42. package/prune-r2.mjs +28 -3
  43. package/publication-gate.mjs +49 -10
  44. package/publish-cargo.mjs +21 -1
  45. package/publish-swift.mjs +78 -141
  46. package/publish-update.mjs +24 -2
  47. package/qa-build-features.mjs +216 -0
  48. package/qa-control-scan.mjs +435 -0
  49. package/registry-parity.mjs +18 -1
  50. package/release-invocation.mjs +2 -1
  51. package/release-state.mjs +9 -0
  52. package/release-switch-registry.json +33 -0
  53. package/release-switch-scan.mjs +452 -0
  54. package/release.mjs +324 -26
  55. package/renamed-away.mjs +29 -0
  56. package/rightkit-ownership.json +682 -709
  57. package/rightkit-sync.mjs +8 -4
  58. package/rightkit-version-policy.mjs +18 -0
  59. package/rightkit-versions.json +212 -127
  60. package/runtime-budget-classes.json +4 -0
  61. package/runtime-budgets.mjs +870 -0
  62. package/runtime-bundle-scan.mjs +61 -0
  63. package/sign-macos.mjs +28 -30
  64. package/sign-updater.mjs +5 -2
  65. package/swift-mac-dmg.mjs +7 -2
  66. package/swift-package-command.mjs +1 -1
  67. package/upload-release.mjs +10 -37
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // Fails when a built or installed bundle carries a Node or Python runtime,
3
+ // interpreter libraries, or script sources (M19 owner gap 2; replaces the
4
+ // per-app scan-bundle.mjs copies in CutRight and GenRight).
5
+ // Usage: right-release bundle-scan [--allow-name <basename>]... <bundle-root>...
6
+ import { readdirSync, statSync } from "node:fs";
7
+ import { basename, extname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ export const BANNED_NAMES = Object.freeze([
11
+ /^node(js)?(\.exe)?$/i, /^libnode/i, /^python[\d.]*w?(\.exe)?$/i, /^libpython/i,
12
+ /^python\d+\.dll$/i, /^pythonw?\d*\.zip$/i,
13
+ ]);
14
+ export const BANNED_EXTENSIONS = Object.freeze(new Set([".js", ".mjs", ".cjs", ".py", ".pyc", ".pyo", ".pyd", ".pyz"]));
15
+ export const BANNED_DIRECTORIES = Object.freeze(new Set(["node_modules", "site-packages", "__pycache__"]));
16
+
17
+ /** Scan one bundle root. `allowNames` exempts exact basenames (product-declared exceptions). */
18
+ export function scanRuntimeBundle(root, { allowNames = [] } = {}) {
19
+ const allowed = new Set(allowNames);
20
+ const offenders = [];
21
+ let files = 0;
22
+ const walk = (dir) => {
23
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
24
+ const path = join(dir, entry.name);
25
+ if (entry.isSymbolicLink()) continue;
26
+ if (entry.isDirectory()) {
27
+ if (BANNED_DIRECTORIES.has(entry.name) && !allowed.has(entry.name)) offenders.push(path);
28
+ walk(path);
29
+ continue;
30
+ }
31
+ files += 1;
32
+ const name = basename(path);
33
+ if (allowed.has(name)) continue;
34
+ if (BANNED_EXTENSIONS.has(extname(name).toLowerCase()) || BANNED_NAMES.some((re) => re.test(name))) offenders.push(path);
35
+ }
36
+ };
37
+ if (statSync(root).isDirectory()) walk(root); else files = 1;
38
+ return { root, files, offenders };
39
+ }
40
+
41
+ export function runtimeBundleScanFailed(results) {
42
+ return results.some((r) => r.offenders.length > 0 || r.files === 0);
43
+ }
44
+
45
+ function main(argv) {
46
+ const roots = [];
47
+ const allowNames = [];
48
+ for (let i = 0; i < argv.length; i++) {
49
+ if (argv[i] === "--allow-name") allowNames.push(argv[++i] ?? "");
50
+ else roots.push(argv[i]);
51
+ }
52
+ if (!roots.length || allowNames.includes("")) {
53
+ console.error("usage: right-release bundle-scan [--allow-name <basename>]... <bundle-root>...");
54
+ return 2;
55
+ }
56
+ const results = roots.map((root) => scanRuntimeBundle(root, { allowNames }));
57
+ console.log(JSON.stringify(results, null, 2));
58
+ return runtimeBundleScanFailed(results) ? 1 : 0;
59
+ }
60
+
61
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) process.exit(main(process.argv.slice(2)));
package/sign-macos.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "./background-process.mjs";
5
- import { fileEvidence } from "./macos-file-evidence.mjs";
5
+ import { readSignManifest, signMacosEntries } from "./mac-sign-core.mjs";
6
6
  import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
7
7
  import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
8
8
  import { runMacCodesignCommands } from "./macos-signing-session.mjs";
@@ -13,44 +13,46 @@ const receiptIndex = args.indexOf("--receipt");
13
13
  const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
14
14
  const entitlementsIndex = args.indexOf("--entitlements");
15
15
  const entitlementsPath = entitlementsIndex >= 0 ? args[entitlementsIndex + 1] : null;
16
+ const manifestIndex = args.indexOf("--manifest");
17
+ const manifestPath = manifestIndex >= 0 ? args[manifestIndex + 1] : null;
16
18
  if (entitlementsIndex >= 0 && (!entitlementsPath || entitlementsPath.startsWith("--"))) fail("--entitlements requires a path");
19
+ if (manifestIndex >= 0 && (!manifestPath || manifestPath.startsWith("--"))) fail("--manifest requires a path");
17
20
  const files = args
18
- .filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && arg !== "--entitlements"
21
+ .filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && arg !== "--entitlements" && arg !== "--manifest"
19
22
  && (receiptIndex < 0 || index !== receiptIndex + 1)
20
- && (entitlementsIndex < 0 || index !== entitlementsIndex + 1))
23
+ && (entitlementsIndex < 0 || index !== entitlementsIndex + 1)
24
+ && (manifestIndex < 0 || index !== manifestIndex + 1))
21
25
  .map((arg) => path.resolve(arg));
22
26
 
23
- if (!files.length) fail("usage: node sign-macos.mjs [--dry-run] [--entitlements <path>] --receipt <path> <file>...");
27
+ // A manifest carries per-file settings (entitlements, codesign options) in signing order; the plain form
28
+ // signs every positional file with the same optional --entitlements.
29
+ if (manifestPath && (files.length || entitlementsPath)) fail("--manifest cannot be combined with positional files or --entitlements");
30
+ if (!manifestPath && !files.length) fail("usage: node sign-macos.mjs [--dry-run] [--entitlements <path>] --receipt <path> <file>... | --manifest <json>");
24
31
  if (!receiptPath) fail("--receipt requires a path");
32
+ let entries;
33
+ if (manifestPath) {
34
+ try { entries = readSignManifest(path.resolve(manifestPath)); } catch (error) { fail(error.message); }
35
+ } else {
36
+ entries = files.map((file) => ({ file, ...(entitlementsPath ? { entitlements: path.resolve(entitlementsPath) } : {}), options: [] }));
37
+ }
25
38
  if (dryRun) {
26
- console.log(`dry-run: Developer ID signing ${files.join(", ")}`);
39
+ console.log(`dry-run: Developer ID signing ${entries.map((entry) => entry.file).join(", ")}`);
27
40
  process.exit(0);
28
41
  }
29
42
  if (process.platform !== "darwin") fail("macOS signing must run on macOS");
30
43
  let identity;
31
44
  try { identity = resolveMacosDeveloperIdIdentity(); } catch (error) { fail(error.message); }
32
- for (const file of files) if (!existsSync(file)) fail(`missing file: ${file}`);
33
- if (entitlementsPath && !existsSync(entitlementsPath)) fail(`missing entitlements: ${entitlementsPath}`);
45
+ for (const entry of entries) if (!existsSync(entry.file)) fail(`missing file: ${entry.file}`);
46
+ for (const entry of entries) if (entry.entitlements && !existsSync(entry.entitlements)) fail(`missing entitlements: ${entry.entitlements}`);
34
47
 
35
- const evidence = [];
36
- for (const file of files) {
37
- const before = fileEvidence(file);
38
- try {
39
- const [signed] = runMacCodesignCommands([["--force", "--options", "runtime", "--timestamp", "--sign", identity,
40
- ...(entitlementsPath ? ["--entitlements", path.resolve(entitlementsPath)] : []), file]]);
41
- if (signed.error || signed.status !== 0) fail(`codesign failed: ${signed.stderr ?? signed.error?.message ?? signed.status}`);
42
- } catch (error) { fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
43
- const verification = run("codesign", ["--verify", "--strict", "--verbose=4", file]);
44
- const details = run("codesign", ["-dvv", file]);
45
- if (!/valid on disk/i.test(verification) || !/satisfies its designated requirement/i.test(verification)) {
46
- fail(`strict codesign verification failed for ${file}`);
47
- }
48
- if (!new RegExp(`^Authority=${escapeRegExp(identity)}$`, "m").test(details) && !details.includes(`Authority=${identity}`)) {
49
- fail(`codesign identity mismatch for ${file}: expected ${identity}`);
50
- }
51
- evidence.push({ file, before, after: fileEvidence(file), codesign: "strict-valid", identity, timestampPresent: /(?:Timestamp|Signed Time)[=:]/i.test(details) });
52
- if (!evidence.at(-1).timestampPresent) fail(`Developer ID timestamp is missing for ${file}`);
53
- }
48
+ let evidence;
49
+ try {
50
+ evidence = signMacosEntries(entries, {
51
+ identity,
52
+ signOne: (codesignArgs) => runMacCodesignCommands([codesignArgs])[0],
53
+ inspect: run,
54
+ });
55
+ } catch (error) { fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
54
56
 
55
57
  const absoluteReceipt = path.resolve(receiptPath);
56
58
  mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
@@ -63,10 +65,6 @@ function run(cmd, runArgs) {
63
65
  return output;
64
66
  }
65
67
 
66
- function escapeRegExp(value) {
67
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
- }
69
-
70
68
  function fail(message) {
71
69
  console.error(`right-sign-macos: ${message}`);
72
70
  process.exit(1);
package/sign-updater.mjs CHANGED
@@ -8,8 +8,10 @@ import { refuseManagedEnvSecrets, MANAGED_HOST_ENV_SECRET_REFUSED } from "./mana
8
8
  const args = process.argv.slice(2);
9
9
  const files = [];
10
10
  let dryRun = false;
11
+ let keyFile = process.env.RIGHT_UPDATER_KEY_FILE || "";
11
12
  for (let i = 0; i < args.length; i++) {
12
13
  if (args[i] === "--file") files.push(path.resolve(args[++i]));
14
+ else if (args[i] === "--key-file") keyFile = args[++i] ?? "";
13
15
  else if (args[i] === "--dry-run") dryRun = true;
14
16
  else fail(`unknown argument: ${args[i]}`);
15
17
  }
@@ -22,8 +24,9 @@ if (!dryRun) {
22
24
  }
23
25
  if (!dryRun && !env.TAURI_SIGNING_PRIVATE_KEY) {
24
26
  if (process.platform === "win32") {
25
- const key = path.join(env.APPDATA || "", "ViewRight", "updater.key");
26
- if (!existsSync(key)) fail(`shared updater key not found: ${key}`);
27
+ if (!keyFile) fail("updater key path required on win32: pass --key-file or set RIGHT_UPDATER_KEY_FILE (release config: targets.win.updater.keyFile), or set TAURI_SIGNING_PRIVATE_KEY");
28
+ const key = path.resolve(keyFile.replace(/%([^%]+)%/g, (m, name) => process.env[name] ?? m));
29
+ if (!existsSync(key)) fail(`updater key not found: ${key}`);
27
30
  env.TAURI_SIGNING_PRIVATE_KEY = readFileSync(key, "utf8");
28
31
  } else if (process.platform !== "darwin") {
29
32
  fail("TAURI_SIGNING_PRIVATE_KEY is required on this platform");
package/swift-mac-dmg.mjs CHANGED
@@ -4,6 +4,7 @@
4
4
  import path from "node:path";
5
5
  import { spawnSync } from "./background-process.mjs";
6
6
  import { buildDmg } from "./dmg-build.mjs";
7
+ import { readDmgSpec } from "./dmg-spec.mjs";
7
8
 
8
9
  const fail = (code, message) => Object.assign(new Error(message), { code });
9
10
  const DMG_HOOK = /dmg|hdiutil/i;
@@ -30,7 +31,9 @@ export function swiftMacDmgPlan({ target, projectType, appName = "app", resolve
30
31
  return {
31
32
  app: resolve(target.notarize.app),
32
33
  out: resolve(d.out ?? target.notarize.file),
33
- options: { volname: d.volname ?? appName, background: d.background && resolve(d.background), iconSize: d.iconSize, appPosition: d.appPosition,
34
+ // `spec` is an appdmg-style JSON file (dmg-spec.mjs); explicit options here win over it, and its title is the volume name.
35
+ fallbackVolname: appName,
36
+ options: { volname: d.volname ?? (d.spec ? undefined : appName), spec: d.spec && resolve(d.spec), background: d.background && resolve(d.background), iconSize: d.iconSize, appPosition: d.appPosition,
34
37
  applicationsPosition: d.applicationsPosition, window: d.window, volumeIcon: d.volumeIcon && resolve(d.volumeIcon) },
35
38
  };
36
39
  }
@@ -46,7 +49,9 @@ export async function buildStapledSwiftMacDmg(plan, { run = spawnSync, build = b
46
49
  if (probe.error || probe.status !== 0) throw fail("SWIFT_DMG_APP_NOT_STAPLED", `refusing to build the DMG: ${plan.app} is not stapled (xcrun stapler validate failed). Notarize and staple the app before packaging.`);
47
50
  remove(plan.out);
48
51
  log(`right-release: building macOS DMG from stapled app ${plan.app}`);
49
- const built = build({ app: plan.app, out: plan.out, ...definedOnly(plan.options) });
52
+ let options = definedOnly(plan.options);
53
+ if (options.spec && !options.volname) options = { ...options, volname: readDmgSpec(options.spec, { app: plan.app }).volname ?? plan.fallbackVolname };
54
+ const built = build({ app: plan.app, out: plan.out, ...options });
50
55
  if (signDmg) await signDmg(plan.out);
51
56
  return built;
52
57
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // right-release swift-package <windows-stage|msix|mac-app|doctor> --config <swift-package.json> [options]
2
+ // right-release swift-package <windows-stage|msix|mac-app> --config <swift-package.json> [options]
3
3
  //
4
4
  // One product config (schema 1) drives all three steps; paths resolve against the config file.
5
5
  // Build-only: nothing here uploads or publishes.
@@ -2,13 +2,8 @@
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import path from "node:path";
5
- import { spawnSync } from "./background-process.mjs";
6
- import { pathToFileURL } from "node:url";
7
5
  import { prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
8
- import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
9
- import { verifySealedRelease } from "./release-state.mjs";
10
- import { assertCleanSource } from "./source-gate.mjs";
11
- import { verifyOwnershipEvidence } from "./ownership-evidence.mjs";
6
+ import { authorizeGitHubUpload } from "./github-upload-gate.mjs";
12
7
  import { recordPublishedVersion } from "./artifact-storage.mjs";
13
8
  import { admitInstalledQa } from "./release-qa-audit.mjs";
14
9
 
@@ -32,33 +27,17 @@ for (let index = 0; index < args.length; index += 1) {
32
27
  else if (arg === "-h" || arg === "--help") usage(0);
33
28
  else fail(`unknown argument: ${arg}`);
34
29
  }
35
- if (tier !== "patch" && tier !== "update") fail("tier is required (patch|update)");
36
- if (!releaseId) fail("--release is required");
37
- if (platform !== "win" && platform !== "mac") fail("--platform must be win or mac");
38
30
 
39
- const repoRoot = git(process.cwd(), ["rev-parse", "--show-toplevel"]);
40
- assertPrimaryReleaseCheckout(repoRoot);
41
- const candidateCommit = git(repoRoot, ["rev-parse", "HEAD"]);
42
- assertCleanSource({
43
- status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]),
44
- commandId: "right-release upload",
45
- });
46
- const platformDir = platform === "win" ? "windows" : "mac";
47
- const sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", releaseId, platformDir));
48
- if (sealed.manifest.commit !== candidateCommit) fail("sealed release was built from a different commit than the current checkout");
49
-
50
- const configPath = path.resolve(configName);
51
- if (!existsSync(configPath)) fail(`release config is required: ${configName}`);
52
- const releaseConfig = (await import(`${pathToFileURL(configPath).href}?upload=${Date.now()}`)).default;
53
- const ownership = verifyOwnershipEvidence(sealed.manifest.ownershipEvidence, repoRoot, path.dirname(configPath));
54
- if (!ownership.ok) fail(`sealed ownership evidence rejected: ${ownership.reason}`);
55
- if (releaseConfig?.distribution?.provider !== "github-releases") {
56
- fail("release config distribution.provider must be github-releases");
31
+ // Tier, primary checkout, clean tree, sealed commit, ownership evidence, provider and repository are checked by
32
+ // the gate that `right-release github --release` also uses; nothing below runs until it passes.
33
+ let authority;
34
+ try {
35
+ authority = await authorizeGitHubUpload({ command: "right-release upload", tier, releaseId, platform, configName, repo });
36
+ } catch (error) {
37
+ fail(error.message);
57
38
  }
58
- const configuredRepo = releaseConfig?.distribution?.repository ?? "";
59
- if (!configuredRepo) fail("release config distribution.repository is required for github-releases");
60
- if (repo && repo !== configuredRepo) fail(`--repo must match configured distribution.repository (${configuredRepo})`);
61
- repo = configuredRepo;
39
+ const { repoRoot, platformDir, sealed, configPath } = authority;
40
+ repo = authority.repo;
62
41
 
63
42
  // Hardening step 4: installed QA is recorded, not enforced; a missing receipt warns and the outcome is written to state.
64
43
  if (!dryRun) {
@@ -87,12 +66,6 @@ if (!dryRun) writeJson(verifiedMarker, {
87
66
  if (!dryRun) recordPublishedVersion({ app: sealed.manifest.app, version: sealed.manifest.version, platform });
88
67
  console.log(`right-release upload: ${dryRun ? "dry-run " : ""}${result.status} ${releaseId} tier=${tier} repo=${repo}`);
89
68
 
90
- function git(cwd, runArgs) {
91
- const result = spawnSync("git", runArgs, { cwd, encoding: "utf8", windowsHide: true });
92
- if (result.status !== 0) fail(`git ${runArgs.join(" ")} failed: ${result.stderr}`);
93
- return result.stdout.trim();
94
- }
95
-
96
69
  function writeJson(file, value) {
97
70
  mkdirSync(path.dirname(file), { recursive: true });
98
71
  writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);