@rightkit/release 0.6.5 → 0.6.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.
Files changed (42) hide show
  1. package/README.md +79 -7
  2. package/android-play-upload.mjs +4 -4
  3. package/app-check.mjs +474 -17
  4. package/apple-profile-create.mjs +4 -3
  5. package/apple-profile-renew.mjs +5 -4
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/rightkit-gate.mjs +431 -44
  8. package/binary-license-notices.mjs +2 -1
  9. package/build-release.mjs +7 -6
  10. package/cache-command.mjs +4 -3
  11. package/cargo-license-notices.mjs +2 -1
  12. package/cargo-manifest-text.mjs +25 -0
  13. package/cargo-profile-contract.mjs +26 -13
  14. package/cli/right-release.mjs +8 -1
  15. package/command-spec.mjs +4 -5
  16. package/deps.mjs +5 -4
  17. package/deterministic-scans.mjs +280 -32
  18. package/development.mjs +5 -5
  19. package/github-release.mjs +5 -3
  20. package/github-upload-gate.mjs +6 -6
  21. package/init-command.mjs +134 -8
  22. package/ios-upload.mjs +4 -4
  23. package/mirror-root-artifact.mjs +3 -4
  24. package/npm-license-notices.mjs +2 -1
  25. package/outdated-command.mjs +91 -0
  26. package/package.json +37 -1
  27. package/pipeline-admission.mjs +12 -0
  28. package/publish-cargo.mjs +215 -3
  29. package/publish-update.mjs +5 -3
  30. package/release-config.mjs +36 -5
  31. package/release-dependencies-command.mjs +4 -3
  32. package/release-qa-audit.mjs +12 -10
  33. package/release-qa-command.mjs +42 -7
  34. package/release-qa-hardening.mjs +10 -3
  35. package/release-qa-policy.mjs +214 -47
  36. package/release.mjs +4 -4
  37. package/rightkit-registry-resolver.mjs +5 -2
  38. package/rightkit-versions.json +11 -8
  39. package/stage-sidecar-command.mjs +5 -5
  40. package/test-command.mjs +3 -2
  41. package/upload-release.mjs +3 -2
  42. package/web-build.mjs +2 -1
@@ -18,6 +18,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
18
  import path from "node:path";
19
19
  import { pathToFileURL } from "node:url";
20
20
  import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
21
+ import { defaultReleaseConfigFile } from "./release-config.mjs";
21
22
  import { escapeHtml } from "./npm-license-notices.mjs";
22
23
 
23
24
  const fail = (message) => { throw new CommandSpecError(message); };
