@rightkit/release 0.2.138 → 0.2.140

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/release.mjs CHANGED
@@ -17,13 +17,17 @@ import {
17
17
  resolveNativeFinalizerConfig,
18
18
  validateNativeFinalizationOutput,
19
19
  } from "./native-release-finalization.mjs";
20
+ import { packageCommandOptional, resolveProjectType, targetUsesMsix } from "./project-type.mjs";
21
+ import { MSIX_SIGNING_CONTRACT, assertValidMsixTarget } from "./msix-config.mjs";
22
+ import { buildMsix } from "./msix-build.mjs";
23
+ import { msixBuildOptions } from "./msix-target.mjs";
20
24
  import { collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
21
25
  import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
22
26
  import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
23
27
  import { terminateProcessTree } from "./heavy-command.mjs";
24
28
  import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
25
29
  import { signingReceiptPath } from "./signing-receipts.mjs";
26
- import { validateNotarizationReceipt } from "./macos-notarization.mjs";
30
+ import { notarizeAppBundle, validateNotarizationReceipt } from "./macos-notarization.mjs";
27
31
  import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
28
32
  import { runConfiguredChecks } from "./release-checks.mjs";
29
33
  import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
@@ -118,7 +122,10 @@ const workdir = path.resolve(root, config.workdir ?? ".");
118
122
  const nativeLayout = resolveNativeCargoLayout({ appRoot: root, config });
119
123
  const nativeAssembly = config.nativeAssembly;
120
124
  const nativeFinalizer = resolveNativeFinalizerConfig(nativeAssembly);
121
- await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
125
+ // A SwiftPM app without a package.json has no Node package contract to validate.
126
+ if (!(resolveProjectType({ appRoot: root, config, target: config.targets?.[opts.platform] }) === "swiftpm" && !existsSync(path.join(root, "package.json")))) {
127
+ await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
128
+ }
122
129
  validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
123
130
  const target = config.targets?.[opts.platform];
124
131
  if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
@@ -131,26 +138,32 @@ if (target.signed !== true) {
131
138
  if (opts.platform === "win") {
132
139
  const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
133
140
  const isPortable = target.signingContract === WINDOWS_PORTABLE_SIGNING_CONTRACT;
134
- if (!isNsis && !isPortable) fail(`${config.app ?? "app"} win must declare signingContract: ${WINDOWS_NSIS_SIGNING_CONTRACT} or ${WINDOWS_PORTABLE_SIGNING_CONTRACT}`);
141
+ const isMsix = target.signingContract === MSIX_SIGNING_CONTRACT;
142
+ if (!isNsis && !isPortable && !isMsix) fail(`${config.app ?? "app"} win must declare signingContract: ${WINDOWS_NSIS_SIGNING_CONTRACT}, ${WINDOWS_PORTABLE_SIGNING_CONTRACT} or ${MSIX_SIGNING_CONTRACT}`);
143
+ if (isMsix) {
144
+ try { assertValidMsixTarget(target, config.app ?? "app"); } catch (error) { fail(error.message); }
145
+ } else {
135
146
  if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} win must declare prePackage for the raw EXE build`);
136
147
  if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} win must declare at least one sign.prePackageFiles raw EXE`);
137
148
  if (isNsis && target.sign.prePackageFiles.length !== 1) fail(`${config.app ?? "app"} win NSIS must declare exactly one sign.prePackageFiles raw EXE`);
138
149
  if (isNsis && !target.sign?.files?.length) fail(`${config.app ?? "app"} win NSIS must declare sign.files for installer signing`);
139
150
  if (isNsis && target.sign.prePackageFiles.some((file) => target.sign.files.includes(file))) fail(`${config.app ?? "app"} win raw EXE and installer signing files must be distinct`);
140
151
  if (isPortable && target.sign?.files?.length) fail(`${config.app ?? "app"} win portable release must sign every executable through sign.prePackageFiles and omit sign.files`);
152
+ }
141
153
  }
142
154
  if (opts.platform === "mac") {
143
155
  if (target.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT) fail(`${config.app ?? "app"} mac must declare signingContract: ${MACOS_PORTABLE_SIGNING_CONTRACT}`);
144
156
  if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
145
157
  if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
146
158
  if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
159
+ if (target.notarize.appBeforePackage && !target.notarize.app) fail(`${config.app ?? "app"} mac notarize.appBeforePackage requires notarize.app`);
147
160
  }
