@rightkit/release 0.3.0 → 0.4.1

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 (83) hide show
  1. package/README.md +74 -10
  2. package/addon-command.mjs +3 -4
  3. package/adoption-evidence.mjs +650 -0
  4. package/android-play-upload.mjs +2 -1
  5. package/apple-profile-create.mjs +113 -10
  6. package/apple-profile-renew.mjs +11 -3
  7. package/artifact-storage.mjs +35 -2
  8. package/bin/darwin/rightkit-swap +0 -0
  9. package/binary-license-notices.mjs +161 -0
  10. package/build-release.mjs +138 -47
  11. package/cargo-license-notices.mjs +111 -0
  12. package/cli/right-release.mjs +21 -2
  13. package/codesign-environment.mjs +2 -2
  14. package/content-digest.mjs +209 -0
  15. package/development.mjs +17 -6
  16. package/direct-bootstrap.mjs +5 -6
  17. package/disk-command.mjs +1 -0
  18. package/disk-policy.json +6 -2
  19. package/disk-stable-root.mjs +13 -9
  20. package/github-updater-release.mjs +70 -7
  21. package/in-place-install.mjs +17 -14
  22. package/install-app-windows.mjs +24 -7
  23. package/install-app.mjs +21 -40
  24. package/ios-asc-credentials.mjs +163 -0
  25. package/ios-build-context.mjs +129 -0
  26. package/ios-build-upload.mjs +282 -0
  27. package/ios-credentials-command.mjs +70 -0
  28. package/ios-dev.mjs +248 -0
  29. package/ios-host.mjs +76 -0
  30. package/ios-lock.mjs +75 -0
  31. package/ios-package-recipe.mjs +707 -0
  32. package/ios-testflight.mjs +266 -0
  33. package/ios-upload.mjs +313 -0
  34. package/local-publish.mjs +222 -53
  35. package/managed-cargo.mjs +8 -0
  36. package/mirror-root-artifact.mjs +5 -1
  37. package/native-cargo-layout.mjs +47 -4
  38. package/ownership-command.mjs +2 -1
  39. package/ownership-dependency-allowlist.json +3 -1
  40. package/ownership-evidence.mjs +30 -18
  41. package/ownership-key-command.mjs +69 -2
  42. package/ownership-mandatory-set.json +117 -67
  43. package/ownership-mandatory-set.mjs +141 -21
  44. package/ownership-repositories.json +1 -1
  45. package/ownership-scope.mjs +94 -0
  46. package/ownership.mjs +49 -7
  47. package/package-recipe-targets.mjs +41 -1
  48. package/package.json +1 -1
  49. package/pipeline-admission.mjs +375 -0
  50. package/pipeline-config.mjs +100 -0
  51. package/pipeline-generators.json +4 -0
  52. package/pipeline-generators.mjs +137 -0
  53. package/pipeline-scan.mjs +285 -0
  54. package/pipeline-tools.mjs +171 -0
  55. package/preflight.mjs +14 -7
  56. package/production-mode.mjs +42 -0
  57. package/project-type.mjs +3 -1
  58. package/protected-credentials.mjs +28 -1
  59. package/publication-artifacts.mjs +30 -5
  60. package/publication-gate.mjs +94 -32
  61. package/publish-cargo.mjs +117 -25
  62. package/publish-swift.mjs +12 -3
  63. package/publish-update.mjs +15 -2
  64. package/qa-build-features.mjs +16 -13
  65. package/qa-contract.mjs +30 -2
  66. package/qa-control-scan.mjs +8 -0
  67. package/release-config.mjs +330 -0
  68. package/release-invocation.mjs +118 -8
  69. package/release-switch-registry.json +126 -27
  70. package/release-switch-scan.mjs +88 -21
  71. package/release.mjs +82 -20
  72. package/rightkit-ownership.json +1 -1
  73. package/rightkit-sync.mjs +8 -0
  74. package/rightkit-versions.json +428 -194
  75. package/runtime-budget-crates.mjs +14 -0
  76. package/runtime-budgets.mjs +248 -47
  77. package/sign-updater.mjs +56 -59
  78. package/source-gate.mjs +8 -1
  79. package/stage-sidecar.mjs +207 -40
  80. package/swap-helper-bundle.mjs +7 -3
  81. package/target-distribution.mjs +5 -0
  82. package/upload-release.mjs +7 -2
  83. package/bin/darwin/rightkit-swap.sign.json +0 -6
