@rightkit/release 0.2.104 → 0.2.106

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/addon-command.mjs CHANGED
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { spawnSync } from "node:child_process";
6
+ import { spawnSync } from "./background-process.mjs";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import { addonManifestSha256, addonRoutes, canonicalAddonManifest, createAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
9
9
  import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
@@ -0,0 +1,33 @@
1
+ import childProcess from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ // Release tooling has no interactive console children. Keep this policy at
5
+ // every Node launch boundary, including fixtures & nested release commands.
6
+ function hiddenArguments(args, options) {
7
+ return Array.isArray(args)
8
+ ? [args, { ...options, windowsHide: true }]
9
+ : [[], { ...(args ?? options), windowsHide: true }];
10
+ }
11
+
12
+ export function spawn(command, args, options) {
13
+ return childProcess.spawn(command, ...hiddenArguments(args, options));
14
+ }
15
+
16
+ export function spawnSync(command, args, options) {
17
+ return childProcess.spawnSync(command, ...hiddenArguments(args, options));
18
+ }
19
+
20
+ export function execFileSync(file, args, options) {
21
+ return childProcess.execFileSync(file, ...hiddenArguments(args, options));
22
+ }
23
+
24
+ export function execFile(file, args, options, callback) {
25
+ if (typeof args === "function") { callback = args; args = undefined; }
26
+ if (typeof options === "function") { callback = options; options = undefined; }
27
+ return childProcess.execFile(file, ...hiddenArguments(args, options), callback);
28
+ }
29
+
30
+ // Node's custom promisifier returns { stdout, stderr } & exposes promise.child.
31
+ // Preserve that contract for release-token's existing promisify(execFile).
32
+ execFile[promisify.custom] = (file, args, options) =>
33
+ childProcess.execFile[promisify.custom](file, ...hiddenArguments(args, options));
package/build-release.mjs CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  } from "node:fs";
21
21
  import { tmpdir } from "node:os";
22
22
  import path from "node:path";
23
- import { spawn, spawnSync } from "node:child_process";
23
+ import { spawn, spawnSync } from "./background-process.mjs";
24
24
  import { fileURLToPath, pathToFileURL } from "node:url";
25
25
  import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
26
26
  import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
@@ -78,6 +78,7 @@ let configName = "right-release.config.mjs";
78
78
  let platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : process.platform;
79
79
  let dryRun = false;
80
80
  let skipChecks = false;
81
+ let resumeFrom = null;
81
82
 
82
83
  for (let i = 0; i < args.length; i += 1) {
83
84
  const arg = args[i];
@@ -85,11 +86,13 @@ for (let i = 0; i < args.length; i += 1) {
85
86
  else if (arg === "--platform") platform = args[++i];
86
87
  else if (arg === "--dry-run") dryRun = true;
87
88
  else if (arg === "--skip-checks") skipChecks = true;
89
+ else if (arg === "--resume-from") resumeFrom = args[++i];
88
90
  else if (arg === "--tier" || arg.startsWith("--tier=")) fail("build is tier-neutral; select patch or update only during upload");
89
91
  else if (arg === "-h" || arg === "--help") usage(0);
90
92
  else fail(`unknown argument: ${arg}`);
91
93
  }
92
94
  if (platform !== "win" && platform !== "mac") fail("--platform must be win or mac");
95
+ if (resumeFrom && !["prepackage", "sign", "package", "post-sign", "harden"].includes(resumeFrom)) fail("--resume-from must be prepackage, sign, package, post-sign, or harden");
93
96
 
94
97
  const configPath = path.resolve(configName);
95
98
  const invocationRoot = path.dirname(configPath);
@@ -137,10 +140,15 @@ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
137
140
  try {
138
141
  throwIfInterrupted();
139
142
  const commit = git(repoRoot, ["rev-parse", "HEAD"]);
140
- assertCleanSource({
141
- status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]),
142
- commandId: "right-release build",
143
- });
143
+ // A phase-resume never reads unrelated source. Its later build-input gate
144
+ // remains strict, but requiring a globally clean mono-workspace here would
145
+ // turn an already-built, signed installer into a needless full rebuild.
146
+ if (!resumeFrom) {
147
+ assertCleanSource({
148
+ status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]),
149
+ commandId: "right-release build",
150
+ });
151
+ }
144
152
  mkdirSync(path.join(vaultRoot, "locks"), { recursive: true });
145
153
  lock = acquireLock(path.join(vaultRoot, "locks", `${platform}.lock.json`));
146
154
  const shortCommit = commit.slice(0, 8);
