@rightkit/release 0.2.139 → 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/README.md +6 -0
- package/committed-source.mjs +2 -1
- package/dmg-build.mjs +10 -3
- package/hdiutil-detach.mjs +42 -0
- package/install-app.mjs +98 -37
- package/macos-notarization.mjs +21 -1
- package/ownership.mjs +24 -0
- package/package.json +1 -1
- package/release.mjs +25 -4
- package/rightkit-versions.json +17 -12
- package/source-gate.mjs +21 -0
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id d
|
|
|
35
35
|
|
|
36
36
|
It mounts the image read-only and hidden (`-nobrowse`), verifies the app (`codesign --verify --deep --strict`, `spctl --assess`, `stapler validate`, bundle id, and Team ID against the installed copy; a Team ID change is refused) before touching the installed copy, quits the running instance with SIGTERM (15 s, then SIGKILL), moves the old bundle to `~/.Trash/<App> <timestamp>.app`, copies the new one with `ditto` (restoring the old bundle on failure), re-verifies, and relaunches only if it was running or `--relaunch` is given, using `open -g -j -b` (background, hidden, never activated). `--json` emits one lifecycle event per line (`start`, `verified`, `quit`, `replaced`, `relaunched`, `done`/`failed`). Win32 exits with "not implemented on win32". Exit codes: 0 ok, 1 failed, 2 usage, 3 unsupported platform.
|
|
37
37
|
|
|
38
|
+
Development builds (from `@rightkit/git` `devArtifact`, Developer ID-signed but not notarized) install with `--dev` and exactly one of `--dmg`, `--zip <path.zip>` (extracted with `ditto -x -k`), or `--app <path.app>`: `right-release install-app --dev --zip dev-mac.zip`. `--dev` still requires `codesign --verify --deep --strict`, a Team ID, no Team ID change, and a bundle-id match; it skips only `spctl` and `stapler`, removes `com.apple.quarantine` from the new bundle only, prints `DEV BUILD (not notarized)`, and sets `dev: true` on every JSON event.
|
|
39
|
+
|
|
38
40
|
## Entry points
|
|
39
41
|
|
|
40
42
|
Bin: `right-release` (`cli/right-release.mjs`). The package has no `exports` map. The CLI is the supported surface.
|
|
@@ -66,6 +68,10 @@ Swift (SwiftPM) apps are a first-class project type. `projectType: "swiftpm"` (c
|
|
|
66
68
|
|
|
67
69
|
macOS Swift apps use `right-release swift-package mac-app` (Info.plist from config, resources, frameworks, entitlements on the bundle only; nested code signed inside-out before the app).
|
|
68
70
|
|
|
71
|
+
Build a SwiftPM app with `right-release build --platform mac|win` (add `--config <path>` when the config is not `right-release.config.mjs`). Build is tier-neutral and never uploads, so it rejects `--tier` and takes no `--upload`/`--no-upload`; the old `release --tier patch --no-upload` form no longer works. Pick the tier only at upload: `right-release upload --release <sealed-id> --platform mac|win --tier patch|update`.
|
|
72
|
+
|
|
73
|
+
Set `targets.mac.notarize.appBeforePackage: true` (with `notarize.app` naming the `.app`) to notarize and staple the app itself right after signing and before the package step copies it into the DMG, so an app copied out of the DMG passes `stapler validate` offline. The DMG named by `notarize.file` is still notarized and stapled afterward; the app ticket receipt is `macos-app-notarization.json`. Without the flag, behavior is unchanged.
|
|
74
|
+
|
|
69
75
|
## Known issues
|
|
70
76
|
|
|
71
77
|
- `smol-toml` is pinned exactly. A consumer that adds another `smol-toml` gets a second copy (npm audit 2026-10-08, release finding).
|
package/committed-source.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripOwnLockRecords } from "./source-gate.mjs";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -193,7 +194,7 @@ export function assertCommittedCheckoutClean(root, { expectedCommit, expectedTre
|
|
|
193
194
|
const output = runGit(path.resolve(root), ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
|
|
194
195
|
encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env: gitEnv(),
|
|
195
196
|
});
|
|
196
|
-
if (output.length) throw new CommittedSourceIncompleteError("committed ownership: protected release checkout has tracked or untracked changes");
|
|
197
|
+
if (stripOwnLockRecords(Buffer.from(output).toString("utf8")).length) throw new CommittedSourceIncompleteError("committed ownership: protected release checkout has tracked or untracked changes");
|
|
197
198
|
return true;
|
|
198
199
|
}
|
|
199
200
|
|
package/dmg-build.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { backgroundAlias, dmgDSStore } from "./dmg-layout.mjs";
|
|
6
|
+
import { detachWithRetrySync } from "./hdiutil-detach.mjs";
|
|
6
7
|
import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
|
|
7
8
|
|
|
8
9
|
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
@@ -17,7 +18,7 @@ const pair = (value, label, positive = false) => {
|
|
|
17
18
|
export function buildDmg({ app, out, volname, background, iconSize = 128, appPosition = [158, 231],
|
|
18
19
|
applicationsPosition = [492, 231], window = [660, 400], volumeIcon } = {},
|
|
19
20
|
{ platform = process.platform, run = spawnSync, makeAlias = backgroundAlias,
|
|
20
|
-
log = (event) => console.error(JSON.stringify(event)) } = {}) {
|
|
21
|
+
log = (event) => console.error(JSON.stringify(event)), sleep } = {}) {
|
|
21
22
|
if (platform !== "darwin") throw fail("DMG_NATIVE_HOST_REQUIRED", "DMG creation requires macOS");
|
|
22
23
|
if (!app || !out || !volname) throw fail("DMG_INVALID_OPTIONS", "--app, --out & --volname are required");
|
|
23
24
|
app = path.resolve(app); out = path.resolve(out);
|
|
@@ -55,6 +56,12 @@ export function buildDmg({ app, out, volname, background, iconSize = 128, appPos
|
|
|
55
56
|
if (result.error || result.status !== 0) throw fail("DMG_COMMAND_FAILED", `${tool} ${args[0]} failed: ${result.error?.message ?? result.stderr ?? result.status}`);
|
|
56
57
|
return result;
|
|
57
58
|
};
|
|
59
|
+
// Detach flakes with "Resource busy": bounded retry with backoff, then -force as the last resort.
|
|
60
|
+
const detach = () => {
|
|
61
|
+
const outcome = detachWithRetrySync({ mount, log, ...(sleep ? { sleep } : {}),
|
|
62
|
+
detach: (args) => run("/usr/bin/hdiutil", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], shell: false, timeout: 300000 }) });
|
|
63
|
+
if (!outcome.ok) throw fail("DMG_COMMAND_FAILED", `hdiutil detach failed after ${outcome.attempts} tries and -force: ${outcome.result?.error?.message ?? outcome.result?.stderr ?? outcome.result?.status}`);
|
|
64
|
+
};
|
|
58
65
|
log({ event: "dmg-build-started", app, out, volname });
|
|
59
66
|
try {
|
|
60
67
|
mkdirSync(stage); mkdirSync(mount);
|
|
@@ -78,7 +85,7 @@ export function buildDmg({ app, out, volname, background, iconSize = 128, appPos
|
|
|
78
85
|
const alias = relative ? makeAlias({ mount, volname, relative }) : undefined;
|
|
79
86
|
writeFileSync(path.join(mount, ".DS_Store"), dmgDSStore({ appName: path.basename(app), iconSize,
|
|
80
87
|
appPosition, applicationsPosition, window, alias }));
|
|
81
|
-
|
|
88
|
+
detach(); attached = false;
|
|
82
89
|
command("/usr/bin/hdiutil", ["convert", rw, "-format", "UDZO", "-imagekey", "zlib-level=9", "-o", compressed]);
|
|
83
90
|
command("/usr/bin/hdiutil", ["verify", compressed]);
|
|
84
91
|
renameSync(compressed, out);
|
|
@@ -89,7 +96,7 @@ export function buildDmg({ app, out, volname, background, iconSize = 128, appPos
|
|
|
89
96
|
throw error;
|
|
90
97
|
} finally {
|
|
91
98
|
if (attached || attaching) {
|
|
92
|
-
try {
|
|
99
|
+
try { detach(); attached = false; attaching = false; }
|
|
93
100
|
catch (error) { log({ event: "dmg-cleanup-failed", mount, work, message: error.message }); }
|
|
94
101
|
}
|
|
95
102
|
// Never recurse through a mount that failed to detach.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Bounded detach for hdiutil mounts. Spotlight, Finder, and quick-look helpers can hold a freshly
|
|
2
|
+
// written volume for a moment, so a first `hdiutil detach` regularly fails with "Resource busy".
|
|
3
|
+
// Retry a fixed number of times with exponential backoff, then fall back to `-force` once.
|
|
4
|
+
|
|
5
|
+
export const DETACH_TRIES = 5;
|
|
6
|
+
export const DETACH_BACKOFF_MS = Object.freeze([500, 1000, 2000, 4000]);
|
|
7
|
+
|
|
8
|
+
const blockingSleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
9
|
+
const describe = (result) => String(result?.error?.message ?? result?.stderr ?? result?.status ?? "failed").trim();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Synchronous. `detach(args)` runs `hdiutil <args>` and returns `{ status, stderr?, error? }`.
|
|
13
|
+
* Returns `{ ok, attempts, forced }`; throws only through the caller when `ok` is false.
|
|
14
|
+
*/
|
|
15
|
+
export function detachWithRetrySync({ detach, mount, sleep = blockingSleep, log = () => {}, tries = DETACH_TRIES, backoffMs = DETACH_BACKOFF_MS }) {
|
|
16
|
+
let last;
|
|
17
|
+
for (let attempt = 1; attempt <= tries; attempt += 1) {
|
|
18
|
+
last = detach(["detach", mount]);
|
|
19
|
+
if (!last?.error && last?.status === 0) return { ok: true, attempts: attempt, forced: false, result: last };
|
|
20
|
+
const wait = backoffMs[Math.min(attempt - 1, backoffMs.length - 1)];
|
|
21
|
+
log({ event: "hdiutil-detach-retry", mount, attempt, of: tries, retryInMs: attempt < tries ? wait : 0, message: describe(last) });
|
|
22
|
+
if (attempt < tries) sleep(wait);
|
|
23
|
+
}
|
|
24
|
+
log({ event: "hdiutil-detach-force", mount, message: describe(last) });
|
|
25
|
+
const forced = detach(["detach", "-force", mount]);
|
|
26
|
+
return { ok: !forced?.error && forced?.status === 0, attempts: tries, forced: true, result: forced };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Async twin for promise-based runners: `detach(args)` resolves on success and rejects on failure. */
|
|
30
|
+
export async function detachWithRetry({ detach, mount, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), log = () => {}, tries = DETACH_TRIES, backoffMs = DETACH_BACKOFF_MS }) {
|
|
31
|
+
let last;
|
|
32
|
+
for (let attempt = 1; attempt <= tries; attempt += 1) {
|
|
33
|
+
try { await detach(["detach", mount]); return { ok: true, attempts: attempt, forced: false }; }
|
|
34
|
+
catch (error) { last = error; }
|
|
35
|
+
const wait = backoffMs[Math.min(attempt - 1, backoffMs.length - 1)];
|
|
36
|
+
log({ event: "hdiutil-detach-retry", mount, attempt, of: tries, retryInMs: attempt < tries ? wait : 0, message: String(last?.message ?? last) });
|
|
37
|
+
if (attempt < tries) await sleep(wait);
|
|
38
|
+
}
|
|
39
|
+
log({ event: "hdiutil-detach-force", mount, message: String(last?.message ?? last) });
|
|
40
|
+
try { await detach(["detach", "-force", mount]); return { ok: true, attempts: tries, forced: true }; }
|
|
41
|
+
catch (error) { return { ok: false, attempts: tries, forced: true, error }; }
|
|
42
|
+
}
|
package/install-app.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Sanctioned, non-intrusive local app install from a signed DMG (macOS)
|
|
2
|
+
// Sanctioned, non-intrusive local app install from a signed DMG (macOS); --dev also accepts a
|
|
3
|
+
// Developer ID-signed, not-notarized zip or .app.
|
|
3
4
|
// Never uses a shell, osascript, AppleEvents, or foreground activation.
|
|
4
5
|
import { execFile as nodeExecFile } from "./background-process.mjs";
|
|
6
|
+
import { detachWithRetry } from "./hdiutil-detach.mjs";
|
|
5
7
|
import fs from "node:fs";
|
|
6
8
|
import os from "node:os";
|
|
7
9
|
import path from "node:path";
|
|
@@ -17,11 +19,11 @@ export class InstallAppError extends Error {
|
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
const VALUE_FLAGS = new Set(["--dmg", "--app-name", "--bundle-id", "--dest"]);
|
|
21
|
-
const BOOL_FLAGS = new Set(["--relaunch", "--no-relaunch", "--json"]);
|
|
22
|
+
const VALUE_FLAGS = new Set(["--dmg", "--zip", "--app", "--app-name", "--bundle-id", "--dest"]);
|
|
23
|
+
const BOOL_FLAGS = new Set(["--relaunch", "--no-relaunch", "--json", "--dev"]);
|
|
22
24
|
|
|
23
25
|
export function parseArgs(argv) {
|
|
24
|
-
const opts = { dmg: null, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false };
|
|
26
|
+
const opts = { dmg: null, zip: null, app: null, dev: false, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false };
|
|
25
27
|
for (let i = 0; i < argv.length; i += 1) {
|
|
26
28
|
const flag = argv[i];
|
|
27
29
|
if (VALUE_FLAGS.has(flag)) {
|
|
@@ -29,25 +31,35 @@ export function parseArgs(argv) {
|
|
|
29
31
|
if (value === undefined || value.startsWith("--")) throw new InstallAppError("usage", `${flag} requires a value`);
|
|
30
32
|
i += 1;
|
|
31
33
|
if (flag === "--dmg") opts.dmg = value;
|
|
34
|
+
else if (flag === "--zip") opts.zip = value;
|
|
35
|
+
else if (flag === "--app") opts.app = value;
|
|
32
36
|
else if (flag === "--app-name") opts.appName = value;
|
|
33
37
|
else if (flag === "--bundle-id") opts.bundleId = value;
|
|
34
38
|
else opts.dest = value;
|
|
35
39
|
} else if (BOOL_FLAGS.has(flag)) {
|
|
36
40
|
if (flag === "--json") opts.json = true;
|
|
41
|
+
else if (flag === "--dev") opts.dev = true;
|
|
37
42
|
else opts.relaunch = flag === "--relaunch";
|
|
38
43
|
} else {
|
|
39
44
|
throw new InstallAppError("usage", `unknown argument: ${flag}`);
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
if (argv.includes("--relaunch") && argv.includes("--no-relaunch")) throw new InstallAppError("usage", "--relaunch and --no-relaunch are mutually exclusive");
|
|
43
|
-
|
|
44
|
-
if (
|
|
48
|
+
const inputs = [opts.dmg, opts.zip, opts.app].filter((v) => v !== null).length;
|
|
49
|
+
if (inputs === 0) throw new InstallAppError("usage", opts.dev ? "one of --dmg <path.dmg>, --zip <path.zip>, or --app <path.app> is required" : "--dmg <path.dmg> is required");
|
|
50
|
+
if (inputs > 1) throw new InstallAppError("usage", "--dmg, --zip, and --app are mutually exclusive");
|
|
51
|
+
if ((opts.zip !== null || opts.app !== null) && !opts.dev) throw new InstallAppError("usage", "--zip and --app require --dev (only DMGs are installable as release builds)");
|
|
52
|
+
if (opts.dmg !== null && !opts.dmg.toLowerCase().endsWith(".dmg")) throw new InstallAppError("usage", "--dmg must name a .dmg file");
|
|
53
|
+
if (opts.zip !== null && !opts.zip.toLowerCase().endsWith(".zip")) throw new InstallAppError("usage", "--zip must name a .zip file");
|
|
54
|
+
if (opts.app !== null && !opts.app.replace(/\/+$/, "").toLowerCase().endsWith(".app")) throw new InstallAppError("usage", "--app must name a .app bundle");
|
|
45
55
|
if (opts.appName !== null && (!/^[^/\0]+\.app$/.test(opts.appName) || opts.appName === ".app" || opts.appName.includes(".."))) {
|
|
46
56
|
throw new InstallAppError("usage", "--app-name must be a bare bundle name ending in .app");
|
|
47
57
|
}
|
|
48
58
|
if (opts.bundleId !== null && !/^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(opts.bundleId)) throw new InstallAppError("usage", "--bundle-id is not a valid bundle identifier");
|
|
49
59
|
if (!path.isAbsolute(opts.dest)) opts.dest = path.resolve(opts.dest);
|
|
50
|
-
opts.dmg = path.resolve(opts.dmg);
|
|
60
|
+
if (opts.dmg !== null) opts.dmg = path.resolve(opts.dmg);
|
|
61
|
+
if (opts.zip !== null) opts.zip = path.resolve(opts.zip);
|
|
62
|
+
if (opts.app !== null) opts.app = path.resolve(opts.app);
|
|
51
63
|
return opts;
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -164,9 +176,11 @@ export async function installApp(opts, deps = {}) {
|
|
|
164
176
|
emit = () => {},
|
|
165
177
|
tmpdir = os.tmpdir(),
|
|
166
178
|
} = deps;
|
|
167
|
-
const
|
|
179
|
+
const dev = opts.dev === true;
|
|
180
|
+
const event = (name, data = {}) => emit({ event: name, ...(dev ? { dev: true } : {}), ...data });
|
|
168
181
|
const run = (cmd, args) => execFile(cmd, args);
|
|
169
182
|
let mountRoot = null;
|
|
183
|
+
let extractRoot = null;
|
|
170
184
|
let mounted = false;
|
|
171
185
|
let mountPoint = null;
|
|
172
186
|
let failedStage = "start";
|
|
@@ -174,51 +188,74 @@ export async function installApp(opts, deps = {}) {
|
|
|
174
188
|
const detach = async () => {
|
|
175
189
|
if (!mounted) return;
|
|
176
190
|
mounted = false;
|
|
177
|
-
|
|
178
|
-
catch { try { await run("/usr/bin/hdiutil", ["detach", "-force", mountPoint]); } catch { /* best effort */ } }
|
|
191
|
+
await detachWithRetry({ mount: mountPoint, log: (e) => event(e.event, e), sleep, detach: (args) => run("/usr/bin/hdiutil", args) });
|
|
179
192
|
try { fsImpl.rmSync(mountRoot, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
180
193
|
};
|
|
194
|
+
const cleanupExtract = () => {
|
|
195
|
+
if (!extractRoot) return;
|
|
196
|
+
try { fsImpl.rmSync(extractRoot, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
197
|
+
extractRoot = null;
|
|
198
|
+
};
|
|
181
199
|
|
|
182
200
|
try {
|
|
183
201
|
if (platform !== "darwin") throw new InstallAppError("unsupported", `install-app is not implemented on ${platform}`);
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
const inputPath = opts.dmg ?? opts.zip ?? opts.app;
|
|
203
|
+
const inputKind = opts.dmg ? "dmg" : opts.zip ? "zip" : "app";
|
|
204
|
+
event("start", { [inputKind]: inputPath, dest: opts.dest });
|
|
205
|
+
if (dev) event("dev-build", { note: "DEV BUILD (not notarized)" });
|
|
206
|
+
if (!fsImpl.existsSync(inputPath)) throw new InstallAppError(`${inputKind}-missing`, `${inputKind.toUpperCase()} not found: ${inputPath}`);
|
|
186
207
|
if (!fsImpl.existsSync(opts.dest)) throw new InstallAppError("dest-missing", `destination not found: ${opts.dest}`);
|
|
187
208
|
|
|
188
|
-
failedStage = "attach";
|
|
189
|
-
mountRoot = fsImpl.mkdtempSync(path.join(tmpdir, "right-install-app-"));
|
|
190
|
-
const attached = await run("/usr/bin/hdiutil", ["attach", "-nobrowse", "-readonly", "-noautoopen", "-mountrandom", mountRoot, "-plist", opts.dmg]);
|
|
191
|
-
const mounts = mountPointsFromAttach(attached.stdout);
|
|
192
|
-
if (mounts.length === 0) throw new InstallAppError("attach", "DMG attached but exposed no mount point");
|
|
193
|
-
mountPoint = mounts[0];
|
|
194
|
-
mounted = true;
|
|
195
|
-
|
|
196
209
|
let sourceApp = null;
|
|
197
|
-
|
|
198
|
-
const apps = fsImpl.readdirSync(
|
|
199
|
-
if (opts.appName)
|
|
200
|
-
|
|
201
|
-
|
|
210
|
+
const pickApp = (dir, label) => {
|
|
211
|
+
const apps = fsImpl.readdirSync(dir).filter((n) => n.endsWith(".app"));
|
|
212
|
+
if (opts.appName) return apps.includes(opts.appName) ? path.join(dir, opts.appName) : null;
|
|
213
|
+
if (apps.length === 1) return path.join(dir, apps[0]);
|
|
214
|
+
if (apps.length > 1) throw new InstallAppError("ambiguous-app", `${label} contains multiple apps (${apps.join(", ")}); pass --app-name`);
|
|
215
|
+
return null;
|
|
216
|
+
};
|
|
217
|
+
if (opts.dmg) {
|
|
218
|
+
failedStage = "attach";
|
|
219
|
+
mountRoot = fsImpl.mkdtempSync(path.join(tmpdir, "right-install-app-"));
|
|
220
|
+
const attached = await run("/usr/bin/hdiutil", ["attach", "-nobrowse", "-readonly", "-noautoopen", "-mountrandom", mountRoot, "-plist", opts.dmg]);
|
|
221
|
+
const mounts = mountPointsFromAttach(attached.stdout);
|
|
222
|
+
if (mounts.length === 0) throw new InstallAppError("attach", "DMG attached but exposed no mount point");
|
|
223
|
+
mountPoint = mounts[0];
|
|
224
|
+
mounted = true;
|
|
225
|
+
for (const mp of mounts) {
|
|
226
|
+
sourceApp = pickApp(mp, "DMG");
|
|
227
|
+
if (sourceApp) break;
|
|
228
|
+
}
|
|
229
|
+
if (!sourceApp) throw new InstallAppError("app-not-found", opts.appName ? `${opts.appName} not found in DMG` : "no .app found in DMG");
|
|
230
|
+
} else if (opts.zip) {
|
|
231
|
+
failedStage = "extract";
|
|
232
|
+
extractRoot = fsImpl.mkdtempSync(path.join(tmpdir, "right-install-app-zip-"));
|
|
233
|
+
try { await run("/usr/bin/ditto", ["-x", "-k", opts.zip, extractRoot]); }
|
|
234
|
+
catch (error) { throw new InstallAppError("zip-extract", `ditto -x -k failed for ${opts.zip}: ${errText(error)}`); }
|
|
235
|
+
sourceApp = pickApp(extractRoot, "zip");
|
|
236
|
+
if (!sourceApp) throw new InstallAppError("app-not-found", opts.appName ? `${opts.appName} not found in zip` : "no .app found in zip");
|
|
237
|
+
} else {
|
|
238
|
+
sourceApp = opts.app.replace(/\/+$/, "");
|
|
239
|
+
if (opts.appName && path.basename(sourceApp) !== opts.appName) throw new InstallAppError("app-not-found", `--app is ${path.basename(sourceApp)}, not ${opts.appName}`);
|
|
202
240
|
}
|
|
203
|
-
if (!sourceApp) throw new InstallAppError("app-not-found", opts.appName ? `${opts.appName} not found in DMG` : "no .app found in DMG");
|
|
204
241
|
const appName = path.basename(sourceApp);
|
|
205
242
|
const installed = path.join(opts.dest, appName);
|
|
206
243
|
const hadInstalled = fsImpl.existsSync(installed);
|
|
207
244
|
|
|
208
245
|
// Verify the mounted app before touching anything installed.
|
|
209
246
|
failedStage = "verify";
|
|
210
|
-
const newTeam = await verifyBundle(run, sourceApp, { assess:
|
|
247
|
+
const { team: newTeam, ticket } = await verifyBundle(run, sourceApp, { assess: !dev, staple: !dev, dmg: opts.dmg });
|
|
211
248
|
const newBundleId = await readBundleId(run, sourceApp);
|
|
212
|
-
if (opts.bundleId && newBundleId !== opts.bundleId) throw new InstallAppError("bundle-id-mismatch",
|
|
249
|
+
if (opts.bundleId && newBundleId !== opts.bundleId) throw new InstallAppError("bundle-id-mismatch", `${inputKind.toUpperCase()} app bundle id ${newBundleId} != --bundle-id ${opts.bundleId}`);
|
|
213
250
|
let installedTeam = null;
|
|
214
251
|
if (hadInstalled) {
|
|
215
252
|
const installedBundleId = await readBundleId(run, installed);
|
|
216
|
-
if (installedBundleId !== newBundleId) throw new InstallAppError("bundle-id-mismatch",
|
|
253
|
+
if (installedBundleId !== newBundleId) throw new InstallAppError("bundle-id-mismatch", `${inputKind.toUpperCase()} app bundle id ${newBundleId} != installed bundle id ${installedBundleId}`);
|
|
217
254
|
try { installedTeam = await teamIdOf(run, installed); } catch { installedTeam = null; }
|
|
218
|
-
if (installedTeam && installedTeam !== newTeam) throw new InstallAppError("team-id-changed", `refusing team change: installed ${installedTeam},
|
|
255
|
+
if (installedTeam && installedTeam !== newTeam) throw new InstallAppError("team-id-changed", `refusing team change: installed ${installedTeam}, ${inputKind.toUpperCase()} ${newTeam}`);
|
|
219
256
|
}
|
|
220
257
|
const bundleId = opts.bundleId || newBundleId;
|
|
221
|
-
event("verified", { app: appName, bundleId, teamId: newTeam, replacing: hadInstalled });
|
|
258
|
+
event("verified", { app: appName, bundleId, teamId: newTeam, replacing: hadInstalled, ...(ticket ? { ticket } : {}) });
|
|
222
259
|
|
|
223
260
|
// Quit running instance without UI.
|
|
224
261
|
failedStage = "quit";
|
|
@@ -244,11 +281,17 @@ export async function installApp(opts, deps = {}) {
|
|
|
244
281
|
throw new InstallAppError("ditto-failed", `ditto failed (${errText(error)}); ${trashed ? (restored ? "previous app restored" : `RESTORE FAILED, previous app is at ${trashed}`) : "no previous app to restore"}`);
|
|
245
282
|
}
|
|
246
283
|
event("replaced", { installed, previous: trashed });
|
|
284
|
+
// Dev builds are not notarized: clear quarantine on the NEW bundle only so Gatekeeper does not block it.
|
|
285
|
+
if (dev) {
|
|
286
|
+
try { await run("/usr/bin/xattr", ["-dr", "com.apple.quarantine", installed]); }
|
|
287
|
+
catch (error) { event("quarantine-clear-failed", { installed, reason: errText(error) }); }
|
|
288
|
+
}
|
|
247
289
|
await detach();
|
|
290
|
+
cleanupExtract();
|
|
248
291
|
|
|
249
292
|
// Re-verify installed copy.
|
|
250
293
|
failedStage = "reverify";
|
|
251
|
-
await verifyBundle(run, installed, { assess:
|
|
294
|
+
await verifyBundle(run, installed, { assess: !dev, staple: false });
|
|
252
295
|
|
|
253
296
|
// Relaunch hidden and in the background only.
|
|
254
297
|
const relaunch = opts.relaunch === null ? wasRunning : opts.relaunch;
|
|
@@ -257,7 +300,7 @@ export async function installApp(opts, deps = {}) {
|
|
|
257
300
|
await run("/usr/bin/open", ["-g", "-j", "-b", bundleId]);
|
|
258
301
|
event("relaunched", { bundleId });
|
|
259
302
|
}
|
|
260
|
-
const result = { ok: true, app: appName, installed, bundleId, teamId: newTeam, previous: trashed, relaunched: relaunch };
|
|
303
|
+
const result = { ok: true, app: appName, installed, bundleId, teamId: newTeam, previous: trashed, relaunched: relaunch, ...(dev ? { dev: true } : {}) };
|
|
261
304
|
event("done", result);
|
|
262
305
|
return result;
|
|
263
306
|
} catch (error) {
|
|
@@ -267,10 +310,11 @@ export async function installApp(opts, deps = {}) {
|
|
|
267
310
|
throw error;
|
|
268
311
|
} finally {
|
|
269
312
|
await detach();
|
|
313
|
+
cleanupExtract();
|
|
270
314
|
}
|
|
271
315
|
}
|
|
272
316
|
|
|
273
|
-
async function verifyBundle(run, app, { assess, staple }) {
|
|
317
|
+
async function verifyBundle(run, app, { assess, staple, dmg = null }) {
|
|
274
318
|
const step = async (code, cmd, args) => {
|
|
275
319
|
try { return await run(cmd, args); }
|
|
276
320
|
catch (error) { throw new InstallAppError(code, `${code} failed for ${app}: ${errText(error)}`); }
|
|
@@ -280,8 +324,23 @@ async function verifyBundle(run, app, { assess, staple }) {
|
|
|
280
324
|
if (!team) throw new InstallAppError("team-id-missing", `${app} has no Team ID (ad-hoc or unsigned)`);
|
|
281
325
|
await step("codesign-requirement", "/usr/bin/codesign", ["-dr", "-", app]);
|
|
282
326
|
if (assess) await step("spctl-assess", "/usr/sbin/spctl", ["--assess", "--type", "execute", app]);
|
|
283
|
-
|
|
284
|
-
|
|
327
|
+
let ticket = null;
|
|
328
|
+
if (staple) {
|
|
329
|
+
try { await run("/usr/bin/xcrun", ["stapler", "validate", app]); ticket = "app-ticket"; }
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (!dmg) throw new InstallAppError("stapler-validate", `stapler-validate failed for ${app}: ${errText(error)}`);
|
|
332
|
+
// Standard Tauri flow: the DMG carries the notarization ticket, not the app. Accept only when the
|
|
333
|
+
// stapled DMG validates, its primary signature is accepted, and Gatekeeper reports the app Notarized Developer ID.
|
|
334
|
+
await step("stapler-validate", "/usr/bin/xcrun", ["stapler", "validate", dmg]);
|
|
335
|
+
await step("spctl-dmg", "/usr/sbin/spctl", ["-a", "-t", "open", "--context", "context:primary-signature", dmg]);
|
|
336
|
+
const assessed = await step("spctl-notarized", "/usr/sbin/spctl", ["-a", "-t", "exec", "-vv", app]);
|
|
337
|
+
if (!/Notarized Developer ID/.test(`${assessed?.stderr || ""}\n${assessed?.stdout || ""}`)) {
|
|
338
|
+
throw new InstallAppError("spctl-notarized", `spctl-notarized failed for ${app}: not accepted as Notarized Developer ID`);
|
|
339
|
+
}
|
|
340
|
+
ticket = "dmg-ticket";
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { team, ticket };
|
|
285
344
|
}
|
|
286
345
|
|
|
287
346
|
async function teamIdOf(run, app) {
|
|
@@ -336,13 +395,15 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
336
395
|
try { opts = parseArgs(argv); }
|
|
337
396
|
catch (error) {
|
|
338
397
|
stderr(`right-release install-app: ${error.message}`);
|
|
339
|
-
stderr("usage: right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id dev.orthic.pulse] [--dest /Applications] [--relaunch|--no-relaunch] [--json]");
|
|
398
|
+
stderr("usage: right-release install-app (--dmg <path.dmg> | --dev (--zip <path.zip> | --app <path.app> | --dmg <path.dmg>)) [--app-name Pulse.app] [--bundle-id dev.orthic.pulse] [--dest /Applications] [--relaunch|--no-relaunch] [--json]");
|
|
399
|
+
stderr(" --dev installs a Developer ID-signed, NOT notarized build (skips spctl/stapler, clears quarantine on the new bundle).");
|
|
340
400
|
return 2;
|
|
341
401
|
}
|
|
342
402
|
const emit = (e) => {
|
|
343
403
|
if (opts.json) { stdout(JSON.stringify({ schema: 1, ts: new Date().toISOString(), ...e })); return; }
|
|
344
404
|
const { event, ...rest } = e;
|
|
345
405
|
if (event === "failed") stderr(`install-app: failed at ${rest.stage}: ${rest.reason}`);
|
|
406
|
+
else if (event === "dev-build") stdout(`install-app: ${rest.note}`);
|
|
346
407
|
else stdout(`install-app: ${event}${Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : ""}`);
|
|
347
408
|
};
|
|
348
409
|
try {
|
package/macos-notarization.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
@@ -70,6 +70,26 @@ export function notarizeMacosArtifact({ archive, receiptPath, authArgs, profile,
|
|
|
70
70
|
return receipt;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Notarize and staple a signed .app BEFORE it is packaged into a DMG, so a copy dragged out of the
|
|
75
|
+
* DMG passes `stapler validate` offline. The app is zipped with ditto (notarytool cannot take a bare
|
|
76
|
+
* bundle), submitted, and its own ticket stapled; the DMG is then notarized and stapled as usual.
|
|
77
|
+
* The caller removes the zip after validating the receipt, which hashes it. The receipt records the submitted zip; its stapling evidence names the app bundle.
|
|
78
|
+
*/
|
|
79
|
+
export function notarizeAppBundle({ app, receiptPath, profile, authArgs, zip, run = spawnSync, env = process.env, ...agentOptions }) {
|
|
80
|
+
app = path.resolve(app);
|
|
81
|
+
if (!/\.app$/i.test(app)) throw new Error(`notarize.app must name an app bundle: ${app}`);
|
|
82
|
+
zip = path.resolve(zip ?? `${app}.zip`);
|
|
83
|
+
rmSync(zip, { force: true });
|
|
84
|
+
checkedDitto(["-c", "-k", "--keepParent", app, zip], run);
|
|
85
|
+
return notarizeMacosArtifact({ archive: zip, receiptPath, profile, authArgs, app, run, env, ...agentOptions });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function checkedDitto(args, run) {
|
|
89
|
+
const result = run("/usr/bin/ditto", args, { encoding: "utf8" });
|
|
90
|
+
if (result.error || result.status !== 0) throw new Error(`ditto failed: ${result.error?.message ?? result.stderr ?? result.stdout ?? result.status}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
export function validateNotarizationReceipt({ receiptPath, archive, run = spawnSync }) {
|
|
74
94
|
let receipt;
|
|
75
95
|
try { receipt = JSON.parse(readFileSync(receiptPath, "utf8")); }
|
package/ownership.mjs
CHANGED
|
@@ -234,7 +234,31 @@ function committedFileInventory(appRoot, trust, pathPrefix, committed) {
|
|
|
234
234
|
return { files, coverage, committed: true, pathPrefix, repositoryFiles, snapshotFiles: new Map(repositoryFiles.map((file) => [file.rel, file])) };
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// Blank out Swift `//` and nested `/* */` comments outside string literals, keeping offsets.
|
|
238
|
+
export function stripSwiftComments(text) {
|
|
239
|
+
const out = text.split("");
|
|
240
|
+
let quote = false, depth = 0;
|
|
241
|
+
for (let i = 0; i < text.length; i++) {
|
|
242
|
+
if (depth) {
|
|
243
|
+
if (text.startsWith("/*", i)) { depth++; out[i] = out[i + 1] = " "; i++; continue; }
|
|
244
|
+
if (text.startsWith("*/", i)) { depth--; out[i] = out[i + 1] = " "; i++; continue; }
|
|
245
|
+
if (text[i] !== "\n") out[i] = " ";
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (quote) {
|
|
249
|
+
if (text[i] === "\\") { i++; continue; }
|
|
250
|
+
if (text[i] === '"' || text[i] === "\n") quote = false;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (text[i] === '"') { quote = true; continue; }
|
|
254
|
+
if (text.startsWith("//", i)) { for (; i < text.length && text[i] !== "\n"; i++) out[i] = " "; continue; }
|
|
255
|
+
if (text.startsWith("/*", i)) { depth = 1; out[i] = out[i + 1] = " "; i++; }
|
|
256
|
+
}
|
|
257
|
+
return out.join("");
|
|
258
|
+
}
|
|
259
|
+
|
|
237
260
|
function swiftPackageArguments(text) {
|
|
261
|
+
text = stripSwiftComments(text);
|
|
238
262
|
const args = [];
|
|
239
263
|
for (const match of text.matchAll(/\.package\s*\(/g)) {
|
|
240
264
|
let at = match.index + match[0].length, start = at, depth = 1, quote = false;
|
package/package.json
CHANGED
package/release.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
|
27
27
|
import { terminateProcessTree } from "./heavy-command.mjs";
|
|
28
28
|
import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
|
|
29
29
|
import { signingReceiptPath } from "./signing-receipts.mjs";
|
|
30
|
-
import { validateNotarizationReceipt } from "./macos-notarization.mjs";
|
|
30
|
+
import { notarizeAppBundle, validateNotarizationReceipt } from "./macos-notarization.mjs";
|
|
31
31
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
32
32
|
import { runConfiguredChecks } from "./release-checks.mjs";
|
|
33
33
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
@@ -156,6 +156,7 @@ if (opts.platform === "mac") {
|
|
|
156
156
|
if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
|
|
157
157
|
if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
|
|
158
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`);
|
|
159
160
|
}
|
|
160
161
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
161
162
|
/**
|
|
@@ -310,7 +311,11 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
310
311
|
// Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
|
|
311
312
|
const signingFiles = target.sign.prePackageFiles.map((p) => resolveReleaseArtifactPath(root, p));
|
|
312
313
|
for (const file of signingFiles) await mustExist(file, `missing macOS Developer ID signing artifact: ${file}`);
|
|
313
|
-
if (runMacPhase("sign"))
|
|
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
|
+
}
|
|
314
319
|
} else if (nativeAssembly) {
|
|
315
320
|
if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
|
|
316
321
|
if (nativeFinalizer) {
|
|
@@ -397,7 +402,7 @@ releaseLock.release();
|
|
|
397
402
|
|
|
398
403
|
function usage(code, message) {
|
|
399
404
|
if (message) console.error(`right-release: ${message}\n`);
|
|
400
|
-
console.error("usage:
|
|
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)");
|
|
401
406
|
process.exit(code);
|
|
402
407
|
}
|
|
403
408
|
|
|
@@ -870,7 +875,8 @@ async function notarizeMacos(archive, root) {
|
|
|
870
875
|
if (opts.dryRun) args.push("--dry-run");
|
|
871
876
|
args.push("--receipt", receipt);
|
|
872
877
|
if (target.notarize.profile) args.push("--profile", target.notarize.profile);
|
|
873
|
-
|
|
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));
|
|
874
880
|
args.push(archive);
|
|
875
881
|
if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
|
|
876
882
|
console.log("right-release: notarizing macOS archive");
|
|
@@ -878,6 +884,21 @@ async function notarizeMacos(archive, root) {
|
|
|
878
884
|
if (!opts.dryRun) verifyMacosNotarizationReceipt(receipt, archive);
|
|
879
885
|
}
|
|
880
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
|
+
|
|
881
902
|
function macosReceiptPath(root, phase) {
|
|
882
903
|
return signingReceiptPath({ appRoot: root, target, platform: "mac", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
883
904
|
}
|
package/rightkit-versions.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
|
@@ -169,7 +170,8 @@
|
|
|
169
170
|
"0.2.135",
|
|
170
171
|
"0.2.136",
|
|
171
172
|
"0.2.137",
|
|
172
|
-
"0.2.138"
|
|
173
|
+
"0.2.138",
|
|
174
|
+
"0.2.139"
|
|
173
175
|
],
|
|
174
176
|
"@rightkit/qa": [
|
|
175
177
|
"0.1.0",
|
|
@@ -276,11 +278,11 @@
|
|
|
276
278
|
"rightkit-framed-sidecar": "0.1.0",
|
|
277
279
|
"rightkit-fs": "0.3.2",
|
|
278
280
|
"rightkit-fsindex": "0.1.0",
|
|
279
|
-
"rightkit-http": "0.2.
|
|
281
|
+
"rightkit-http": "0.2.4",
|
|
280
282
|
"rightkit-jobs": "0.2.6",
|
|
281
283
|
"rightkit-license": "0.1.3",
|
|
282
284
|
"rightkit-logs": "0.2.0",
|
|
283
|
-
"rightkit-mcp": "0.5.
|
|
285
|
+
"rightkit-mcp": "0.5.2",
|
|
284
286
|
"rightkit-media": "0.2.4",
|
|
285
287
|
"rightkit-models": "0.2.7",
|
|
286
288
|
"rightkit-oauth": "0.3.0",
|
|
@@ -333,11 +335,11 @@
|
|
|
333
335
|
"rightkit-framed-sidecar": "0.1.0",
|
|
334
336
|
"rightkit-fs": "0.3.2",
|
|
335
337
|
"rightkit-fsindex": "0.1.0",
|
|
336
|
-
"rightkit-http": "0.2.
|
|
338
|
+
"rightkit-http": "0.2.4",
|
|
337
339
|
"rightkit-jobs": "0.2.6",
|
|
338
340
|
"rightkit-license": "0.1.3",
|
|
339
341
|
"rightkit-logs": "0.2.0",
|
|
340
|
-
"rightkit-mcp": "0.5.
|
|
342
|
+
"rightkit-mcp": "0.5.2",
|
|
341
343
|
"rightkit-media": "0.2.4",
|
|
342
344
|
"rightkit-models": "0.2.7",
|
|
343
345
|
"rightkit-oauth": "0.3.0",
|
|
@@ -368,7 +370,7 @@
|
|
|
368
370
|
"@rightkit/app-shell": "0.3.0",
|
|
369
371
|
"@rightkit/ax": "0.2.4",
|
|
370
372
|
"@rightkit/doc-render": "0.1.4",
|
|
371
|
-
"@rightkit/git": "0.2.
|
|
373
|
+
"@rightkit/git": "0.2.32",
|
|
372
374
|
"@rightkit/hooks": "0.1.2",
|
|
373
375
|
"@rightkit/i18n": "0.1.0",
|
|
374
376
|
"@rightkit/legal": "0.3.1",
|
|
@@ -381,7 +383,7 @@
|
|
|
381
383
|
"@rightkit/platform-ui": "0.2.0",
|
|
382
384
|
"@rightkit/qa": "0.4.0",
|
|
383
385
|
"@rightkit/qa-chrome": "0.1.3",
|
|
384
|
-
"@rightkit/release": "0.2.
|
|
386
|
+
"@rightkit/release": "0.2.140",
|
|
385
387
|
"@rightkit/settings-ui": "0.3.0",
|
|
386
388
|
"@rightkit/shell": "0.1.0",
|
|
387
389
|
"@rightkit/tauri": "0.2.0",
|
|
@@ -433,7 +435,8 @@
|
|
|
433
435
|
"0.3.0",
|
|
434
436
|
"0.3.1",
|
|
435
437
|
"0.4.0",
|
|
436
|
-
"0.5.0"
|
|
438
|
+
"0.5.0",
|
|
439
|
+
"0.5.1"
|
|
437
440
|
],
|
|
438
441
|
"rightkit-media": [
|
|
439
442
|
"0.2.2",
|
|
@@ -505,7 +508,9 @@
|
|
|
505
508
|
"rightkit-http": [
|
|
506
509
|
"0.1.0",
|
|
507
510
|
"0.2.0",
|
|
508
|
-
"0.2.1"
|
|
511
|
+
"0.2.1",
|
|
512
|
+
"0.2.2",
|
|
513
|
+
"0.2.3"
|
|
509
514
|
],
|
|
510
515
|
"rightkit-fs": [
|
|
511
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")}`);
|