package/ios-host.mjs ADDED
@@ -0,0 +1,76 @@
1
+ // Host and storage checks shared by the iOS lane (ios-package-recipe.mjs, ios-upload.mjs, ios-dev.mjs).
2
+ //
3
+ // Everything that needs Xcode, a signing keychain or the login Keychain is macOS only. On Windows or Linux the lane allows
4
+ // config validation and --dry-run and nothing else. The signing session rule follows macos-signing-session.mjs: Xcode's
5
+ // automatic signing needs the unlocked login keychain of the GUI (Aqua) session, so an SSH or other non-Aqua session is
6
+ // refused unless the caller supplies a routing seam (`route`). The signing agent protocol has no xcodebuild request, so the
7
+ // production default is a clear refusal, not a silent fallback.
8
+ import path from "node:path";
9
+ import { spawnSync } from "./background-process.mjs";
10
+ import { detectMacSigningSession } from "./macos-signing-session.mjs";
11
+
12
+ export class IosHostError extends Error {
13
+ constructor(code, message) { super(message); this.name = "IosHostError"; this.code = code; }
14
+ }
15
+
16
+ export function assertIosHost({ operation, platform = process.platform }) {
17
+ if (platform !== "darwin") {
18
+ throw new IosHostError("IOS_HOST_UNSUPPORTED", `${operation} needs macOS with Xcode; on ${platform} only config validation and --dry-run are available`);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * macOS-only stages run in the GUI signing session. `detect` and `route` are injectable.
24
+ * Returns { routed, reason }. `route` (when given) is used for a non-Aqua session; without it that session is refused.
25
+ */
26
+ export function assertGuiSigningSession({ operation, env = process.env, run = spawnSync, detect = detectMacSigningSession, route } = {}) {
27
+ let session;
28
+ try { session = detect({ env, run }); } catch (error) {
29
+ throw new IosHostError("IOS_GUI_SESSION_UNKNOWN", `${operation}: cannot prove the GUI signing session (${error.message})`);
30
+ }
31
+ if (!session.guiRunner) return { routed: false, reason: session.reason };
32
+ if (typeof route === "function") return { routed: true, reason: session.reason, route };
33
+ throw new IosHostError("IOS_GUI_SESSION_REQUIRED", `${operation} needs the unlocked login keychain of the GUI (Aqua) session, but this is a ${session.reason}; run it from the logged-in macOS desktop session (the signing agent cannot run xcodebuild)`);
34
+ }
35
+
36
+ /**
37
+ * DerivedData and result-bundle paths. With RIGHTKIT_RUN_ROOT set they come from `rightkit storage xcode-args --run-root <root>`
38
+ * (a JSON argv list holding -derivedDataPath and -resultBundlePath), the same source CodeRight and HeardRight use. Without a
39
+ * managed run the fallback is a directory under the right-release state root, never an in-repo DerivedData.
40
+ */
41
+ export function resolveXcodeStorage({ env = process.env, run = spawnSync, cwd = process.cwd(), fallbackRoot }) {
42
+ const runRoot = typeof env.RIGHTKIT_RUN_ROOT === "string" ? env.RIGHTKIT_RUN_ROOT.trim() : "";
43
+ if (!runRoot) {
44
+ if (!fallbackRoot) throw new IosHostError("IOS_STORAGE_UNAVAILABLE", "no RIGHTKIT_RUN_ROOT and no fallback storage root; run via: rightkit storage run --app <app> -- right-release ...");
45
+ return { derivedData: path.join(fallbackRoot, "DerivedData"), resultBundle: undefined, source: "fallback" };
46
+ }
47
+ const result = run("rightkit", ["storage", "xcode-args", "--run-root", runRoot], { cwd, env, encoding: "utf8", windowsHide: true, shell: false, timeout: 30_000 });
48
+ if (result.error || result.status !== 0) throw new IosHostError("IOS_STORAGE_FAILED", `rightkit storage xcode-args failed (${result.status ?? "unavailable"})`);
49
+ let args;
50
+ try { args = JSON.parse(String(result.stdout ?? "")); } catch { args = null; }
51
+ if (!Array.isArray(args) || !args.length || args.some((arg) => typeof arg !== "string")) throw new IosHostError("IOS_STORAGE_FAILED", "rightkit storage xcode-args returned no argv list");
52
+ const value = (flag, required) => {
53
+ const index = args.indexOf(flag);
54
+ if (index < 0 || index + 1 >= args.length) {
55
+ if (required) throw new IosHostError("IOS_STORAGE_FAILED", `rightkit storage xcode-args is missing ${flag}`);
56
+ return undefined;
57
+ }
58
+ const found = args[index + 1];
59
+ if (!path.isAbsolute(found)) throw new IosHostError("IOS_STORAGE_FAILED", `rightkit storage xcode-args ${flag} is not an absolute path`);
60
+ return found;
61
+ };
62
+ return { derivedData: value("-derivedDataPath", true), resultBundle: value("-resultBundlePath", false), source: "rightkit-storage" };
63
+ }
64
+
65
+ /** Last lines of command output for an error message: bounded, with obvious key material masked. */
66
+ export function outputTail(result, lines = 40) {
67
+ const text = `${result?.stdout ?? ""}${result?.stderr ?? ""}`
68
+ .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[redacted key]")
69
+ .replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
70
+ // a bare JWT (eyJ...), and any long base64 or hex run (a key, a token, a digest) with no path separator in it
71
+ .replace(/eyJ[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]+){0,2}/g, "[redacted jwt]")
72
+ .replace(/(?<![A-Za-z0-9+/_-])[A-Za-z0-9+_-]{40,}={0,2}(?![A-Za-z0-9+/_-])/g, "[redacted]")
73
+ // standard base64 whose alphabet includes "/" (a run that does not start at a path root and has no dot or hyphen)
74
+ .replace(/(?<![A-Za-z0-9+/_.:~-])(?![/.])[A-Za-z0-9+/]{64,}={0,2}(?![A-Za-z0-9+/_.-])/g, "[redacted]");
75
+ return text.split(/\r?\n/).filter(Boolean).slice(-lines).join("\n");
76
+ }
package/ios-lock.mjs ADDED
@@ -0,0 +1,75 @@
1
+ // A single-holder lock file for the iOS upload, the same mechanism as the build lane's run lock (build-release.mjs acquireLock):
2
+ // the file is created exclusively with the holder's pid, and a lock whose holder is no longer alive is replaced. Two
3
+ // `upload --apply` runs of one release can therefore never interleave. The file lives in `.right-release/locks/`, which the source gate
4
+ // already exempts from the clean-tree check.
5
+ //
6
+ // Replacing a stale lock is race-free: the stale file is first RENAMED to a unique tombstone (rename is atomic, so exactly one process
7
+ // wins; the others see ENOENT and just retry the exclusive create), and only the winner unlinks the tombstone. A lock that was created
8
+ // but not yet written (empty or unparsable) is held until it is older than a short grace period; it is never taken for stale at once.
9
+ // If the file renamed away turns out not to be the stale one that was judged (another process replaced it in between), it is put back.
10
+ import * as nodeFs from "node:fs";
11
+ import path from "node:path";
12
+
13
+ export const LOCK_GRACE_MS = 5_000;
14
+
15
+ export class IosLockError extends Error {
16
+ constructor(code, message) { super(message); this.name = "IosLockError"; this.code = code; }
17
+ }
18
+
19
+ const processAlive = (pid) => {
20
+ if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
21
+ try { process.kill(Number(pid), 0); return true; } catch (error) { return error.code === "EPERM"; }
22
+ };
23
+
24
+ export function acquireFileLock(file, label, { fs = nodeFs, isAlive = processAlive, pid = process.pid, now = () => Date.now(), graceMs = LOCK_GRACE_MS } = {}) {
25
+ fs.mkdirSync(path.dirname(file), { recursive: true });
26
+ const locked = (detail) => new IosLockError("IOS_UPLOAD_LOCKED", `${label} is already running (${detail}, lock ${file})`);
27
+ for (let attempt = 0; attempt < 5; attempt += 1) {
28
+ try {
29
+ const fd = fs.openSync(file, "wx");
30
+ try { fs.writeFileSync(fd, `${JSON.stringify({ pid, label, startedAt: new Date().toISOString() })}\n`); } finally { fs.closeSync(fd); }
31
+ let released = false;
32
+ const release = () => {
33
+ if (released) return;
34
+ released = true;
35
+ process.removeListener("exit", release);
36
+ try { fs.unlinkSync(file); } catch { /* already released */ }
37
+ };
38
+ process.on("exit", release);
39
+ return { release };
40
+ } catch (error) {
41
+ if (error.code !== "EEXIST") throw error;
42
+ }
43
+ let raw;
44
+ try { raw = fs.readFileSync(file, "utf8"); } catch (error) {
45
+ if (error.code === "ENOENT") continue; // released meanwhile: create it
46
+ throw error;
47
+ }
48
+ let holder;
49
+ try { holder = JSON.parse(raw); } catch { holder = undefined; }
50
+ if (holder && typeof holder === "object") {
51
+ if (isAlive(holder.pid) && holder.pid !== pid) throw locked(`pid ${holder.pid}`);
52
+ } else {
53
+ // created but not written yet (or damaged): held until it is older than the grace period
54
+ let age;
55
+ try { age = now() - fs.statSync(file).mtimeMs; } catch (error) { if (error.code === "ENOENT") continue; throw error; }
56
+ if (age < graceMs) throw locked("its lock file is still being written");
57
+ }
58
+ // stale: take it away atomically; only one process can win this rename
59
+ const tombstone = `${file}.stale-${pid}-${now()}-${Math.random().toString(16).slice(2)}`;
60
+ try { fs.renameSync(file, tombstone); } catch (error) {
61
+ if (error.code === "ENOENT") continue; // somebody else won; retry the exclusive create
62
+ throw error;
63
+ }
64
+ let taken;
65
+ try { taken = fs.readFileSync(tombstone, "utf8"); } catch { taken = raw; }
66
+ if (taken !== raw) {
67
+ // not the stale file we judged: a live lock was replaced under us. Put it back if the name is still free.
68
+ try { fs.linkSync(tombstone, file); } catch { /* the name is taken again; the holder will see its lock gone only if it checks */ }
69
+ try { fs.unlinkSync(tombstone); } catch { /* ignore */ }
70
+ throw locked("another process took the lock while it was being replaced");
71
+ }
72
+ try { fs.unlinkSync(tombstone); } catch { /* ignore */ }
73
+ }
74
+ throw new IosLockError("IOS_UPLOAD_LOCKED", `${label}: cannot take the lock ${file}`);
75
+ }