@rightkit/release 0.2.172 → 0.2.173

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.
@@ -0,0 +1,95 @@
1
+ // Declarative package targets (bogusyogi/rightkit#345). `targets.win.winPackage` and `targets.mac.macPackage` replace
2
+ // the per-app NSIS / DMG hook scripts. They expand to the SAME hook shape release.mjs already runs (prePackage,
3
+ // package, sign, notarize, dmg), so every existing gate still applies unchanged: QA-control scans, the bundle
4
+ // marker patch before signing, raw-EXE and installer Authenticode, the NSIS embedding verification, Developer ID
5
+ // signing of the .app and its nested code, app notarization before packing, the signed DMG and its notarization.
6
+ // The standalone recipes (win-package-recipe.mjs, mac-package-recipe.mjs) hold the same steps for direct callers.
7
+ import path from "node:path";
8
+ import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
9
+
10
+ export class PackageRecipeTargetError extends Error {
11
+ constructor(message) { super(message); this.name = "PackageRecipeTargetError"; this.code = "RK_PACKAGE_RECIPE_TARGET"; }
12
+ }
13
+
14
+ const fail = (message) => { throw new PackageRecipeTargetError(message); };
15
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ const rel = (appRoot, file) => path.relative(appRoot, file).split(path.sep).join("/");
17
+ // Tauri never signs or creates updater artifacts here: release.mjs signs, and sign-updater signs the signed bytes.
18
+ const withoutRecipe = (target) => {
19
+ if (!isObject(target) || !("recipe" in target)) return target;
20
+ const { recipe: _ignored, ...rest } = target;
21
+ return rest;
22
+ };
23
+ const QUIET_TAURI_ENV = { TAURI_SIGNING_PRIVATE_KEY: "", TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "" };
24
+
25
+ /** `targets.win.winPackage` → prePackage (`tauri build --no-bundle`) and package (`tauri bundle --bundles nsis`). */
26
+ export function expandWinPackageTarget(target, { appRoot, config = {} }) {
27
+ if (!target?.winPackage) return withoutRecipe(target);
28
+ if (target.prePackage !== undefined) fail("targets.win.winPackage replaces prePackage; remove the prePackage script");
29
+ if (target.winPackage.sidecars !== undefined) fail("targets.win.winPackage.sidecars is not supported in a right-release target: declare sidecars through externalBin or mediaSidecar, which release.mjs signs with the raw EXE");
30
+ let plan;
31
+ try { plan = validateWinPackage({ appRoot, config, target }); } catch (error) {
32
+ if (error instanceof WinPackageConfigError) fail(`targets.win.winPackage: ${(error.issues ?? [error.message]).join("; ")}`);
33
+ throw error;
34
+ }
35
+ if (target.signingContract !== undefined && target.signingContract !== "windows-raw-exe-authenticode-before-nsis-v1") fail(`targets.win.winPackage builds an NSIS installer; signingContract must be windows-raw-exe-authenticode-before-nsis-v1, got ${target.signingContract}`);
36
+ const cwd = rel(plan.appRoot, plan.tauriCwd) || ".";
37
+ const override = { bundle: { createUpdaterArtifacts: false, windows: { nsis: { installMode: plan.perMachine ? "perMachine" : "currentUser" } } } };
38
+ // `recipe` is set only here: a hand-written one is dropped so it can never select a recipe path.
39
+ const { winPackage, recipe: _ignoredWin, ...rest } = target;
40
+ return {
41
+ ...rest,
42
+ signingContract: "windows-raw-exe-authenticode-before-nsis-v1",
43
+ prePackage: { cmd: "pnpm", args: ["exec", "tauri", "build", "--no-bundle", "--target", plan.triple], cwd, env: QUIET_TAURI_ENV },
44
+ package: { cmd: "pnpm", args: ["exec", "tauri", "bundle", "--bundles", "nsis", "--target", plan.triple, "--config", JSON.stringify(override)], cwd, env: QUIET_TAURI_ENV },
45
+ sign: { ...(target.sign ?? {}), prePackageFiles: [rel(plan.appRoot, plan.rawExe)], files: [rel(plan.appRoot, plan.installerFile)] },
46
+ recipe: { kind: "winPackage", productName: plan.productName, updater: plan.updater },
47
+ };
48
+ }
49
+
50
+ const MAC_KEYS = new Set(["universal", "tauriCwd", "features"]);
51
+
52
+ /**
53
+ * `targets.mac.macPackage` → prePackage (`tauri build --bundles app`, universal by default), the .app as the
54
+ * Developer ID signing set, app notarization before packing, and right-release's own signed DMG from the stapled app.
55
+ * The app path and DMG path are `notarize.app` and `notarize.file`; DMG branding is `targets.mac.dmg`.
56
+ */
57
+ export function expandMacPackageTarget(target, { appRoot }) {
58
+ if (!target?.macPackage) return withoutRecipe(target);
59
+ const spec = target.macPackage;
60
+ if (!isObject(spec)) fail("targets.mac.macPackage must be an object");
61
+ const unknown = Object.keys(spec).filter((key) => !MAC_KEYS.has(key));
62
+ if (unknown.length) fail(`targets.mac.macPackage has unknown key(s): ${unknown.join(", ")} (allowed: ${[...MAC_KEYS].join(", ")})`);
63
+ if (target.prePackage !== undefined || target.package !== undefined) fail("targets.mac.macPackage replaces prePackage and package; remove both scripts");
64
+ if (typeof target.notarize?.app !== "string" || !target.notarize.app.endsWith(".app")) fail("targets.mac.macPackage needs notarize.app: the built .app path");
65
+ if (typeof target.notarize?.file !== "string" || !target.notarize.file.endsWith(".dmg")) fail("targets.mac.macPackage needs notarize.file: the .dmg path right-release builds");
66
+ if (target.dmg === false) fail("targets.mac.macPackage always builds a DMG; remove dmg: false");
67
+ if (spec.universal !== undefined && typeof spec.universal !== "boolean") fail("targets.mac.macPackage.universal must be a boolean");
68
+ if (spec.features !== undefined && (!Array.isArray(spec.features) || spec.features.some((f) => typeof f !== "string" || !/^[A-Za-z0-9_,:-]+$/.test(f)))) fail("targets.mac.macPackage.features must be Cargo feature names");
69
+ const cwd = spec.tauriCwd ?? ".";
70
+ if (typeof cwd !== "string" || path.isAbsolute(cwd) || cwd.split(/[\\/]/).includes("..")) fail("targets.mac.macPackage.tauriCwd must be a relative path inside the app");
71
+ // Tauri's universal-apple-darwin target builds both slices and lipos them itself.
72
+ const triple = spec.universal === false ? (process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin") : "universal-apple-darwin";
73
+ const override = { bundle: { createUpdaterArtifacts: false, macOS: { signingIdentity: null } } };
74
+ const args = ["exec", "tauri", "build", "--target", triple, "--bundles", "app", "--no-sign", "--config", JSON.stringify(override)];
75
+ if (spec.features?.length) args.push("--features", spec.features.join(","));
76
+ const prePackageFiles = target.sign?.prePackageFiles ?? [target.notarize.app];
77
+ if (!prePackageFiles.some((entry) => (typeof entry === "string" ? entry : entry?.file) === target.notarize.app)) fail("targets.mac.macPackage: sign.prePackageFiles must include notarize.app");
78
+ const { macPackage, recipe: _ignoredMac, ...rest } = target;
79
+ return {
80
+ ...rest,
81
+ prePackage: { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
82
+ sign: { ...(target.sign ?? {}), prePackageFiles },
83
+ notarize: { ...target.notarize, appBeforePackage: true },
84
+ dmg: isObject(target.dmg) ? target.dmg : {},
85
+ recipe: { kind: "macPackage", universal: triple === "universal-apple-darwin" },
86
+ };
87
+ }
88
+
89
+ /** Expand whichever declarative package key the target carries. */
90
+ export function expandPackageRecipeTarget(target, { platform, appRoot, config }) {
91
+ if (platform === "win") return expandWinPackageTarget(target, { appRoot, config });
92
+ if (platform === "mac") return expandMacPackageTarget(target, { appRoot, config });
93
+ if (target?.winPackage || target?.macPackage) fail(`winPackage / macPackage are not valid on ${platform}`);
94
+ return target;
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.172",
3
+ "version": "0.2.173",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/project-type.mjs CHANGED
@@ -40,4 +40,4 @@ export function targetUsesMsix(target) {
40
40
  }
41
41
 
42
42
  /** SwiftPM targets build and package inside prePackage (mac DMG) or the MSIX step; no separate `package` command is required. */
43
- export const packageCommandOptional = (target, projectType) => targetUsesMsix(target) || (projectType === "swiftpm" && Boolean(target?.prePackage?.cmd));
43
+ export const packageCommandOptional = (target, projectType) => targetUsesMsix(target) || Boolean(target?.winPackage || target?.macPackage) || (projectType === "swiftpm" && Boolean(target?.prePackage?.cmd));
package/release.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { expandPackageRecipeTarget } from "./package-recipe-targets.mjs";
2
3
  import { access, readFile, readdir } from "node:fs/promises";
3
4
  import { createHash } from "node:crypto";
4
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
@@ -189,7 +190,9 @@ validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.b
189
190
  assertCargoProfileContract(root, config.app ?? path.basename(root));
190
191
  // RightKit QA is mandatory for every app (also SwiftPM apps without a package.json): no QA lane refuses the release.
191
192
  assertQaAdoption({ appRoot: root, app: config.app });
192
- const target = config.targets?.[opts.platform];
193
+ let target = config.targets?.[opts.platform];
194
+ // Declarative winPackage / macPackage (#345) expand to the same hook shape every gate below already checks.
195
+ try { target = expandPackageRecipeTarget(target, { platform: opts.platform, appRoot: root, config }); } catch (error) { fail(`${config.app ?? "app"} ${opts.platform} ${error.message}`); }
193
196
  if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
194
197
  const legalContract = config.legal
195
198
  ? assertLegalReleaseContract(root, config.legal, config.app ?? path.basename(root), opts.platform)
package/rightkit-sync.mjs CHANGED
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  const packageRoot = path.dirname(fileURLToPath(import.meta.url));
11
11
  const rightkitRoot = path.resolve(packageRoot, "..", "..");
12
12
  const VERSIONS_FILE = path.join(packageRoot, "rightkit-versions.json");
13
- const USER_AGENT = "rightkit-sync/1 (https://github.com/Orthic-Labs/rightkit)";
13
+ const USER_AGENT = "rightkit-sync/1 (https://github.com/bogusyogi/rightkit)";
14
14
 
15
15
  const field = (toml, key) => toml.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"))?.[1] ?? null;
16
16
 
@@ -31,7 +31,7 @@
31
31
  "@rightkit/qa": "0.4.9",
32
32
  "@rightkit/qa-chrome": "0.1.8",
33
33
  "@rightkit/react-shell": "0.6.8",
34
- "@rightkit/release": "0.2.172",
34
+ "@rightkit/release": "0.2.173",
35
35
  "@rightkit/settings-ui": "0.5.0",
36
36
  "@rightkit/tauri": "0.2.1",
37
37
  "@rightkit/tauri-shell": "0.1.1",
@@ -209,7 +209,8 @@
209
209
  "0.2.162",
210
210
  "0.2.163",
211
211
  "0.2.164",
212
- "0.2.165"
212
+ "0.2.165",
213
+ "0.2.172"
213
214
  ],
214
215
  "@rightkit/qa": [
215
216
  "0.1.0",
@@ -537,10 +538,10 @@
537
538
  "@rightkit/outdated": "0.2.0",
538
539
  "@rightkit/pdf-ui": "0.1.8",
539
540
  "@rightkit/platform-ui": "0.2.8",
540
- "@rightkit/qa": "0.4.8",
541
+ "@rightkit/qa": "0.4.9",
541
542
  "@rightkit/qa-chrome": "0.1.8",
542
543
  "@rightkit/react-shell": "0.6.8",
543
- "@rightkit/release": "0.2.165",
544
+ "@rightkit/release": "0.2.172",
544
545
  "@rightkit/settings-ui": "0.5.0",
545
546
  "@rightkit/shell": "0.1.1",
546
547
  "@rightkit/tauri": "0.2.1",
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // `right-release stage-sidecar [--config right-release.config.mjs] [--target <triple>]` (bogusyogi/rightkit#345):
3
+ // stages the config's declarative `sidecars` into src-tauri/binaries with Tauri externalBin naming.
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { resolveTargetTriple, stageSidecars } from "./stage-sidecar.mjs";
7
+
8
+ export function parseStageSidecarArgs(argv) {
9
+ const opts = { config: "right-release.config.mjs", target: undefined, json: false };
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const arg = argv[i];
12
+ if (arg === "--config" || arg === "--target") {
13
+ const value = argv[i + 1];
14
+ if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
15
+ opts[arg.slice(2)] = value;
16
+ i += 1;
17
+ } else if (arg === "--json") opts.json = true;
18
+ else throw new Error(`unknown stage-sidecar argument: ${arg}`);
19
+ }
20
+ return opts;
21
+ }
22
+
23
+ async function main() {
24
+ const opts = parseStageSidecarArgs(process.argv.slice(2));
25
+ const configPath = path.resolve(opts.config);
26
+ const appRoot = path.dirname(configPath);
27
+ const loaded = await import(pathToFileURL(configPath).href);
28
+ const config = loaded.default ?? loaded.config ?? loaded;
29
+ if (config?.sidecars === undefined) throw new Error(`${opts.config} declares no sidecars`);
30
+ const target = resolveTargetTriple({ target: opts.target, cwd: appRoot });
31
+ const result = await stageSidecars({ appRoot, manifest: config.sidecars, target });
32
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
33
+ else {
34
+ for (const file of result.files ?? []) console.log(`right-release stage-sidecar: staged ${typeof file === "string" ? file : file.path ?? JSON.stringify(file)}`);
35
+ console.log(`right-release stage-sidecar: externalBin ${JSON.stringify(result.externalBinNames ?? [])}; receipt ${result.receiptPath}`);
36
+ }
37
+ }
38
+
39
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
40
+ main().catch((error) => { console.error(`right-release stage-sidecar: ${error.code ? `[${error.code}] ` : ""}${error.message}`); process.exitCode = 1; });
41
+ }