@@ -265,6 +273,7 @@ try {
265
273
  const platformDir = platform === "win" ? "windows" : "mac";
266
274
  const buildRoot = managedCargoTarget ? null : path.join(vaultRoot, "build", config.app, config.version, shortCommit, platformDir);
267
275
  const stateRoot = path.join(vaultRoot, "state", releaseId, platformDir);
276
+ env.RIGHT_RELEASE_STATE_ROOT = stateRoot;
268
277
  const lockPayload = {
269
278
  schema: 1,
270
279
  releaseId,
@@ -339,7 +348,7 @@ try {
339
348
  throwIfInterrupted();
340
349
  await runProgress(
341
350
  process.execPath,
342
- [WORKER, "--config", configPath, "--platform", platform, "--no-upload", ...(skipChecks ? ["--skip-checks"] : [])],
351
+ [WORKER, "--config", configPath, "--platform", platform, "--no-upload", "--resume", ...(resumeFrom ? ["--resume-from", resumeFrom] : []), ...(skipChecks ? ["--skip-checks"] : [])],
343
352
  appRoot,
344
353
  env,
345
354
  [managedCargoTarget ?? env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
@@ -350,6 +359,21 @@ try {
350
359
  checkpoint(stateRoot, "signed");
351
360
  checkpoint(stateRoot, "hardened");
352
361
  },
362
+ // Product-specific checks that must run after the signed/hardened bytes
363
+ // exist but before they become immutable. This is intentionally a small
364
+ // hook: Right Release still owns signing, hardening, and sealing; apps
365
+ // may only inspect their candidate and emit a pass/fail result.
366
+ preSeal: async ({ sealedDir }) => {
367
+ if (!target.preSeal) return;
368
+ await runConfiguredReleaseCheck(target.preSeal, {
369
+ root: appRoot,
370
+ env: {
371
+ RIGHT_RELEASE_SEALED_DIR: sealedDir,
372
+ RIGHT_RELEASE_RELEASE_ID: releaseId,
373
+ RIGHT_RELEASE_PLATFORM: platform,
374
+ },
375
+ });
376
+ },
353
377
  seal: async ({ sealedDir }) => {
354
378
  sealRelease({ configRoot: appRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity });
355
379
  verifySealedRelease(sealedDir);
@@ -671,6 +695,23 @@ function runChecked(cmd, runArgs, cwd, env = process.env) {
671
695
  if (result.status !== 0) fail(`${cmd} exited ${result.status}`);
672
696
  }
673
697
 
698
+ function runConfiguredReleaseCheck(command, { root, env }) {
699
+ if (!command) return;
700
+ if (typeof command.cmd !== "string" || !command.cmd.trim()) {
701
+ fail("release preSeal check must declare a command");
702
+ }
703
+ const args = Array.isArray(command.args) ? command.args.map(String) : [];
704
+ const cwd = path.resolve(root, command.cwd ?? ".");
705
+ const result = spawnSync(command.cmd, args, {
706
+ cwd,
707
+ env: { ...process.env, ...env },
708
+ stdio: "inherit",
709
+ windowsHide: true,
710
+ shell: false,
711
+ });
712
+ if (result.status !== 0) fail(`release preSeal check failed: ${command.cmd} ${args.join(" ")}`);
713
+ }
714
+
674
715
  function killTree(pid) {
675
716
  terminateProcessTree(pid);
676
717
  }
package/cache-command.mjs CHANGED
@@ -25,12 +25,12 @@ import { brokerManagedHost } from "./cargo-contract.mjs";
25
25
  const BROKER_MANAGED_MESSAGE = "broker-managed host: the build broker owns CARGO_TARGET_DIR; Cache V2 target checks skipped";
26
26
  const BROKER_GATED_COMMANDS = new Set(["status", "prune", "migrate"]);
27
27
 
28
- export async function runCacheCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, stderr = console.error } = {}) {
28
+ export async function runCacheCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, stderr = console.error, isBrokerHost = brokerManagedHost } = {}) {
29
29
  const [command, ...rest] = args;
30
30
  const json = rest.includes("--json");
31
31
  const platform = platformFrom(env);
32
32
  const cacheRoot = resolveSharedCacheRoot({ platform, env });
33
- if (BROKER_GATED_COMMANDS.has(command) && brokerManagedHost(env)) {
33
+ if (BROKER_GATED_COMMANDS.has(command) && isBrokerHost(env)) {
34
34
  const result = { schema: 1, command, cacheRoot, brokerManaged: true, skipped: true, message: BROKER_MANAGED_MESSAGE };
35
35
  if (json) stdout(JSON.stringify(result));
36
36
  else stdout(BROKER_MANAGED_MESSAGE);
package/cache-policy.mjs CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  import os from "node:os";
18
18
  import path from "node:path";
19
19
  import { createHash } from "node:crypto";
20
- import { spawnSync } from "node:child_process";
20
+ import { spawnSync } from "./background-process.mjs";
21
21
 
22
22
  export const CACHE_SCHEMA = 1;
23
23
  export const DEFAULT_TARGET_MAX_BYTES = 12 * 1024 ** 3;