@rightkit/release 0.6.8 → 0.6.11
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 +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- package/win-package-recipe.mjs +20 -7
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// The freshness census of a direct (Linux) snapshot compile (item 7 row 7.19; GATE-CONTRACT "Returned evidence", `freshness`:
|
|
2
|
+
// `{ unitsAtStart, fresh, notFresh, invalidationReasons }`, the census the measured-cache rule is judged from; E16: "the second
|
|
3
|
+
// build's census shows reuse").
|
|
4
|
+
//
|
|
5
|
+
// Cargo keeps no invalidation reason on disk: a fingerprint file holds the current hash only. The reason exists only in Cargo's own
|
|
6
|
+
// fingerprint log (`CARGO_LOG=cargo::core::compiler::fingerprint=info`: a `fingerprint dirty for <unit>` line, then `dirty: <reason>`),
|
|
7
|
+
// the same source the RustRight broker reads for its receipt (rustright-broker job_service/cargo_evidence.rs; attested-runs.md).
|
|
8
|
+
// The gate turns that log on for the cargo processes that compile a workspace, on THAT process only (never in the scan environment
|
|
9
|
+
// the repository's own tests and scripts inherit), reads it from the process's standard error, and keeps the lines out of the output
|
|
10
|
+
// unless the caller set CARGO_LOG itself (a caller's own CARGO_LOG is read the same way and left visible).
|
|
11
|
+
//
|
|
12
|
+
// Counts come from the target directory the way the broker takes them (rustright-policy cargo_freshness.rs): a unit is one directory
|
|
13
|
+
// of a `.fingerprint`; a unit whose newest file is at or after the census start was rebuilt (`notFresh`, a new unit included), the
|
|
14
|
+
// others were reused (`fresh`). Judging unexplained rebuilds belongs to item 11; this module only measures and records.
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
/** The `CARGO_LOG` filter the gate sets for a compiling cargo process (the broker's FINGERPRINT_LOG_FILTER). */
|
|
19
|
+
export const FINGERPRINT_LOG_FILTER = "cargo::core::compiler::fingerprint=info";
|
|
20
|
+
const FINGERPRINT_TARGET = "cargo::core::compiler::fingerprint";
|
|
21
|
+
export const FRESHNESS_SOURCE = "cargo-fingerprint-log+fingerprint-mtime";
|
|
22
|
+
/** Reasons kept in the receipt; the rest are counted. */
|
|
23
|
+
export const REASONS_CAP = 200;
|
|
24
|
+
/** Characters kept of one reason (Cargo prints it with `Debug`, paths and mtimes included). */
|
|
25
|
+
export const REASON_TEXT_CAP = 512;
|
|
26
|
+
/** Unit directories and files visited before the census is declared unavailable rather than partial. */
|
|
27
|
+
export const CENSUS_ENTRY_CAP = 250_000;
|
|
28
|
+
// A held partial line longer than this is not a log line.
|
|
29
|
+
const PARTIAL_CAP = 64 * 1024;
|
|
30
|
+
|
|
31
|
+
const stripAnsi = (text) => text.replace(/\u001b\[[^A-Za-z]*[A-Za-z]/g, "");
|
|
32
|
+
const afterPrefix = (message, prefixes) => {
|
|
33
|
+
for (const prefix of prefixes) if (message.startsWith(prefix)) return message.slice(prefix.length);
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** `name version` and the target of the unit a dirty line names (the span carries `target="..."`; the message `<name> v<ver> (<src>)/<mode>/...`). */
|
|
38
|
+
function unitOf(prefix, rest) {
|
|
39
|
+
let target = null;
|
|
40
|
+
const at = prefix.indexOf('target="');
|
|
41
|
+
if (at >= 0) {
|
|
42
|
+
const value = prefix.slice(at + 8);
|
|
43
|
+
const end = value.indexOf('"');
|
|
44
|
+
if (end >= 0) target = value.slice(0, end);
|
|
45
|
+
}
|
|
46
|
+
const head = rest.split("/")[0] ?? rest;
|
|
47
|
+
const unit = (head.split(" (")[0] ?? head).trim();
|
|
48
|
+
return { unit, target };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The reason's variant name (`FsStatusOutdated`, `RustflagsChanged`, ...): what a summary groups by. */
|
|
52
|
+
const reasonKind = (reason) => {
|
|
53
|
+
const kind = reason.split(/[^\p{L}\p{N}_]/u)[0];
|
|
54
|
+
return kind ? kind : "unknown";
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Reads Cargo's fingerprint log from a process's standard error; `strip` keeps its lines out of what the caller sees. */
|
|
58
|
+
export class FingerprintLog {
|
|
59
|
+
constructor({ strip = false } = {}) {
|
|
60
|
+
this.strip = strip;
|
|
61
|
+
this.partial = "";
|
|
62
|
+
this.current = null;
|
|
63
|
+
this.dirtyUnits = 0;
|
|
64
|
+
this.reasons = [];
|
|
65
|
+
this.truncated = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One chunk of standard error in; what the caller sees out. Without stripping that is the chunk itself. */
|
|
69
|
+
feed(chunk) {
|
|
70
|
+
this.partial += String(chunk);
|
|
71
|
+
let shown = "";
|
|
72
|
+
for (let at = this.partial.indexOf("\n"); at >= 0; at = this.partial.indexOf("\n")) {
|
|
73
|
+
const line = this.partial.slice(0, at + 1);
|
|
74
|
+
this.partial = this.partial.slice(at + 1);
|
|
75
|
+
const isLog = this.#line(line);
|
|
76
|
+
if (!isLog || !this.strip) shown += line;
|
|
77
|
+
}
|
|
78
|
+
if (this.partial.length > PARTIAL_CAP) {
|
|
79
|
+
const rest = this.partial;
|
|
80
|
+
this.partial = "";
|
|
81
|
+
if (this.strip) shown += rest;
|
|
82
|
+
}
|
|
83
|
+
return this.strip ? shown : String(chunk);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The end of the stream: a held partial line is judged and, when stripping and it is not a log line, returned. */
|
|
87
|
+
finish() {
|
|
88
|
+
const rest = this.partial;
|
|
89
|
+
this.partial = "";
|
|
90
|
+
if (rest === "" || this.#line(rest) || !this.strip) return "";
|
|
91
|
+
return rest;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#line(raw) {
|
|
95
|
+
const line = stripAnsi(raw.replace(/\s+$/, ""));
|
|
96
|
+
const at = line.indexOf(FINGERPRINT_TARGET);
|
|
97
|
+
if (at < 0) return false;
|
|
98
|
+
const message = line.slice(at + FINGERPRINT_TARGET.length).replace(/^:+/, "").trim();
|
|
99
|
+
const unitLine = afterPrefix(message, ["fingerprint dirty for ", "fingerprint error for "]);
|
|
100
|
+
if (unitLine !== null) {
|
|
101
|
+
this.dirtyUnits += 1;
|
|
102
|
+
this.current = unitOf(line.slice(0, at), unitLine);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const reason = afterPrefix(message, ["dirty:", "err:"]);
|
|
106
|
+
if (reason !== null) this.#record(reason.trim());
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#record(reason) {
|
|
111
|
+
if (this.reasons.length >= REASONS_CAP) { this.truncated += 1; return; }
|
|
112
|
+
const { unit, target } = this.current ?? { unit: "unknown", target: null };
|
|
113
|
+
const chars = Array.from(reason);
|
|
114
|
+
const text = chars.length > REASON_TEXT_CAP ? `${chars.slice(0, REASON_TEXT_CAP).join("")}…` : reason;
|
|
115
|
+
this.reasons.push({ unit, target, kind: reasonKind(reason), reason: text });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What another log's reading adds to this one (the census keeps one aggregate over every compiling process). */
|
|
119
|
+
merge(other) {
|
|
120
|
+
this.dirtyUnits += other.dirtyUnits;
|
|
121
|
+
this.truncated += other.truncated;
|
|
122
|
+
for (const reason of other.reasons) {
|
|
123
|
+
if (this.reasons.length >= REASONS_CAP) this.truncated += 1;
|
|
124
|
+
else this.reasons.push(reason);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Cargo subcommands that compile a workspace's units (the wrapper's COMPILE set without metadata and package, which the census
|
|
130
|
+
// does not count). `--` ends cargo's own arguments; `--color` and `--explain` take a value.
|
|
131
|
+
const COMPILING = new Set(["build", "b", "check", "c", "test", "t", "bench", "run", "r", "doc", "d", "rustc", "rustdoc", "clippy"]);
|
|
132
|
+
const GLOBAL_VALUE = new Set(["--color", "--explain"]);
|
|
133
|
+
|
|
134
|
+
/** True when `command args` is a cargo process that compiles a workspace's units (its fingerprint log is the census's). */
|
|
135
|
+
export function compilesWorkspace(command, args) {
|
|
136
|
+
if (path.basename(String(command)).replace(/\.exe$/i, "") !== "cargo") return false;
|
|
137
|
+
const words = Array.isArray(args) ? args : [];
|
|
138
|
+
let i = typeof words[0] === "string" && words[0].startsWith("+") ? 1 : 0;
|
|
139
|
+
for (; i < words.length; i += 1) {
|
|
140
|
+
const w = String(words[i]);
|
|
141
|
+
if (w === "--") return false;
|
|
142
|
+
if (GLOBAL_VALUE.has(w)) { i += 1; continue; }
|
|
143
|
+
if (w.startsWith("-")) continue;
|
|
144
|
+
return COMPILING.has(w);
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Directories never searched for `.fingerprint`s: rustdoc output and packaging verification are not a profile's units.
|
|
150
|
+
const NOT_PROFILES = new Set(["doc", "package", "tmp", "incremental"]);
|
|
151
|
+
|
|
152
|
+
/** The `.fingerprint` directories of one target directory: `<target>/<profile>/.fingerprint` and `<target>/<triple>/<profile>/.fingerprint`. */
|
|
153
|
+
export function fingerprintDirectories(targetDir, fsApi = fs) {
|
|
154
|
+
const found = [];
|
|
155
|
+
const subdirs = (dir) => {
|
|
156
|
+
try { return fsApi.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !NOT_PROFILES.has(e.name)).map((e) => e.name); } catch { return []; }
|
|
157
|
+
};
|
|
158
|
+
const has = (dir) => { try { return fsApi.statSync(dir).isDirectory(); } catch { return false; } };
|
|
159
|
+
for (const first of subdirs(targetDir)) {
|
|
160
|
+
const direct = path.join(targetDir, first, ".fingerprint");
|
|
161
|
+
if (has(direct)) { found.push(direct); continue; }
|
|
162
|
+
for (const second of subdirs(path.join(targetDir, first))) {
|
|
163
|
+
const nested = path.join(targetDir, first, second, ".fingerprint");
|
|
164
|
+
if (has(nested)) found.push(nested);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Walks every fingerprint directory of `targetDirs`. `withTimes: false` only counts units; `withTimes: true` also takes each unit's
|
|
172
|
+
* newest file time. `capped` is set when more than `cap` entries (units and files) were visited.
|
|
173
|
+
*/
|
|
174
|
+
export function walkFingerprints(targetDirs, { withTimes, cap = CENSUS_ENTRY_CAP, fsApi = fs } = {}) {
|
|
175
|
+
const units = new Map();
|
|
176
|
+
let visited = 0;
|
|
177
|
+
for (const targetDir of targetDirs) {
|
|
178
|
+
for (const fpDir of fingerprintDirectories(targetDir, fsApi)) {
|
|
179
|
+
let names;
|
|
180
|
+
try { names = fsApi.readdirSync(fpDir); } catch { continue; }
|
|
181
|
+
for (const name of names) {
|
|
182
|
+
visited += 1;
|
|
183
|
+
if (visited > cap) return { units, capped: true };
|
|
184
|
+
const unitDir = path.join(fpDir, name);
|
|
185
|
+
if (!withTimes) { units.set(unitDir, 0); continue; }
|
|
186
|
+
let newest = 0;
|
|
187
|
+
try {
|
|
188
|
+
const own = fsApi.statSync(unitDir);
|
|
189
|
+
newest = own.mtimeMs;
|
|
190
|
+
if (own.isDirectory()) {
|
|
191
|
+
for (const file of fsApi.readdirSync(unitDir)) {
|
|
192
|
+
visited += 1;
|
|
193
|
+
if (visited > cap) return { units, capped: true };
|
|
194
|
+
try { newest = Math.max(newest, fsApi.statSync(path.join(unitDir, file)).mtimeMs); } catch { /* vanished: ignore */ }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch { /* vanished: counted with the time it had */ }
|
|
198
|
+
units.set(unitDir, newest);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { units, capped: false };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* One census for one stage of a direct compile: `start()` before the scans, `wrapSpawn(spawn)` around the scan runner's spawner
|
|
207
|
+
* (it turns the fingerprint log on for the compiling cargo processes only), `finish()` after them.
|
|
208
|
+
*/
|
|
209
|
+
export function createFreshnessCensus({ targetDirs, now = Date.now, fsApi = fs, cap = CENSUS_ENTRY_CAP } = {}) {
|
|
210
|
+
const dirs = [...new Set(Object.values(targetDirs ?? {}).filter((d) => typeof d === "string" && d))];
|
|
211
|
+
const log = new FingerprintLog({ strip: false });
|
|
212
|
+
let since = null; let atStart = null; let processes = 0; let outputCut = false;
|
|
213
|
+
return {
|
|
214
|
+
start() { since = now(); atStart = walkFingerprints(dirs, { withTimes: false, cap, fsApi }); },
|
|
215
|
+
wrapSpawn(spawn) {
|
|
216
|
+
return async (command, args, options = {}) => {
|
|
217
|
+
if (!compilesWorkspace(command, args)) return spawn(command, args, options);
|
|
218
|
+
const env = options.env ?? process.env;
|
|
219
|
+
// A caller's own CARGO_LOG stands and stays visible; the gate's own is kept out of the output.
|
|
220
|
+
const ours = !Object.keys(env).some((k) => k.toUpperCase() === "CARGO_LOG");
|
|
221
|
+
const result = await spawn(command, args, ours ? { ...options, env: { ...env, CARGO_LOG: FINGERPRINT_LOG_FILTER } } : options);
|
|
222
|
+
processes += 1;
|
|
223
|
+
if (!result || typeof result.stderr !== "string") return result;
|
|
224
|
+
if (result.truncated) outputCut = true;
|
|
225
|
+
const reading = new FingerprintLog({ strip: ours });
|
|
226
|
+
const shown = reading.feed(result.stderr) + reading.finish();
|
|
227
|
+
log.merge(reading);
|
|
228
|
+
return { ...result, stderr: shown };
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
finish() {
|
|
232
|
+
const base = { source: FRESHNESS_SOURCE, cargoProcesses: processes, dirtyUnits: log.dirtyUnits, truncated: log.truncated, ...(outputCut ? { outputCut: true } : {}) };
|
|
233
|
+
const unavailable = (reason) => ({ unitsAtStart: 0, fresh: 0, notFresh: 0, invalidationReasons: log.reasons, available: false, reason, ...base });
|
|
234
|
+
if (since === null || atStart === null) return unavailable("fingerprint-census-not-started");
|
|
235
|
+
const end = walkFingerprints(dirs, { withTimes: true, cap, fsApi });
|
|
236
|
+
if (atStart.capped || end.capped) return unavailable("fingerprint-census-entry-cap-exceeded");
|
|
237
|
+
if (end.units.size === 0) return unavailable("no-fingerprint-units-found");
|
|
238
|
+
let notFresh = 0;
|
|
239
|
+
for (const newest of end.units.values()) if (newest >= since) notFresh += 1;
|
|
240
|
+
return { unitsAtStart: atStart.units.size, fresh: end.units.size - notFresh, notFresh, invalidationReasons: log.reasons, available: true, ...base };
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const isCount = (n) => Number.isInteger(n) && n >= 0;
|
|
246
|
+
|
|
247
|
+
/** What is wrong with a `freshness` object (an empty list: it is the census the contract names, well formed). */
|
|
248
|
+
export function validateFreshness(freshness) {
|
|
249
|
+
const problems = [];
|
|
250
|
+
if (!freshness || typeof freshness !== "object" || Array.isArray(freshness)) return ["freshness is absent"];
|
|
251
|
+
for (const key of ["unitsAtStart", "fresh", "notFresh"]) if (!isCount(freshness[key])) problems.push(`freshness.${key} is not a count`);
|
|
252
|
+
if (!Array.isArray(freshness.invalidationReasons)) problems.push("freshness.invalidationReasons is not a list");
|
|
253
|
+
else freshness.invalidationReasons.forEach((r, i) => { if (!r || typeof r.unit !== "string" || typeof r.kind !== "string" || typeof r.reason !== "string") problems.push(`freshness.invalidationReasons[${i}] has no unit, kind and reason`); });
|
|
254
|
+
if (freshness.available !== undefined && typeof freshness.available !== "boolean") problems.push("freshness.available is not a boolean");
|
|
255
|
+
if (freshness.available === false && typeof freshness.reason !== "string") problems.push("an unavailable census names no reason");
|
|
256
|
+
return problems;
|
|
257
|
+
}
|
package/gate-executor.mjs
CHANGED
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
// broker - `rustright` is on PATH, or the first `cargo` is the broker's managed shim: every cargo call goes through the broker.
|
|
3
3
|
// refuse - a broker-managed machine (Mac/Dell markers, broker socket, managed agent-bin) whose broker cannot be reached from here:
|
|
4
4
|
// the gate never compiles around it.
|
|
5
|
-
// direct -
|
|
5
|
+
// direct - every Linux host (WSL2, VM, cloud, CI, Linux hosted on the Dell) always, and any unmanaged Mac or Windows host: cargo
|
|
6
|
+
// runs directly, through the same gates and stages.
|
|
6
7
|
import { brokerManagedHost, managedCargoShimOnPath } from "./cargo-contract.mjs";
|
|
8
|
+
import { directCargoPath, directPlatform } from "./managed-cargo.mjs";
|
|
9
|
+
|
|
10
|
+
export { directCargoPath };
|
|
7
11
|
|
|
8
12
|
export const EXECUTOR_KINDS = Object.freeze(["broker", "refuse", "direct"]);
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* @param {{ env?: object, platform?: string, exists?: (p: string) => boolean, which: (cmd: string) => string[], readFile: (p: string) => string }} o
|
|
12
|
-
* @returns {{ kind: "broker" | "refuse" | "direct", cargo: string[] | null, problem?: string }}
|
|
16
|
+
* @returns {{ kind: "broker" | "refuse" | "direct", cargo: string[] | null, problem?: string, env?: { PATH: string }, note?: string }}
|
|
13
17
|
*/
|
|
14
18
|
export function resolveExecutor({ env = process.env, platform = process.platform, exists, which, readFile }) {
|
|
19
|
+
// Every Linux (and any other non-Mac, non-Windows) execution host runs direct, whatever is on PATH or in the environment: WSL2, a VM,
|
|
20
|
+
// a cloud or CI runner, or Linux hosted on the Dell (GATE-CONTRACT §1 "One entry"; joint plan step 4). Only Adrian's Mac and Windows
|
|
21
|
+
// hosts use the broker.
|
|
22
|
+
// `cargo` there must be a real toolchain: RustRight shim directories are taken off the PATH the gate hands its children (`env`).
|
|
23
|
+
if (directPlatform(platform)) {
|
|
24
|
+
const direct = directCargoPath({ env, exists });
|
|
25
|
+
return { kind: "direct", cargo: ["cargo"], ...(direct.dropped.length ? { env: { PATH: direct.path }, note: `RustRight shims taken off PATH for direct cargo: ${direct.dropped.join(", ")}` } : {}), ...(direct.cargo ? {} : { note: `no direct cargo on PATH${direct.dropped.length ? ` (RustRight shims skipped: ${direct.dropped.join(", ")})` : ""}; cargo scans fail as a missing tool` }) };
|
|
26
|
+
}
|
|
15
27
|
if (which("rustright").length) return { kind: "broker", cargo: ["rustright", "cargo"] };
|
|
16
28
|
const first = which("cargo")[0];
|
|
17
29
|
if (first) {
|
package/gate-stages.mjs
CHANGED
|
@@ -15,9 +15,62 @@ import path from "node:path";
|
|
|
15
15
|
export const STAGES = Object.freeze(["check", "compile", "package", "qualify", "release"]);
|
|
16
16
|
export const RECEIPT_SCHEMA = "rightkit.gate-receipt/1";
|
|
17
17
|
export const RUN_ENV = "RK_GATE_RUN";
|
|
18
|
+
/** Set beside RUN_ENV for a run that carries a source split: the checkout whose git directory holds the run file. */
|
|
19
|
+
export const CHECKOUT_ENV = "RK_GATE_CHECKOUT";
|
|
18
20
|
|
|
19
21
|
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Where every stage the gate runs reads its source from (docs/GATE-CONTRACT.md "Commit-only build": every stage, `--to dev` and
|
|
25
|
+
* `--to test` included, builds a commit; uncommitted changes never enter a build). One row per stage name the gate dispatches:
|
|
26
|
+
* the STAGES, `dev` and `test`, `stage` (the hand-off to the publisher) and each STAGES entry again as a RightKit unit stage
|
|
27
|
+
* (`--to <stage> --unit <selector>`). `compiles` and `packages` say whether the stage starts a compiler or seals bytes.
|
|
28
|
+
* source "snapshot": the gate-owned snapshot of the selected commit (`wired: false` names a stage whose snapshot route is
|
|
29
|
+
* refused today, `blocker` saying why; it still never reads the checkout).
|
|
30
|
+
* source "sealed" or "publisher", with `basis`: the stage compiles nothing from source (it acts on bytes an earlier stage
|
|
31
|
+
* sealed, or runs only in the trusted publisher on hosted CI with its normal checkout, docs/GATE-CONTRACT.md).
|
|
32
|
+
* source "checkout", with `gap`: a known breach, listed so it cannot grow; stageSourceProblems ignores it, stageSourceGaps names it.
|
|
33
|
+
* door-stages.test.mjs fails on a stage with no row, on a compiling or packaging row that has none of these, and on a gap that is
|
|
34
|
+
* not in its expected list.
|
|
35
|
+
*/
|
|
36
|
+
export const STAGE_SOURCES = Object.freeze({
|
|
37
|
+
check: { compiles: false, packages: false, source: "snapshot" },
|
|
38
|
+
compile: { compiles: true, packages: false, source: "snapshot", interim: "Mac and Dell: the clean-HEAD checkout route until the cutover" },
|
|
39
|
+
// Wired in code (row 7.04: build-release.mjs, release.mjs, cli/right-release.mjs, release-source-identity.mjs): right-release builds and seals the selected commit from the snapshot. `wired` becomes true when door-package.test.mjs passes on L and on M and D (dev broker).
|
|
40
|
+
package: { compiles: true, packages: true, source: "snapshot", wired: false, blocker: "wired in code (rows 7.04 and 7.04c: release-artifact-path.mjs binds each Cargo manifest's target, one resolver reads every declared artifact path); awaiting 7.04c green on L, M and D: the dev-broker rows need the snapshot carrier (rows 7.20 to 7.24) and RustRight's item-4 dev pins" },
|
|
41
|
+
// Narrowed by decision 2 (7.06 correction): qualification rebuilds nothing, but its source reads (config, QA plan, test code) go through the split.
|
|
42
|
+
qualify: { compiles: false, packages: false, source: "snapshot", wired: false, blocker: "qa --stage installed takes its identity from the split (release-qa-command.mjs, row 7.04); awaiting its tests on every host" },
|
|
43
|
+
release: { compiles: false, packages: false, source: "publisher", basis: "--to release runs only in the trusted publisher on hosted CI with its normal checkout (docs/GATE-CONTRACT.md), and only uploads sealed bytes" },
|
|
44
|
+
dev: { compiles: true, packages: false, source: "snapshot", interim: "Mac and Dell: the checkout route (check --worktree) until the cutover" },
|
|
45
|
+
test: { compiles: true, packages: false, source: "snapshot", interim: "Mac and Dell: the checkout route (check --worktree) until the cutover" },
|
|
46
|
+
stage: { compiles: false, packages: false, source: "sealed", basis: "stage packs the sealed directory and its receipt into the publisher's draft release; it compiles nothing" },
|
|
47
|
+
// Row 7.06a: a unit stage builds the selected commit's snapshot (Linux now; Mac and Dell at the cutover commit, until which they take
|
|
48
|
+
// the clean-HEAD checkout route, decision 2 "Interim").
|
|
49
|
+
"unit:check": { compiles: false, packages: false, source: "snapshot", interim: "Mac and Dell: clean HEAD of the checkout until the cutover" },
|
|
50
|
+
"unit:compile": { compiles: true, packages: false, source: "snapshot", interim: "Mac and Dell: clean HEAD of the checkout until the cutover" },
|
|
51
|
+
"unit:package": { compiles: true, packages: true, source: "snapshot", interim: "Mac and Dell: clean HEAD of the checkout until the cutover" },
|
|
52
|
+
"unit:qualify": { compiles: false, packages: false, source: "snapshot", interim: "Mac and Dell: clean HEAD of the checkout until the cutover" },
|
|
53
|
+
"unit:release": { compiles: false, packages: false, source: "publisher", basis: "a unit release is dispatched to the trusted publisher (releaseUnit); it compiles nothing here" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Rows of `table` that compile or package without a snapshot source, a stated exclusion basis or a registered gap. */
|
|
57
|
+
export function stageSourceProblems(table = STAGE_SOURCES) {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const [stage, row] of Object.entries(table)) {
|
|
60
|
+
if (!row.compiles && !row.packages) continue;
|
|
61
|
+
if (row.source === "snapshot") continue;
|
|
62
|
+
if (typeof row.basis === "string" && row.basis && ["sealed", "publisher"].includes(row.source)) continue;
|
|
63
|
+
if (row.source === "checkout" && typeof row.gap === "string" && row.gap) continue;
|
|
64
|
+
out.push({ stage, problem: `${stage} compiles or packages and its source is ${JSON.stringify(row.source)} with no snapshot source, exclusion basis or registered gap` });
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The registered breaches in `table`: stages that compile or package from the checkout. */
|
|
70
|
+
export function stageSourceGaps(table = STAGE_SOURCES) {
|
|
71
|
+
return Object.entries(table).filter(([, row]) => row.source === "checkout" && (row.compiles || row.packages)).map(([stage]) => stage);
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
/** The stages a run to `target` covers, in order; null when `target` is not a stage. */
|
|
22
75
|
export function stagesTo(target) {
|
|
23
76
|
const i = STAGES.indexOf(target);
|
|
@@ -29,14 +82,18 @@ export function gateStateDir(commonDir) {
|
|
|
29
82
|
return path.join(commonDir, "rightkit-gate");
|
|
30
83
|
}
|
|
31
84
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Starts a door run: writes the run file and returns { nonce, file, env }. The caller removes it with endRun. `source` is the
|
|
87
|
+
* commit-only build's source split (source-split.mjs): { checkout, snapshot, commit, tree, appDir }, written into the run so a
|
|
88
|
+
* right-release child can learn which snapshot it may read; its env then names the checkout the run file lives under.
|
|
89
|
+
*/
|
|
90
|
+
export function startRun({ commonDir, commit, tree, stage, source, pid = process.pid, write = fs.writeFileSync, mkdir = fs.mkdirSync }) {
|
|
34
91
|
const nonce = randomBytes(16).toString("hex");
|
|
35
92
|
const dir = path.join(gateStateDir(commonDir), "runs");
|
|
36
93
|
mkdir(dir, { recursive: true });
|
|
37
94
|
const file = path.join(dir, `${nonce}.json`);
|
|
38
|
-
write(file, `${JSON.stringify({ pid, commit, tree, stage, startedAt: new Date().toISOString() })}\n`);
|
|
39
|
-
return { nonce, file, env: { [RUN_ENV]: nonce } };
|
|
95
|
+
write(file, `${JSON.stringify({ pid, commit, tree, stage, ...(source ? { source } : {}), startedAt: new Date().toISOString() })}\n`);
|
|
96
|
+
return { nonce, file, env: { [RUN_ENV]: nonce, ...(source ? { [CHECKOUT_ENV]: source.checkout } : {}) } };
|
|
40
97
|
}
|
|
41
98
|
|
|
42
99
|
export function endRun(run, rm = fs.rmSync) {
|
|
@@ -48,26 +105,45 @@ export function endRun(run, rm = fs.rmSync) {
|
|
|
48
105
|
* whose pid is gone, or that names another commit, is no run.
|
|
49
106
|
*/
|
|
50
107
|
export function requireDoorRun({ env = process.env, commonDir, commit, readFile = (f) => fs.readFileSync(f, "utf8"), alive = pidAlive, command = "this command" }) {
|
|
51
|
-
const nonce = env[RUN_ENV];
|
|
52
108
|
const refusal = `${command} runs only through the one door: rightkit-gate --to <check|compile|package|qualify|release> (it runs class A first, then every stage in order)`;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
if (commit && run.commit !== commit) return `${refusal}; the door run is for ${String(run.commit).slice(0, 12)}, HEAD is ${commit.slice(0, 12)}`;
|
|
109
|
+
const found = readDoorRun({ env, commonDir, readFile, alive });
|
|
110
|
+
if (found.problem === "none") return refusal;
|
|
111
|
+
if (found.problem === "unread") return `${refusal}; RK_GATE_RUN names no live door run`;
|
|
112
|
+
if (found.problem === "ended") return `${refusal}; the door run that launched it has ended`;
|
|
113
|
+
if (commit && found.run.commit !== commit) return `${refusal}; the door run is for ${String(found.run.commit).slice(0, 12)}, HEAD is ${commit.slice(0, 12)}`;
|
|
58
114
|
return null;
|
|
59
115
|
}
|
|
60
116
|
|
|
117
|
+
/** The live door run RK_GATE_RUN names: { run } or { problem: "none" | "unread" | "ended" }. */
|
|
118
|
+
export function readDoorRun({ env = process.env, commonDir, readFile = (f) => fs.readFileSync(f, "utf8"), alive = pidAlive }) {
|
|
119
|
+
const nonce = env[RUN_ENV];
|
|
120
|
+
if (!nonce || !/^[0-9a-f]{32}$/.test(nonce)) return { problem: "none" };
|
|
121
|
+
let run;
|
|
122
|
+
try { run = JSON.parse(readFile(path.join(gateStateDir(commonDir), "runs", `${nonce}.json`))); } catch { return { problem: "unread" }; }
|
|
123
|
+
if (!alive(run.pid)) return { problem: "ended" };
|
|
124
|
+
return { run };
|
|
125
|
+
}
|
|
126
|
+
|
|
61
127
|
function pidAlive(pid) {
|
|
62
128
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
63
129
|
try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; }
|
|
64
130
|
}
|
|
65
131
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The receipt file for `commit` and the app it was run for: a repository with one app at its root keeps `<commit>.json`; an app in a
|
|
134
|
+
* nested directory (CodeRight's apps/coderight-tauri and apps/coderight-ios, CutRight's apps/cutright, HeardRight's tauri-app-next)
|
|
135
|
+
* gets `<commit>--<dir, _ as _u, / as __>.json`, so two apps of one repository never share or overwrite a receipt (Astra, 2026-10-26).
|
|
136
|
+
*/
|
|
137
|
+
export function receiptKey(commit, app = ".") {
|
|
138
|
+
// Reversible: "_" becomes "_u" before "/" becomes "__", so apps/a__b (apps__a_u_ub) and apps/a/b (apps__a__b) never share a key.
|
|
139
|
+
return app && app !== "." ? `${commit}--${app.replaceAll("_", "_u").replaceAll("/", "__")}` : commit;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Reads the receipt for `commit` (and `app`), or null. */
|
|
143
|
+
export function readReceipt({ commonDir, commit, app = ".", readFile = (f) => fs.readFileSync(f, "utf8") }) {
|
|
68
144
|
try {
|
|
69
|
-
const r = JSON.parse(readFile(path.join(gateStateDir(commonDir), "receipts", `${commit}.json`)));
|
|
70
|
-
if (r.schema === RECEIPT_SCHEMA && r.commit === commit) return r;
|
|
145
|
+
const r = JSON.parse(readFile(path.join(gateStateDir(commonDir), "receipts", `${receiptKey(commit, app)}.json`)));
|
|
146
|
+
if (r.schema === RECEIPT_SCHEMA && r.commit === commit && (r.app ?? ".") === (app ?? ".")) return r;
|
|
71
147
|
} catch { /* none yet */ }
|
|
72
148
|
return null;
|
|
73
149
|
}
|
|
@@ -75,7 +151,7 @@ export function readReceipt({ commonDir, commit, readFile = (f) => fs.readFileSy
|
|
|
75
151
|
export function writeReceipt({ commonDir, receipt, write = fs.writeFileSync, mkdir = fs.mkdirSync }) {
|
|
76
152
|
const dir = path.join(gateStateDir(commonDir), "receipts");
|
|
77
153
|
mkdir(dir, { recursive: true });
|
|
78
|
-
write(path.join(dir, `${receipt.commit}.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
154
|
+
write(path.join(dir, `${receiptKey(receipt.commit, receipt.app ?? ".")}.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
79
155
|
}
|
|
80
156
|
|
|
81
157
|
/**
|
package/heavy-command.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, r
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn, spawnSync } from "./background-process.mjs";
|
|
6
|
+
import { spawnUnderOwnership } from "./build-ownership.mjs";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
8
9
|
|
|
@@ -208,7 +209,7 @@ export async function watchOwnedProcessTree(
|
|
|
208
209
|
|
|
209
210
|
function spawnOwnerWatcher(details) {
|
|
210
211
|
const payload = Buffer.from(JSON.stringify(details)).toString("base64url");
|
|
211
|
-
const watcher = spawn
|
|
212
|
+
const watcher = spawnUnderOwnership(spawn, process.execPath, [SELF, "--watch-owner-base64", payload], {
|
|
212
213
|
detached: true, env: process.env, stdio: "ignore", windowsHide: true,
|
|
213
214
|
});
|
|
214
215
|
watcher.once("error", () => {
|
|
@@ -335,7 +336,7 @@ export async function runHeavyCommand(args, { env = process.env } = {}) {
|
|
|
335
336
|
|
|
336
337
|
function spawnAttached(command, args, env, slot) {
|
|
337
338
|
return new Promise((resolve, reject) => {
|
|
338
|
-
const child = spawn
|
|
339
|
+
const child = spawnUnderOwnership(spawn, command, args, { cwd: process.cwd(), env, stdio: "inherit", windowsHide: true, detached: heavyChildDetached({ slot }) });
|
|
339
340
|
try {
|
|
340
341
|
if (child.pid && slot) slot.trackChild(child.pid, command);
|
|
341
342
|
} catch (error) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Retirement of the git hooks RightKit used to write (docs/GATE-CONTRACT.md: "Commit and push run no gate and no local hook
|
|
2
|
+
// checks"). `rightkit-gate install-hook` and `right-release hooks install` no longer write a pre-push gate, the forwarders
|
|
3
|
+
// that chained .git/hooks beside it, or the advisory ownership pre-commit step; they remove the ones an earlier release wrote.
|
|
4
|
+
//
|
|
5
|
+
// A file is ours only by its own text, never by its name or its place:
|
|
6
|
+
// - the gate hook : line 2 is the header every published hook carries ("# Mandatory local [check and] gate before every
|
|
7
|
+
// push (rightkit-gate install-hook). Never bypass with --no-verify.");
|
|
8
|
+
// - a forwarder : line 2 is the forwarder header naming the hook it forwards, and that hook is the file's own name
|
|
9
|
+
// (pre-commit.rightkit-foreign may forward `pre-commit` or itself);
|
|
10
|
+
// - the ownership step : the HOOK_MARKER sits in the first three lines.
|
|
11
|
+
// Anything else is a foreign hook and is never read for change, moved, rewritten or deleted. A symbolic link is never ours.
|
|
12
|
+
// One exception to "never moved": the foreign pre-commit the ownership step preserved as pre-commit.rightkit-foreign goes back to
|
|
13
|
+
// pre-commit when the ownership step that displaced it is removed.
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
export const HOOK_MARKER = "rightkit-ownership-precommit v1";
|
|
17
|
+
export const FOREIGN_PRE_COMMIT = "pre-commit.rightkit-foreign";
|
|
18
|
+
|
|
19
|
+
const GATE_HEADER = /^# Mandatory local (?:check and )?gate before every push \(rightkit-gate install-hook\)\. Never bypass with --no-verify\.$/;
|
|
20
|
+
const FORWARDER_HEADER = /^# rightkit-gate install-hook: core\.hooksPath=\.githooks hides \.git\/hooks, so run this repository's own (\S+) hook from there\.$/;
|
|
21
|
+
|
|
22
|
+
const lines = (text) => String(text).replace(/\r\n/g, "\n").split("\n");
|
|
23
|
+
|
|
24
|
+
/** What a hook file is, by its text and its name: "gate", "forwarder", "ownership" or null (foreign). `scripts` is false for a directory the gate never wrote into. */
|
|
25
|
+
export function hookKind(name, text, { scripts = true } = {}) {
|
|
26
|
+
const [shebang, second = "", third = ""] = lines(text);
|
|
27
|
+
if (shebang !== "#!/bin/sh") return null;
|
|
28
|
+
if ([second, third].some((line) => line.startsWith(`# ${HOOK_MARKER}`))) return "ownership";
|
|
29
|
+
if (!scripts) return null;
|
|
30
|
+
if (name === "pre-push" && GATE_HEADER.test(second)) return "gate";
|
|
31
|
+
const forwarded = FORWARDER_HEADER.exec(second)?.[1];
|
|
32
|
+
if (forwarded && (forwarded === name || (name === FOREIGN_PRE_COMMIT && forwarded === "pre-commit"))) return "forwarder";
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove the hooks RightKit wrote from `dir`. `ops` = { readdir, readFile, lstat, unlink, rename, rmdir } (fs-shaped). Returns
|
|
38
|
+
* { found, removed: [name], restored: [name], remaining: [name] }: `found` is false when `dir` cannot be listed, `remaining` is every
|
|
39
|
+
* file left in it (foreign hooks), and an emptied directory is removed (`removedDir`).
|
|
40
|
+
*/
|
|
41
|
+
export function retireHooksIn(dir, ops, { scripts = true, removeEmptyDir = scripts } = {}) {
|
|
42
|
+
let names;
|
|
43
|
+
try { names = [...ops.readdir(dir)].map(String).sort(); } catch { return { found: false, removed: [], restored: [], remaining: [], removedDir: false }; }
|
|
44
|
+
const removed = []; const restored = []; const kinds = new Map();
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const file = path.join(dir, name);
|
|
47
|
+
let kind = null;
|
|
48
|
+
try {
|
|
49
|
+
const stat = ops.lstat(file);
|
|
50
|
+
if (stat.isFile() && !stat.isSymbolicLink()) kind = hookKind(name, ops.readFile(file), { scripts });
|
|
51
|
+
} catch { kind = null; }
|
|
52
|
+
kinds.set(name, kind);
|
|
53
|
+
if (!kind) continue;
|
|
54
|
+
try { ops.unlink(file); removed.push(name); } catch { kinds.set(name, null); }
|
|
55
|
+
}
|
|
56
|
+
// The ownership step displaced a foreign pre-commit into pre-commit.rightkit-foreign: it goes back where git runs it.
|
|
57
|
+
if (kinds.get("pre-commit") === "ownership" && removed.includes("pre-commit") && names.includes(FOREIGN_PRE_COMMIT) && kinds.get(FOREIGN_PRE_COMMIT) === null) {
|
|
58
|
+
try { ops.rename(path.join(dir, FOREIGN_PRE_COMMIT), path.join(dir, "pre-commit")); restored.push("pre-commit"); } catch { /* left where it is, still foreign */ }
|
|
59
|
+
}
|
|
60
|
+
let left = names.filter((name) => !removed.includes(name));
|
|
61
|
+
if (restored.length) left = [...left.filter((name) => name !== FOREIGN_PRE_COMMIT), "pre-commit"].sort();
|
|
62
|
+
let removedDir = false;
|
|
63
|
+
if (removeEmptyDir && removed.length && !left.length) { try { ops.rmdir(dir); removedDir = true; } catch { /* not empty after all */ } }
|
|
64
|
+
return { found: true, removed, restored, remaining: left, removedDir };
|
|
65
|
+
}
|
package/init-command.mjs
CHANGED
|
@@ -253,7 +253,7 @@ export function initTemplate(root, options) {
|
|
|
253
253
|
|
|
254
254
|
/** What a person does after init: the steps no command can do for them, in order. */
|
|
255
255
|
export const NEXT_STEPS = Object.freeze([
|
|
256
|
-
"pnpm install (installs the pinned packages and, through `prepare`, the
|
|
256
|
+
"pnpm install (installs the pinned packages and, through `prepare`, the agent hook; no git hook)",
|
|
257
257
|
"right-release doctor (the build's own checks for this app, as rows)",
|
|
258
258
|
"set the app's public legal host in legal-snapshot.config.json, replace legal/PRODUCT_SCHEDULE.md and legal/PRIVACY_NOTICE.md with reviewed text, then `pnpm exec legal-snapshot`",
|
|
259
259
|
"register the repository on each host: add it to packages/release/ownership-repositories.json in RightKit (and its byte copy under packages/build-control-policy), then run `right-release ownership trust-refresh` on the Mac and the Dell (a person with administrator rights; an agent cannot)",
|
package/install-app.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import os from "node:os";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
14
|
|
|
15
|
+
const LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
15
16
|
const SIGTERM_WAIT_MS = 15_000;
|
|
16
17
|
const POLL_MS = 250;
|
|
17
18
|
|
|
@@ -632,12 +633,20 @@ export async function installApp(opts, deps = {}) {
|
|
|
632
633
|
swap = null;
|
|
633
634
|
event("replaced", { installed, previous: null, swap: swapMode });
|
|
634
635
|
|
|
636
|
+
// A dev install from a build folder (--app) leaves that product registered with Launch Services, which then resolves the bundle
|
|
637
|
+
// id to it instead of the installed copy (rightkit#1225): unregister it so only the installed copy is known.
|
|
638
|
+
if (opts.app && path.resolve(sourceApp) !== path.resolve(installed)) {
|
|
639
|
+
try { await run(LSREGISTER, ["-u", sourceApp]); event("unregistered", { path: sourceApp }); }
|
|
640
|
+
catch (error) { event("unregister-failed", { path: sourceApp, reason: errText(error) }); }
|
|
641
|
+
}
|
|
642
|
+
|
|
635
643
|
// Relaunch hidden and in the background only.
|
|
636
644
|
const relaunch = opts.relaunch === null ? wasRunning : opts.relaunch;
|
|
637
645
|
if (relaunch) {
|
|
638
646
|
failedStage = "relaunch";
|
|
639
|
-
|
|
640
|
-
|
|
647
|
+
// By the installed path, never by bundle id: Launch Services may resolve the id to another registered copy (rightkit#1225).
|
|
648
|
+
await run("/usr/bin/open", ["-g", "-j", installed]);
|
|
649
|
+
event("relaunched", { bundleId, installed });
|
|
641
650
|
}
|
|
642
651
|
const result = { ok: true, app: appName, installed, bundleId, teamId: newTeam, previous: null, swap: swapMode, relaunched: relaunch, ...(dev ? { dev: true } : {}) };
|
|
643
652
|
event("done", result);
|