148
161
  if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
149
162
  /**
150
163
  * Assemble preflight inputs from the app's own files. Kept here (not in
151
164
  * preflight.mjs) so the check module stays free of release.mjs's config shape.
152
165
  */
153
- function releasePreflight({ config, configPath, root, repoRoot, platform, nativeLayout: suppliedNativeLayout = nativeLayout }) {
166
+ function releasePreflight({ config, configPath, root, repoRoot, platform, nativeLayout: suppliedNativeLayout = nativeLayout, projectTarget = config.targets?.[platform] }) {
154
167
  let version;
155
168
  try {
156
169
  version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
@@ -173,6 +186,8 @@ function releasePreflight({ config, configPath, root, repoRoot, platform, native
173
186
  cargoLockPaths,
174
187
  nativeLayout: suppliedNativeLayout,
175
188
  targetLink: suppliedNativeLayout?.targetLink,
189
+ projectType: resolveProjectType({ appRoot: root, config, target: projectTarget }),
190
+ msix: targetUsesMsix(projectTarget),
176
191
  });
177
192
  }
178
193
 
@@ -234,7 +249,7 @@ if (opts.doctor) {
234
249
  const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
235
250
  const mode = "package";
236
251
  const command = target.package;
237
- if (!command) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
252
+ if (!command && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
238
253
  if (opts.upload && !target.publish && !target.upload) {
239
254
  fail(`${config.app ?? "app"} has no ${opts.platform} upload config`);
240
255
  }
@@ -255,7 +270,17 @@ await runConfiguredChecks({
255
270
 
256
271
  let nativeFinalization = null;
257
272
  let bundleMarkerReceipts = [];
258
- if (opts.platform === "win") {
273
+ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT) {
274
+ // MSIX order: prePackage builds + stages the payload; buildMsix signs inner PE files (DLLs, EXEs),
275
+ // packs the exact signed bytes, signs the package through sign-windows, verifies signer == Publisher,
276
+ // writes the App Installer route and the seal record. Never a second signer.
277
+ await runCommand(target.prePackage, root);
278
+ const options = msixBuildOptions({ root, target, app: config.app, dryRun: opts.dryRun, receipts: {
279
+ pe: windowsReceiptPath(root, "raw-exe"), msix: windowsReceiptPath(root, "installer"),
280
+ dir: path.dirname(windowsReceiptPath(root, "raw-exe")) } });
281
+ const built = buildMsix(options, { log: (event) => console.log(`right-release: ${JSON.stringify(event)}`) });
282
+ console.log(`right-release: msix ${built.dryRun ? "dry-run" : "built"} ${options.out}`);
283
+ } else if (opts.platform === "win") {
259
284
  const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
260
285
  await runCommand(target.prePackage, root);
261
286
  for (const file of rawFiles) await mustExist(file, `missing raw EXE signing artifact: ${file}`);
@@ -286,7 +311,11 @@ if (opts.platform === "win") {
286
311
  // Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
287
312
  const signingFiles = target.sign.prePackageFiles.map((p) => resolveReleaseArtifactPath(root, p));
288
313
  for (const file of signingFiles) await mustExist(file, `missing macOS Developer ID signing artifact: ${file}`);
289
- if (runMacPhase("sign")) await signMacos(signingFiles, root);
314
+ if (runMacPhase("sign")) {
315
+ await signMacos(signingFiles, root);
316
+ // Opt-in (notarize.appBeforePackage): staple the .app itself before the package step copies it into the DMG.
317
+ if (target.notarize.appBeforePackage) await notarizeAppBeforePackage(root);
318
+ }
290
319
  } else if (nativeAssembly) {
291
320
  if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
292
321
  if (nativeFinalizer) {
@@ -300,7 +329,7 @@ if (opts.platform === "win") {
300
329
  }
301
330
  }
302
331
 
303
- if (opts.platform !== "mac" || runMacPhase("package")) await runCommand(command, root);
332
+ if (command && (opts.platform !== "mac" || runMacPhase("package"))) await runCommand(command, root);
304
333
 
305
334
  for (const rel of target.artifacts ?? []) {
306
335
  await mustExist(resolveReleaseArtifactPath(root, rel), `missing release artifact: ${rel}`);
@@ -373,7 +402,7 @@ releaseLock.release();
373
402
 
374
403
  function usage(code, message) {
375
404
  if (message) console.error(`right-release: ${message}\n`);
376
- console.error("usage: node release.mjs --tier patch|update [--config right-release.config.mjs] [--platform mac|win] [--upload|--no-upload] [--install] [--dry-run] [--doctor]");
405
+ console.error("usage: right-release build --platform mac|win [--config right-release.config.mjs] (build is tier-neutral; tier applies only to: right-release upload --release <id> --platform mac|win --tier patch|update)");
377
406
  process.exit(code);
378
407
  }
379
408
 
@@ -846,7 +875,8 @@ async function notarizeMacos(archive, root) {
846
875
  if (opts.dryRun) args.push("--dry-run");
847
876
  args.push("--receipt", receipt);
848
877
  if (target.notarize.profile) args.push("--profile", target.notarize.profile);
849
- if (target.notarize.app) args.push("--app", resolveReleaseArtifactPath(root, target.notarize.app));
878
+ // With appBeforePackage the app already carries its own stapled ticket inside the packaged DMG.
879
+ if (target.notarize.app && !target.notarize.appBeforePackage) args.push("--app", resolveReleaseArtifactPath(root, target.notarize.app));
850
880
  args.push(archive);
851
881
  if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
852
882
  console.log("right-release: notarizing macOS archive");
@@ -854,6 +884,21 @@ async function notarizeMacos(archive, root) {
854
884
  if (!opts.dryRun) verifyMacosNotarizationReceipt(receipt, archive);
855
885
  }
856
886
 
887
+ async function notarizeAppBeforePackage(root) {
888
+ const app = resolveReleaseArtifactPath(root, target.notarize.app);
889
+ const receipt = macosReceiptPath(root, "app-notarization");
890
+ const zip = `${app}.zip`;
891
+ console.log("right-release: notarizing and stapling macOS app before packaging");
892
+ if (opts.dryRun) { console.log(`dry-run: notarize app ${app} (zip ${zip}) receipt ${receipt}`); return; }
893
+ await mustExist(app, `missing macOS app bundle for notarize.app: ${app}`);
894
+ if (existsSync(receipt)) unlinkSync(receipt);
895
+ try {
896
+ notarizeAppBundle({ app, zip, receiptPath: receipt, profile: target.notarize.profile });
897
+ validateNotarizationReceipt({ receiptPath: receipt, archive: zip });
898
+ } catch (error) { fail(`app notarization failed: ${error.message}`); }
899
+ finally { rmSync(zip, { force: true }); }
900
+ }
901
+
857
902
  function macosReceiptPath(root, phase) {
858
903
  return signingReceiptPath({ appRoot: root, target, platform: "mac", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
859
904
  }
@@ -16,7 +16,7 @@
16
16
  "@rightkit/app-shell": "0.3.0",
17
17
  "@rightkit/ax": "0.2.4",
18
18
  "@rightkit/doc-render": "0.1.4",
19
- "@rightkit/git": "0.2.31",
19
+ "@rightkit/git": "0.2.32",
20
20
  "@rightkit/hooks": "0.1.2",
21
21
  "@rightkit/i18n": "0.1.0",
22
22
  "@rightkit/legal": "0.3.1",
@@ -29,7 +29,7 @@
29
29
  "@rightkit/platform-ui": "0.2.0",
30
30
  "@rightkit/qa": "0.4.0",
31
31
  "@rightkit/qa-chrome": "0.1.3",
32
- "@rightkit/release": "0.2.138",
32
+ "@rightkit/release": "0.2.140",
33
33
  "@rightkit/settings-ui": "0.3.0",
34
34
  "@rightkit/shell": "0.1.0",
35
35
  "@rightkit/tauri": "0.2.0",
@@ -72,7 +72,8 @@
72
72
  "0.2.27",
73
73
  "0.2.28",
74
74
  "0.2.29",
75
- "0.2.30"
75
+ "0.2.30",
76
+ "0.2.31"
76
77
  ],
77
78
  "@rightkit/legal-ui": [
78
79
  "0.1.0",
@@ -168,7 +169,9 @@
168
169
  "0.2.134",
169
170
  "0.2.135",
170
171
  "0.2.136",
171
- "0.2.137"
172
+ "0.2.137",
173
+ "0.2.138",
174
+ "0.2.139"
172
175
  ],
173
176
  "@rightkit/qa": [
174
177
  "0.1.0",
@@ -275,11 +278,11 @@
275
278
  "rightkit-framed-sidecar": "0.1.0",
276
279
  "rightkit-fs": "0.3.2",
277
280
  "rightkit-fsindex": "0.1.0",
278
- "rightkit-http": "0.2.2",
281
+ "rightkit-http": "0.2.4",
279
282
  "rightkit-jobs": "0.2.6",
280
283
  "rightkit-license": "0.1.3",
281
284
  "rightkit-logs": "0.2.0",
282
- "rightkit-mcp": "0.5.1",
285
+ "rightkit-mcp": "0.5.2",
283
286
  "rightkit-media": "0.2.4",
284
287
  "rightkit-models": "0.2.7",
285
288
  "rightkit-oauth": "0.3.0",
@@ -332,11 +335,11 @@
332
335
  "rightkit-framed-sidecar": "0.1.0",
333
336
  "rightkit-fs": "0.3.2",
334
337
  "rightkit-fsindex": "0.1.0",
335
- "rightkit-http": "0.2.2",
338
+ "rightkit-http": "0.2.4",
336
339
  "rightkit-jobs": "0.2.6",
337
340
  "rightkit-license": "0.1.3",
338
341
  "rightkit-logs": "0.2.0",
339
- "rightkit-mcp": "0.5.1",
342
+ "rightkit-mcp": "0.5.2",
340
343
  "rightkit-media": "0.2.4",
341
344
  "rightkit-models": "0.2.7",
342
345
  "rightkit-oauth": "0.3.0",
@@ -367,7 +370,7 @@
367
370
  "@rightkit/app-shell": "0.3.0",
368
371
  "@rightkit/ax": "0.2.4",
369
372
  "@rightkit/doc-render": "0.1.4",
370
- "@rightkit/git": "0.2.31",
373
+ "@rightkit/git": "0.2.32",
371
374
  "@rightkit/hooks": "0.1.2",
372
375
  "@rightkit/i18n": "0.1.0",
373
376
  "@rightkit/legal": "0.3.1",
@@ -380,7 +383,7 @@
380
383
  "@rightkit/platform-ui": "0.2.0",
381
384
  "@rightkit/qa": "0.4.0",
382
385
  "@rightkit/qa-chrome": "0.1.3",
383
- "@rightkit/release": "0.2.138",
386
+ "@rightkit/release": "0.2.140",
384
387
  "@rightkit/settings-ui": "0.3.0",
385
388
  "@rightkit/shell": "0.1.0",
386
389
  "@rightkit/tauri": "0.2.0",
@@ -432,7 +435,8 @@
432
435
  "0.3.0",
433
436
  "0.3.1",
434
437
  "0.4.0",
435
- "0.5.0"
438
+ "0.5.0",
439
+ "0.5.1"
436
440
  ],
437
441
  "rightkit-media": [
438
442
  "0.2.2",
@@ -504,7 +508,9 @@
504
508
  "rightkit-http": [
505
509
  "0.1.0",
506
510
  "0.2.0",
507
- "0.2.1"
511
+ "0.2.1",
512
+ "0.2.2",
513
+ "0.2.3"
508
514
  ],
509
515
  "rightkit-fs": [
510
516
  "0.1.0",
package/source-gate.mjs CHANGED
@@ -10,8 +10,29 @@
10
10
  // real risk is shipping stale, dirty, or untested source. Distribution trust still
11
11
  // comes from OS code signing, notarization, and the Tauri updater key — untouched.
12
12
 
13
+ // right-release's own per-platform run lock (`.right-release/locks/*`, every platform) is
14
+ // written before the later clean-tree preflights run. Only untracked files directly in that lock directory are
15
+ // exempt, so repos that do not ignore `.right-release/` still pass, and nothing else in
16
+ // the vault (sealed output, receipts, state) or elsewhere is ever excused.
17
+ const OWN_LOCK_RECORD = /^\?\? (?:[^\0]*\/)?\.right-release\/locks\/[^\0/]+$/;
18
+
19
+ /** Remove right-release's own untracked lock-file records from NUL-separated porcelain v1 output. */
20
+ export function stripOwnLockRecords(status) {
21
+ const records = status.split("\0");
22
+ const kept = [];
23
+ for (let index = 0; index < records.length; index += 1) {
24
+ const record = records[index];
25
+ if (!record) continue;
26
+ if (OWN_LOCK_RECORD.test(record)) continue;
27
+ kept.push(record);
28
+ if (/^[RC]/.test(record) || /^.[RC]/.test(record)) kept.push(records[++index] ?? "");
29
+ }
30
+ return kept.length ? `${kept.join("\0")}\0` : "";
31
+ }
32
+
13
33
  export function assertCleanSource({ status, commandId = "release" }) {
14
34
  if (typeof status !== "string") throw new Error("release source status is required");
35
+ status = stripOwnLockRecords(status);
15
36
  if (status.length !== 0) {
16
37
  const files = status.split("\0").filter(Boolean).map((entry) => entry.slice(3)).slice(0, 10);
17
38
  throw new Error(`${commandId} requires a clean working tree; commit or stash first:\n${files.map((file) => `- ${file}`).join("\n")}`);
@@ -0,0 +1,43 @@
1
+ // Everything the sealed `build` path does differently for a SwiftPM target, as pure/stub-friendly
2
+ // functions so build-release.mjs only branches once. Rust-only steps (rustc/cargo probes, Cargo lock
3
+ // identity, broker cargo target, Cache V2 shared Rust cache, target bridge, CARGO/SCCACHE env) are skipped.
4
+
5
+ import path from "node:path";
6
+ import { resolveSwiftCacheIdentity } from "./cache-policy.mjs";
7
+
8
+ const RUST_ENV = ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "SCCACHE_CACHE_SIZE", "SCCACHE_IGNORE_SERVER_IO_ERROR", "RUSTFLAGS", "RIGHT_RELEASE_CACHE_OWNER"];
9
+
10
+ export function swiftArchitecture({ target = {}, platform, hostArch = process.arch }) {
11
+ const arch = target.msix?.identity?.architecture ?? target.architecture ?? (hostArch === "arm64" ? "arm64" : "x64");
12
+ return platform === "mac" ? (arch === "x64" ? "x86_64" : arch) : arch;
13
+ }
14
+
15
+ /** exec(cmd, args) -> stdout string. Throws when swift is missing. */
16
+ export function collectSwiftToolVersions({ packageManager, exec, nodeVersion = process.version }) {
17
+ const swift = exec("swift", ["--version"]).trim();
18
+ if (!swift) throw new Error("swift --version printed nothing");
19
+ return { node: nodeVersion, packageManager: packageManager ? `${packageManager} ${exec(packageManager, ["--version"]).trim()}` : null, swift, cargo: null, rustc: null, rustHost: null, sccache: null };
20
+ }
21
+
22
+ /**
23
+ * Plan for a swiftpm sealed build: identity, forced non-shared cache mode, no broker cargo target, no target
24
+ * bridge, a plain build/cache dir under the vault, and an environment with every Rust variable removed.
25
+ */
26
+ export function planSwiftBuild({ appRoot, vaultRoot, platform, target, toolVersions, profile = "release", hostArch }) {
27
+ const architecture = swiftArchitecture({ target, platform, hostArch });
28
+ const identity = resolveSwiftCacheIdentity({ swiftVersion: toolVersions.swift, packageResolvedPath: path.join(appRoot, "Package.resolved"), architecture, profile: target.profile ?? profile });
29
+ const buildDir = path.resolve(vaultRoot, "cache", "swift-build", platform, identity.fingerprint);
30
+ return {
31
+ identity, architecture, buildDir,
32
+ cacheMode: "legacy",
33
+ managedCargoTarget: null,
34
+ targetBridge: { run: (fn) => fn(), ensure() {} },
35
+ sanitizeEnv(env) {
36
+ const next = { ...env };
37
+ for (const name of RUST_ENV) delete next[name];
38
+ next.RIGHT_RELEASE_SWIFT_BUILD_DIR = buildDir;
39
+ return next;
40
+ },
41
+ watchDirs: [buildDir, path.join(appRoot, ".build")],
42
+ };
43
+ }