@rightkit/release 0.2.32 → 0.2.34

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/build-release.mjs CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  import path from "node:path";
21
21
  import { spawn, spawnSync } from "node:child_process";
22
22
  import { fileURLToPath, pathToFileURL } from "node:url";
23
- import { cacheFingerprint, releaseEnvironment, runBuildStateMachine } from "./release-state.mjs";
23
+ import { cacheFingerprint, commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine } from "./release-state.mjs";
24
24
 
25
25
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
26
26
  const WORKER = path.join(TOOL_ROOT, "release.mjs");
@@ -44,7 +44,7 @@ const invocationRoot = path.dirname(path.resolve(configName));
44
44
  const repoRoot = git(invocationRoot, ["rev-parse", "--show-toplevel"]);
45
45
  const relativeConfig = path.relative(repoRoot, path.resolve(configName)).replaceAll("\\", "/");
46
46
  if (relativeConfig.startsWith("../")) fail("release config must live inside the Git repository");
47
- const vaultRoot = path.join(repoRoot, ".right-release");
47
+ const vaultRoot = resolveReleaseLayout({ repoRoot, configPath: path.resolve(configName) }).vaultRoot;
48
48
  mkdirSync(path.join(vaultRoot, "locks"), { recursive: true });
49
49
  const lock = acquireLock(path.join(vaultRoot, "locks", `${platform}.lock.json`));
50
50
  let child = null;
@@ -71,6 +71,8 @@ try {
71
71
  }
72
72
 
73
73
  const worktreeConfigPath = path.join(worktree, relativeConfig);
74
+ const appRoot = path.dirname(worktreeConfigPath);
75
+ const layout = resolveReleaseLayout({ repoRoot, configPath: path.resolve(configName) });
74
76
  const config = (await import(`${pathToFileURL(worktreeConfigPath).href}?commit=${commit}`)).default;
75
77
  if (!config?.app || !config?.version) fail("release config must expose app and version");
76
78
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(config.app) || !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(config.version)) {
@@ -78,7 +80,7 @@ try {
78
80
  }
79
81
  const target = config.targets?.[platform];
80
82
  if (!target?.package) fail(`${config.app} has no ${platform} package command`);
81
- const requiredInputs = inputPaths(worktree, worktreeConfigPath, target);
83
+ const requiredInputs = inputPaths(appRoot, worktreeConfigPath, target);
82
84
  const toolVersions = collectToolVersions(config.packageManager ?? "pnpm");
83
85
  const inputHashes = hashInputs(requiredInputs);
84
86
  const cargoLock = requiredInputs.find((file) => /Cargo\.lock$/i.test(file));
@@ -92,6 +94,8 @@ try {
92
94
  const env = {
93
95
  ...process.env,
94
96
  ...releaseEnvironment({ root: vaultRoot, platform, cacheKey, kind: "release" }),
97
+ RIGHT_RELEASE_REPO_ROOT: layout.repoRoot,
98
+ RIGHT_RELEASE_APP_ROOT: layout.appRoot,
95
99
  };
96
100
  if (!commandExists("sccache")) delete env.RUSTC_WRAPPER;
97
101
  const releaseId = `${config.app}-${config.version}-${shortCommit}`;
@@ -123,7 +127,7 @@ try {
123
127
  assertDiskSpace(repoRoot, target.preflight?.minFreeGb ?? 20);
124
128
  assertExecutables(["git", config.packageManager ?? "pnpm", "cargo", "rustc", ...(target.preflight?.executables ?? [])]);
125
129
  for (const name of target.preflight?.env ?? []) if (!process.env[name]) fail(`missing required environment variable: ${name}`);
126
- for (const command of target.preflight?.commands ?? []) runChecked(command.cmd, command.args ?? [], path.resolve(worktree, command.cwd ?? "."), env);
130
+ for (const command of target.preflight?.commands ?? []) runChecked(command.cmd, command.args ?? [], path.resolve(appRoot, command.cwd ?? "."), env);
127
131
  mkdirSync(buildRoot, { recursive: true });
128
132
  mkdirSync(stateRoot, { recursive: true });
129
133
  writeJson(path.join(buildRoot, "release-inputs.lock.json"), lockPayload);
@@ -132,16 +136,16 @@ try {
132
136
  },
133
137
  prepare: async () => {
134
138
  const cacheTarget = env.CARGO_TARGET_DIR;
135
- const targetLink = path.join(worktree, "src-tauri", "target");
139
+ const targetLink = path.join(appRoot, "src-tauri", "target");
136
140
  mkdirSync(cacheTarget, { recursive: true });
137
141
  if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
138
142
  else if (realpathSync(targetLink) !== realpathSync(cacheTarget)) {
139
143
  fail(`worktree target exists and is not the release cache link: ${targetLink}`);
140
144
  }
141
- await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], worktree, env, cacheTarget);
145
+ await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget);
142
146
  },