@@ -123,7 +124,7 @@ export function generateBinaryNotices({ appRoot, config, flags = {}, check = fal
123
124
  }
124
125
 
125
126
  export function parseBinaryNoticeArgs(argv) {
126
- const opts = { config: "right-release.config.mjs", output: undefined, productName: undefined, receipt: undefined, check: false };
127
+ const opts = { config: defaultReleaseConfigFile(), output: undefined, productName: undefined, receipt: undefined, check: false };
127
128
  for (let i = 0; i < argv.length; i += 1) {
128
129
  const arg = argv[i];
129
130
  if (arg === "--config" || arg === "--output" || arg === "--product-name" || arg === "--receipt") {
package/build-release.mjs CHANGED
@@ -23,7 +23,8 @@ import { spawn, spawnSync } from "./background-process.mjs";
23
23
  import { RightKitRegistryError, SNAPSHOT_ENV, SNAPSHOT_FILE, assertManifestRegistryEvidence, loadAdmittedSnapshot, prepareRightKitResolution, registryManifestFields, rightKitLockInventory, writeAdmissionRecord, writeSnapshot } from "./rightkit-registry-resolver.mjs";
24
24
  import { formatAdmission, formatNotRun, runAdmission } from "./admission.mjs";
25
25
  import { sccacheRefusal } from "./admission-host.mjs";
26
- import { fileURLToPath, pathToFileURL } from "node:url";
26
+ import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
27
+ import { fileURLToPath } from "node:url";
27
28
  import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
28
29
  import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
29
30
  import { admitSourceQa } from "./release-qa-audit.mjs";
@@ -111,7 +112,7 @@ const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
111
112
  const WORKER = path.join(TOOL_ROOT, "release.mjs");
112
113
  const RELEASE_TOOL_VERSION = JSON.parse(readFileSync(path.join(TOOL_ROOT, "package.json"), "utf8")).version;
113
114
  const args = process.argv.slice(2);
114
- let configName = "right-release.config.mjs";
115
+ let configName = defaultReleaseConfigFile();
115
116
  let platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : process.platform;
116
117
  let dryRun = false;
117
118
  let skipChecks = false;
@@ -195,7 +196,7 @@ try {
195
196
  const shortCommit = commit.slice(0, 8);
196
197
  const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
197
198
  if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
198
- const config = (await import(`${pathToFileURL(configPath).href}?commit=${commit}`)).default;
199
+ const config = await importReleaseConfig(configPath, { query: `commit=${commit}` });
199
200
  if (!config?.app || !config?.version) fail("release config must expose app and version");
200
201
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(config.app) || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(config.version)) {
201
202
  fail("app and version must be safe release-id components");
@@ -566,7 +567,7 @@ async function admit() {
566
567
  commit = git(invocationRoot, ["rev-parse", "HEAD"]);
567
568
  const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
568
569
  if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
569
- try { config = (await import(`${pathToFileURL(configPath).href}?commit=${commit}`)).default; } catch (error) { fail(`release config could not be loaded (${configPath}): ${error instanceof Error ? error.message : String(error)}`); }
570
+ try { config = await importReleaseConfig(configPath, { query: `commit=${commit}` }); } catch (error) { fail(`release config could not be loaded (${configPath}): ${error instanceof Error ? error.message : String(error)}`); }
570
571
  } catch (error) {
571
572
  if (error?.reported) process.exit(1);
572
573
  throw error;
@@ -630,7 +631,7 @@ function sqlCipherFeatures(text) {
630
631
  }
631
632
 
632
633
  function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot, stateRoot, rightkitRegistryFields }) {
633
- // Every app's right-release.config.mjs names installer/updater artifacts
634
+ // Every app's release config names installer/updater artifacts
634
635
  // relative to its resolved native target. On a broker-managed host that
635
636
  // directory is never populated (the broker owns CARGO_TARGET_DIR and
636
637
  // targetBridge.ensure() deliberately leaves a real local directory alone,
@@ -1053,7 +1054,7 @@ function writeJson(file, value) {
1053
1054
  }
1054
1055
 
1055
1056
  function usage(code) {
1056
- console.log("usage: right-release build --platform win|mac|android|ios [--config right-release.config.mjs] [--dry-run]");
1057
+ console.log("usage: right-release build --platform win|mac|android|ios [--config right-release.config.json] [--dry-run]");
1057
1058
  process.exit(code);
1058
1059
  }
1059
1060
 
package/cache-command.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, realpathSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { fileURLToPath } from "node:url";
5
5
  import {
6
6
  applyCachePrune,
7
7
  inspectCache,
@@ -12,6 +12,7 @@ import {
12
12
  resolveSharedCacheIdentity,
13
13
  resolveSharedCacheRoot,
14
14
  } from "./cache-policy.mjs";
15
+ import { importReleaseConfig } from "./release-config.mjs";
15
16
  import { resolveReleaseLayout } from "./release-state.mjs";
16
17
  import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
17
18
  import { brokerManagedHost } from "./cargo-contract.mjs";
@@ -52,11 +53,11 @@ export async function runCacheCommand(args = process.argv.slice(2), { env = proc
52
53
  }
53
54
  if (command === "migrate") {
54
55
  const configPath = option(rest, "--config");
55
- if (!configPath) throw new Error("cache migrate requires --config <right-release.config.mjs>");
56
+ if (!configPath) throw new Error("cache migrate requires --config <right-release.config.json>");
56
57
  rejectUnknown(rest.filter((value) => value !== "--config" && value !== configPath), new Set(["--json", "--dry-run", "--apply"]));
57
58
  if (rest.includes("--dry-run") && rest.includes("--apply")) throw new Error("cache migrate accepts either --dry-run or --apply");
58
59
  const configFile = path.resolve(configPath);
59
- const config = (await import(pathToFileURL(configFile).href)).default;
60
+ const config = await importReleaseConfig(configFile);
60
61
  if (!config?.app) throw new Error("cache migrate config must expose app");
61
62
  const appRoot = path.dirname(configFile);
62
63
  const configuredTarget = config.targets?.[platform] ?? {};
@@ -15,6 +15,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
15
  import path from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
18
+ import { defaultReleaseConfigFile } from "./release-config.mjs";
18
19
  import { runCargoSync } from "./managed-cargo.mjs";
19
20
 
20
21
  const fail = (message) => { throw new CommandSpecError(message); };
@@ -73,7 +74,7 @@ export function generateCargoNotices({ appRoot, config, flags = {}, check = fals
73
74
  }
74
75
 
75
76
  export function parseCargoNoticeArgs(argv) {
76
- const opts = { config: "right-release.config.mjs", output: undefined, check: false };
77
+ const opts = { config: defaultReleaseConfigFile(), output: undefined, check: false };
77
78
  for (let i = 0; i < argv.length; i += 1) {
78
79
  const arg = argv[i];
79
80
  if (arg === "--config" || arg === "--output") {
@@ -0,0 +1,25 @@
1
+ // A line reading of the [package] table of a Cargo.toml, for the scans that must run without a TOML parser (the license scan and
2
+ // the secret scan of the repository gate). This module has no imports on purpose: publish-cargo.mjs is in the trusted runtime
3
+ // helper closure (scripts/ci/runtime-supplement.mjs), which may only follow static relative imports of plain modules.
4
+
5
+ /**
6
+ * `{ hasPackage, unpublished }` for the text of a Cargo.toml: whether it has a `[package]` table, and whether that table says
7
+ * `publish = false` or `publish = []`. A crate that can never be published ships its licence to no one, like a package.json with
8
+ * `"private": true`. An array-of-tables header (`[[bin]]`, `[[package.metadata.x]]`) starts a new table like any other, so a
9
+ * `publish` key under it is not the package's.
10
+ */
11
+ export function cargoManifestState(text) {
12
+ let table = "";
13
+ let hasPackage = false;
14
+ let unpublished = false;
15
+ for (const line of String(text).split(/\r?\n/)) {
16
+ const header = /^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/.exec(line);
17
+ if (header) {
18
+ table = header[1].trim();
19
+ if (table === "package") hasPackage = true;
20
+ continue;
21
+ }
22
+ if (table === "package" && /^\s*publish\s*=\s*(?:false\b|\[\s*\])/.test(line)) unpublished = true;
23
+ }
24
+ return { hasPackage, unpublished };
25
+ }
@@ -19,6 +19,7 @@ import path from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { parse } from "smol-toml";
21
21
  import { findCargoManifests, isExcludedScanDirectory } from "./cargo-contract.mjs";
22
+ import { defaultReleaseConfigFile, isJsonReleaseConfig } from "./release-config.mjs";
22
23
 
23
24
  export const CARGO_PROFILE_TEMPLATE = fileURLToPath(new URL("./templates/cargo-profiles.toml", import.meta.url));
24
25
  export const RETIRED_PROFILE = "release-iterate";
@@ -74,28 +75,40 @@ export function checkManifestProfiles(manifest, template, file) {
74
75
  // Non-shipping workspaces: fixed prefixes plus the app's declared `cargoProfileExempt` directories.
75
76
  const NON_SHIPPING_PREFIXES = ["qa", "benchmarks"];
76
77
  const CARGO_PROFILE_EXEMPT_KEY = "cargoProfileExempt";
77
- const RELEASE_CONFIG = "right-release.config.mjs";
78
78
  const STRING_LITERAL = /"([^"\\\n]*)"|'([^'\\\n]*)'/g;
79
79
 
80
- // release.mjs imports the app's release config as an ES module. This scanner is synchronous and has only the
81
- // app root, so it reads the literal `cargoProfileExempt` array from the same file: every entry must be a plain
82
- // string literal naming a directory inside the app. Anything else is a finding, never silently ignored.
80
+ // This scanner is synchronous and has only the app root, so it reads `cargoProfileExempt` from the release config file itself and
81
+ // never imports it. A JSON config (the one `defaultReleaseConfigFile` picks when it exists) is parsed as data: the array must hold
82
+ // plain strings. A legacy `.mjs` config, which release.mjs still imports as an ES module, is read as text: the literal array must
83
+ // hold plain string literals. Either way every entry names a directory inside the app, and anything else is a finding, never
84
+ // silently ignored. M24 command admission is not run here: this is a pure file reader.
83
85
  export function readCargoProfileExempt(root) {
84
- const file = path.join(root, RELEASE_CONFIG);
86
+ const configFile = defaultReleaseConfigFile(root);
87
+ const file = path.join(root, configFile);
85
88
  const findings = [];
86
89
  if (!existsSync(file)) return { paths: [], findings };
87
- const match = new RegExp(`\\b${CARGO_PROFILE_EXEMPT_KEY}\\s*:\\s*\\[([^\\]]*)\\]`).exec(readFileSync(file, "utf8"));
88
- if (!match) return { paths: [], findings };
89
- const body = match[1];
90
- if (body.replace(STRING_LITERAL, "").replace(/[,\s]/g, "") !== "") {
91
- findings.push({ rule: "invalid-exempt", file: RELEASE_CONFIG, message: `${CARGO_PROFILE_EXEMPT_KEY} must be an array of string literals` });
90
+ let entries;
91
+ if (isJsonReleaseConfig(configFile)) {
92
+ let declared;
93
+ try { declared = JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""))?.[CARGO_PROFILE_EXEMPT_KEY]; }
94
+ catch (error) { return { paths: [], findings: [{ rule: "invalid-exempt", file: configFile, message: `is not valid JSON, so ${CARGO_PROFILE_EXEMPT_KEY} cannot be read: ${error.message}` }] }; }
95
+ if (declared === undefined) return { paths: [], findings };
96
+ if (!Array.isArray(declared) || declared.some((entry) => typeof entry !== "string")) findings.push({ rule: "invalid-exempt", file: configFile, message: `${CARGO_PROFILE_EXEMPT_KEY} must be an array of strings` });
97
+ entries = Array.isArray(declared) ? declared.filter((entry) => typeof entry === "string") : [];
98
+ } else {
99
+ const match = new RegExp(`\\b${CARGO_PROFILE_EXEMPT_KEY}\\s*:\\s*\\[([^\\]]*)\\]`).exec(readFileSync(file, "utf8"));
100
+ if (!match) return { paths: [], findings };
101
+ const body = match[1];
102
+ if (body.replace(STRING_LITERAL, "").replace(/[,\s]/g, "") !== "") {
103
+ findings.push({ rule: "invalid-exempt", file: configFile, message: `${CARGO_PROFILE_EXEMPT_KEY} must be an array of string literals` });
104
+ }
105
+ entries = [...body.matchAll(STRING_LITERAL)].map(([, double, single]) => double ?? single);
92
106
  }
93
107
  const paths = [];
94
- for (const [, double, single] of body.matchAll(STRING_LITERAL)) {
95
- const entry = double ?? single;
108
+ for (const entry of entries) {
96
109
  const normalized = path.posix.normalize(entry.replace(/\\/g, "/")).replace(/\/+$/, "");
97
110
  if (normalized === "." || normalized === "" || path.posix.isAbsolute(normalized) || normalized.split("/").includes("..")) {
98
- findings.push({ rule: "invalid-exempt", file: RELEASE_CONFIG, message: `${CARGO_PROFILE_EXEMPT_KEY} entry ${JSON.stringify(entry)} must be a relative directory inside the app` });
111
+ findings.push({ rule: "invalid-exempt", file: configFile, message: `${CARGO_PROFILE_EXEMPT_KEY} entry ${JSON.stringify(entry)} must be a relative directory inside the app` });
99
112
  } else paths.push(normalized);
100
113
  }
101
114
  return { paths, findings };
@@ -13,6 +13,7 @@ const commands = new Map([
13
13
  ["publish", "upload-release.mjs"],
14
14
  ["upload", "upload-release.mjs"],
15
15
  ["deps", "deps.mjs"],
16
+ ["update", "outdated-command.mjs"],
16
17
  ["publish-update", "publish-update.mjs"],
17
18
  ["hardening", "hardeningscan.mjs"],
18
19
  ["harden", "hardeningscan.mjs"],
@@ -295,8 +296,12 @@ Commands:
295
296
  ios-credentials store --key-id <KEYID> --issuer <uuid> | check
296
297
  Put the App Store Connect API key (stdin), key id and issuer into the login Keychain once, or check them; never a file (macOS)
297
298
  qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa (and the journeys lane when the config sets qa.journeys) and write the sealed QA receipt. Refused inside a QA run. The receipt must match the platform and release id, come from an approved runner digest, and show every release-critical journey passed
298
- Admission is per app in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks build, upload and publish. Only adrian moves an app from refuse back to warn
299
+ Admission is per app, and per stage (source = build, installed = upload and publish), in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks that stage. Only adrian moves an app or stage from refuse back to warn, or approves a runner
299
300
  qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] List source/installed QA status per release; exit 1 if an uploaded build lacks passing QA
301
+ qa policy show | set --app <app> --mode warn|refuse --set-by <name> [--stage source|installed]
302
+ Show the QA admission policy, or set an app's mode (both stages, or one with --stage)
303
+ qa policy approve-runner --runner <file> --version <v> --platform mac|win --set-by adrian [--note <text>]
304
+ Approve a rightkit-qa runner build for one platform: the file must report "rightkit-qa <v>" from --version and <v> must be the published version
300
305
  publish-update --release <id> --platform mac|win --tier patch|update
301
306
  Upload sealed bytes to R2 & register RightApps
302
307
  release Compatibility alias for build
@@ -371,6 +376,8 @@ Commands:
371
376
  disk run self-check, check, dry-run sweep, move sweep, warn (the scheduled job)
372
377
  disk schedule install|uninstall|status Daily launchd job (Mac) / hidden Task Scheduler entry (Windows), bound to the stable per-user copy
373
378
  deps --check|--audit|--update Shared dependency lane
379
+ update check|apply|gate|land|discard|run|schedule [--repo <path>]
380
+ Run the pinned @rightkit/outdated updater (right-update) that moves stale pins; reads .rightkit/update.json
374
381
  hardening <artifact...> Run the Right Suite hardening scan
375
382
  lsclean <AppName.app> Clear macOS LaunchServices duplicates
376
383
  generate-dmg-background <options> Generate the branded multi-resolution DMG background
package/command-spec.mjs CHANGED
@@ -5,13 +5,12 @@
5
5
  // `cwd` and `node` are app-relative and may not leave the app. `nodeFlags` go before the script, `nodeOptions` are merged
6
6
  // (de-duplicated) into NODE_OPTIONS for the child. Unknown keys are refused so a typo cannot silently change a run.
7
7
  import path from "node:path";
8
- import { pathToFileURL } from "node:url";
8
+ import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
9
9
 
10
- /** Load an app's `right-release.config.mjs`; the app root is the config file's directory. */
11
- export async function loadAppConfig(configFile = "right-release.config.mjs") {
10
+ /** Load an app's release config (`right-release.config.json`, or the legacy `.mjs` while it is the only one); the app root is the config file's directory. */
11
+ export async function loadAppConfig(configFile = defaultReleaseConfigFile()) {
12
12
  const configPath = path.resolve(configFile);
13
- const loaded = await import(pathToFileURL(configPath).href);
14
- return { config: loaded.default ?? loaded.config ?? loaded, appRoot: path.dirname(configPath), configPath };
13
+ return { config: await importReleaseConfig(configPath, { lenient: true }), appRoot: path.dirname(configPath), configPath };
15
14
  }
16
15
 
17
16
  export class CommandSpecError extends Error {
package/deps.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
- import { pathToFileURL } from "node:url";
3
+ import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
4
4
  import { spawnSync } from "./background-process.mjs";
5
5
  import { existsSync } from "node:fs";
6
6
  import { cargoAuditWorkdirs, runCargoAudits } from "./deps-cargo.mjs";
7
7
 
8
8
  const args = process.argv.slice(2);
9
9
  const opts = {
10
- config: "right-release.config.mjs",
10
+ config: defaultReleaseConfigFile(),
11
11
  mode: "check",
12
12
  latest: false,
13
13
  };
@@ -25,7 +25,8 @@ for (let i = 0; i < args.length; i++) {
25
25
  }
26
26
 
27
27
  const configPath = path.resolve(opts.config);
28
- const config = (await import(pathToFileURL(configPath))).default;
28
+ let config;
29
+ try { config = await importReleaseConfig(configPath); } catch (error) { fail(`release config could not be loaded (${configPath}): ${error instanceof Error ? error.message : String(error)}`); }
29
30
  if (config?.schema !== 1) fail(`unsupported config schema: ${config?.schema ?? "<missing>"} (expected 1)`);
30
31
  if (config.packageManager !== "pnpm") fail(`deps tool expects pnpm, got ${config.packageManager}`);
31
32
 
@@ -69,7 +70,7 @@ if (failures.length > 0) {
69
70
 
70
71
  function usage(code, message) {
71
72
  if (message) console.error(`right-deps: ${message}\n`);
72
- console.error("usage: node deps.mjs [--config right-release.config.mjs] [--check|--audit|--update] [--latest]");
73
+ console.error("usage: node deps.mjs [--config right-release.config.json] [--check|--audit|--update] [--latest]");
73
74
  process.exit(code);
74
75
  }
75
76