143
147
  build: async () => {
144
- await runProgress(process.execPath, [WORKER, "--config", worktreeConfigPath, "--platform", platform, "--no-upload"], worktree, env, env.CARGO_TARGET_DIR);
148
+ await runProgress(process.execPath, [WORKER, "--config", worktreeConfigPath, "--platform", platform, "--no-upload"], appRoot, env, env.CARGO_TARGET_DIR);
145
149
  checkpoint(stateRoot, "build_complete");
146
150
  checkpoint(stateRoot, "signed");
147
151
  checkpoint(stateRoot, "hardened");
@@ -158,14 +162,14 @@ try {
158
162
  lock.release();
159
163
  }
160
164
 
161
- function inputPaths(worktree, configPath, target) {
165
+ function inputPaths(appRoot, configPath, target) {
162
166
  const candidates = [
163
167
  configPath,
164
- path.join(worktree, "package.json"),
165
- path.join(worktree, "pnpm-lock.yaml"),
166
- path.join(worktree, "src-tauri", "Cargo.toml"),
167
- path.join(worktree, "src-tauri", "Cargo.lock"),
168
- ...(target.preflight?.files ?? []).map((file) => path.resolve(worktree, expandEnv(file))),
168
+ path.join(appRoot, "package.json"),
169
+ path.join(appRoot, "pnpm-lock.yaml"),
170
+ path.join(appRoot, "src-tauri", "Cargo.toml"),
171
+ path.join(appRoot, "src-tauri", "Cargo.lock"),
172
+ ...(target.preflight?.files ?? []).map((file) => path.resolve(appRoot, expandEnv(file))),
169
173
  ];
170
174
  const required = [...new Set(candidates)];
171
175
  for (const file of required) if (!existsSync(file)) fail(`missing required release input: ${file}`);
@@ -338,9 +342,11 @@ function commandExists(name) {
338
342
  }
339
343
 
340
344
  function commandOutput(cmd, runArgs) {
341
- const result = spawnSync(cmd, runArgs, { encoding: "utf8", windowsHide: true });
342
- if (result.status !== 0) fail(`required command failed: ${cmd} ${runArgs.join(" ")}`);
343
- return result.stdout.trim();
345
+ try {
346
+ return commandOutputPortable(cmd, runArgs);
347
+ } catch (error) {
348
+ fail(`required command failed: ${error.message}`);
349
+ }
344
350
  }
345
351
 
346
352
  function git(cwd, runArgs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
5
5
  "type": "module",
6
6
  "bin": {
package/release-state.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import path from "node:path";
4
5
 
@@ -11,6 +12,38 @@ export function cacheFingerprint({ cargoLockSha256, rustc, target, features = []
11
12
  });
12
13
  return createHash("sha256").update(payload).digest("hex").slice(0, 16);
13
14
  }
15
+
16
+ export function resolveReleaseLayout({ repoRoot, configPath }) {
17
+ const resolvedRepoRoot = path.resolve(repoRoot);
18
+ const appRoot = path.dirname(path.resolve(configPath));
19
+ return {
20
+ repoRoot: resolvedRepoRoot,
21
+ appRoot,
22
+ vaultRoot: path.join(resolvedRepoRoot, ".right-release"),
23
+ targetLink: path.join(appRoot, "src-tauri", "target"),
24
+ };
25
+ }
26
+
27
+ export function commandOutputPortable(cmd, args, { cwd, env = process.env } = {}) {
28
+ const windows = process.platform === "win32";
29
+ if (windows && ![cmd, ...args].every((value) => /^[A-Za-z0-9._:/@+=-]+$/.test(value))) {
30
+ throw new Error("unsafe Windows command token");
31
+ }
32
+ const executable = windows ? (env.ComSpec || env.COMSPEC || "cmd.exe") : cmd;
33
+ const executableArgs = windows ? ["/d", "/s", "/c", [cmd, ...args].join(" ")] : args;
34
+ const result = spawnSync(executable, executableArgs, {
35
+ cwd,
36
+ env,
37
+ encoding: "utf8",
38
+ windowsHide: true,
39
+ });
40
+ if (result.status !== 0) {
41
+ const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
42
+ throw new Error(`${cmd} ${args.join(" ")} failed: ${detail}`);
43
+ }
44
+ return result.stdout.trim();
45
+ }
46
+
14
47
  export function releaseEnvironment({ root, platform, cacheKey, kind = "release" }) {
15
48
  if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
16
49
  const targetKind = kind === "release" ? "cargo-target" : "test-target";
@@ -7,12 +7,28 @@ import test from "node:test";
7
7
 
8
8
  import {
9
9
  cacheFingerprint,
10
+ commandOutputPortable,
11
+ resolveReleaseLayout,
10
12
  releaseEnvironment,
11
13
  runBuildStateMachine,
12
14
  runUploadStateMachine,
13
15
  verifySealedRelease,
14
16
  } from "./release-state.mjs";
15
17
 
18
+ test("portable command capture resolves Windows command shims", { skip: process.platform !== "win32" }, () => {
19
+ assert.match(commandOutputPortable("pnpm", ["--version"]), /^11\.12\.0$/);
20
+ });
21
+
22
+ test("nested app configs keep the vault at repo root and build from the app root", () => {
23
+ const layout = resolveReleaseLayout({
24
+ repoRoot: "D:/suite/heardright",
25
+ configPath: "D:/suite/heardright/tauri-app-next/right-release.config.mjs",
26
+ });
27
+ assert.equal(layout.vaultRoot, path.resolve("D:/suite/heardright/.right-release"));
28
+ assert.equal(layout.appRoot, path.resolve("D:/suite/heardright/tauri-app-next"));
29
+ assert.equal(layout.targetLink, path.resolve("D:/suite/heardright/tauri-app-next/src-tauri/target"));
30
+ });
31
+
16
32
  function sha256(value) {
17
33
  return createHash("sha256").update(value).digest("hex");
18
34
  }
@@ -317,7 +317,7 @@ test("Cargo version contract rejects staged versions that mismatch canonical man
317
317
  test("RightKit exposes one current version manifest", () => {
318
318
  assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
319
319
  assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
320
- assert.equal(versions.npm["@rightkit/release"], "0.2.32");
320
+ assert.equal(versions.npm["@rightkit/release"], "0.2.34");
321
321
  assert.equal(versions.npm["@rightkit/legal"], "0.2.0");
322
322
  assert.equal(versions.npm["@rightkit/license"], "0.1.5");
323
323
  assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
@@ -7,7 +7,7 @@
7
7
  "@rightkit/logs": "0.1.3",
8
8
  "@rightkit/platform-ui": "0.1.0",
9
9
  "@rightkit/qa": "0.1.0",
10
- "@rightkit/release": "0.2.32",
10
+ "@rightkit/release": "0.2.34",
11
11
  "@rightkit/tauri": "0.1.0",
12
12
  "@rightkit/updates": "0.2.3"
13
13
  },