@geonosis/release 2.4.3 → 2.5.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # @geonosis/release
2
2
 
3
+ ## 2.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 7f24da6: `geonosis-release smoke baseline`/`smoke run --rc-dir` now write, beside the tarballs `packRc`
8
+ makes, a `candidate/` scratch package pinned to the candidate's own `@geonosis/cli` tarball (an
9
+ override for every packed name, in `package.json` and — pnpm actually reads it from —
10
+ `pnpm-workspace.yaml`), and print `candidate CLI: (cd <dir>/candidate && pnpm install
11
+ --ignore-scripts && pnpm exec geonosis update --registry <dir> --to <version> …)` (#389). A
12
+ consumer at 2.4.3 cannot run `update --registry <dir>` at all (it parses the directory as a URL),
13
+ and a bare tarball install cannot resolve its own siblings either (#387) — the adoption story had
14
+ no first step. `docs/releasing.md`'s RC section carries the line as that first step.
15
+ - 7f24da6: A machine-wide heavy budget, two slots, held by every tool that spends one (#385): measured load 48
16
+ with during.day's Stop-hook tier, this kit's Stop-hook tier, its ratchet and an engineer's suite all
17
+ running at once — nothing held the two-slot budget across the two repos, and three kit tests read
18
+ red under it that were green alone. `@geonosis/ratchet` ships `takeSlot`/`heavySlotDir` (a lock
19
+ directory under `os.tmpdir()/geonosis-heavy`, two slot files each holding `{ pid, command, cwd,
20
+ startedAt }`, a dead holder reaped on the next take, a taker that finds both held waiting once
21
+ before refusing naming them); `geonosis-verify` (each tier run) and `geonosis-ratchet` (the run, not
22
+ `--prove`'s inner unit) take a slot for the run's duration. `@geonosis/release`'s `smoke
23
+ baseline`/`smoke run` take one too, from their own copy (this layer imports nothing, same posture as
24
+ `envelope.ts`). The plugin's Stop hook reads a fresh green `gate-report.<tier>.json` — its own
25
+ recorded commit equal to the current `HEAD` with a clean tree — and reports PASS without re-running;
26
+ otherwise it peeks the same two slots (never claims one itself, or the verify child it is about to
27
+ spawn would be racing against its own reservation) and blocks naming both holders rather than
28
+ running a third tier.
29
+ - 7f24da6: The smoke's run copy sits where nothing is above it (#373's true root): it used to land at
30
+ `<root>/.geonosis/consumer-snapshots/<name>/run`, an ancestor of which (`<root>` itself) carries
31
+ this kit's own `node_modules` — so a package a consumer never installed resolved to the kit's own
32
+ workspace copy, two directories up, and every tool run inside the copy (the doctor included) read
33
+ it as theirs. The copy now lands under the OS temp directory instead, as `packages/verify`'s prove
34
+ copy does; its path is printed as `run copy: <path>` and it is removed on a passing run and kept
35
+ (for reading) on a failing one.
36
+ - 7f24da6: The smoke runner owns its child's lifetime (#368): two `pnpm install` processes from a 2026-09-03
37
+ smoke were still alive two days later, holding pnpm's store lock every other install on the machine
38
+ then waits on. Every install and phase now spawns in its own process group with a timeout (default
39
+ 20 minutes, `release.smoke.timeoutMs` overrides it) and is killed by the WHOLE group — SIGTERM, then
40
+ SIGKILL after a grace period — on timeout; a timed-out phase is recorded as a fail with the tail
41
+ `timed out after <n> ms — killed`.
42
+
43
+ ## 2.5.0
44
+
45
+ ### Patch Changes
46
+
47
+ - 6b095e8: Two smoke findings that were true sentences about the wrong thing. (#372) A phase whose own tail
48
+ is a git refusal inside a snapshot's run copy — its `.git` is a bare `ownRepository` init with no
49
+ commit, not the consumer's history — is now `unmeasurable` rather than a plain exit code, so a
50
+ gate that asks git is reported `NOT MEASURED` and never "already failing at their versions". (#373)
51
+ The baseline's install is now frozen to the consumer's own lockfile whenever nothing rewrote a
52
+ manifest under it, so it can no longer install a dependency the consumer never declared; a frozen
53
+ install that refuses exits 2 with the manager's own tail rather than silently installing a tree
54
+ the consumer does not have.
55
+
3
56
  ## 2.4.3
4
57
 
5
58
  ## 2.4.2
@@ -9,11 +9,19 @@ import { dirname, join } from 'node:path'
9
9
  import { fileURLToPath } from 'node:url'
10
10
 
11
11
  const here = dirname(fileURLToPath(import.meta.url))
12
- const newest = (dir) =>
12
+ // tsup never reads a test file, so one counting as source made every bin refuse a build that had
13
+ // not gone stale (#399).
14
+ const NOT_SOURCE = (name) =>
15
+ name.endsWith('.test.ts') ||
16
+ name.endsWith('.test.tsx') ||
17
+ name === '__tests__' ||
18
+ name === '__fixtures__'
19
+ const newest = (dir, skip) =>
13
20
  existsSync(dir)
14
21
  ? readdirSync(dir, { withFileTypes: true }).reduce((most, entry) => {
22
+ if (skip !== undefined && skip(entry.name)) return most
15
23
  const at = join(dir, entry.name)
16
- return Math.max(most, entry.isDirectory() ? newest(at) : statSync(at).mtimeMs)
24
+ return Math.max(most, entry.isDirectory() ? newest(at, skip) : statSync(at).mtimeMs)
17
25
  }, 0)
18
26
  : 0
19
27
  const src = join(here, '..', 'src')
@@ -30,7 +38,7 @@ if (existsSync(src)) {
30
38
  )
31
39
  process.exit(2)
32
40
  }
33
- if (newest(src) > built) {
41
+ if (newest(src, NOT_SOURCE) > built) {
34
42
  process.stderr.write(
35
43
  'geonosis-release: dist is older than src — run pnpm build before trusting this bin.\n',
36
44
  )
@@ -97,7 +97,7 @@ var parseSchema = (value2) => {
97
97
  sessionVariables: strings(value2["sessionVariables"], "release.schema.sessionVariables")
98
98
  };
99
99
  };
100
- var SMOKE_KEYS = "expected { exclude?, snapshots? }";
100
+ var SMOKE_KEYS = "expected { exclude?, snapshots?, timeoutMs? }";
101
101
  var SNAPSHOT_KEYS = "expected { commands?, install?, name }";
102
102
  var SMOKE_PHASES = PHASES;
103
103
  var parseCommands = (value2, at) => {
@@ -146,10 +146,11 @@ var oneSnapshot = (value2, index) => {
146
146
  name
147
147
  };
148
148
  };
149
+ var KNOWN_SMOKE_KEYS = /* @__PURE__ */ new Set(["exclude", "snapshots", "timeoutMs"]);
149
150
  var parseSmoke = (value2) => {
150
151
  if (value2 === void 0) return { exclude: [], snapshots: [] };
151
152
  if (!isRecord(value2)) throw new CannotRun(`release.smoke must be an object \u2014 ${SMOKE_KEYS}`);
152
- const unknown = Object.keys(value2).find((key) => key !== "exclude" && key !== "snapshots");
153
+ const unknown = Object.keys(value2).find((key) => !KNOWN_SMOKE_KEYS.has(key));
153
154
  if (unknown !== void 0) {
154
155
  throw new CannotRun(`release.smoke.${unknown} is not a key it reads \u2014 ${SMOKE_KEYS}`);
155
156
  }
@@ -157,9 +158,14 @@ var parseSmoke = (value2) => {
157
158
  if (snapshots !== void 0 && !Array.isArray(snapshots)) {
158
159
  throw new CannotRun(`release.smoke.snapshots must be a list \u2014 ${SNAPSHOT_KEYS}, per entry`);
159
160
  }
161
+ const timeoutMs = value2["timeoutMs"];
162
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || timeoutMs <= 0)) {
163
+ throw new CannotRun("release.smoke.timeoutMs must be a positive number of milliseconds");
164
+ }
160
165
  return {
161
166
  exclude: strings(value2["exclude"], "release.smoke.exclude"),
162
- snapshots: (snapshots ?? []).map(oneSnapshot)
167
+ snapshots: (snapshots ?? []).map(oneSnapshot),
168
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
163
169
  };
164
170
  };
165
171
  var parseReleaseConfig = (raw) => {
@@ -366,9 +372,18 @@ var askRegistry = async ({
366
372
  };
367
373
 
368
374
  // src/published.ts
369
- import { readdirSync, readFileSync as readFileSync3 } from "fs";
375
+ import { readFileSync as readFileSync3 } from "fs";
376
+
377
+ // src/manifests.ts
378
+ import { readdirSync } from "fs";
370
379
  import { join as join2, relative, sep } from "path";
371
- var NEVER_WALKED = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
380
+ var NEVER_WALKED = /* @__PURE__ */ new Set([
381
+ "build",
382
+ "coverage",
383
+ "dist",
384
+ "node_modules",
385
+ "storybook-static"
386
+ ]);
372
387
  var pathOf = (root, path) => relative(root, path).split(sep).join("/");
373
388
  var manifestsUnder = (root) => {
374
389
  const found = [];
@@ -391,6 +406,8 @@ var manifestsUnder = (root) => {
391
406
  walk(root);
392
407
  return found;
393
408
  };
409
+
410
+ // src/published.ts
394
411
  var censusOf = (root) => {
395
412
  const excused = [];
396
413
  const locals = [];
@@ -1178,16 +1195,16 @@ var declaredIn = (config) => {
1178
1195
  routes: listed.map((route) => isRecord3(route) ? route["pattern"] : route).filter((pattern) => typeof pattern === "string").toSorted()
1179
1196
  };
1180
1197
  };
1181
- var readWrangler = (root, relative6, env) => {
1182
- const path = resolve4(root, relative6);
1198
+ var readWrangler = (root, relative4, env) => {
1199
+ const path = resolve4(root, relative4);
1183
1200
  let parsed;
1184
1201
  try {
1185
1202
  const source = readFileSync6(path, "utf8");
1186
- parsed = relative6.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
1203
+ parsed = relative4.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
1187
1204
  } catch (error) {
1188
- throw new CannotRun(`${relative6} could not be read: ${error.message}`);
1205
+ throw new CannotRun(`${relative4} could not be read: ${error.message}`);
1189
1206
  }
1190
- if (!isRecord3(parsed)) throw new CannotRun(`${relative6} is not a wrangler configuration`);
1207
+ if (!isRecord3(parsed)) throw new CannotRun(`${relative4} is not a wrangler configuration`);
1191
1208
  const environments = parsed["env"];
1192
1209
  const block = env !== void 0 && isRecord3(environments) && isRecord3(environments[env]) ? environments[env] : parsed;
1193
1210
  return declaredIn(block);
@@ -1987,14 +2004,28 @@ var formatSchema = (report) => {
1987
2004
  };
1988
2005
 
1989
2006
  // src/smoke-run.ts
1990
- import { spawnSync as spawnSync4 } from "child_process";
1991
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1992
- import { join as join7, relative as relative4, resolve as resolve9 } from "path";
2007
+ import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
2008
+ import {
2009
+ existsSync as existsSync7,
2010
+ mkdirSync as mkdirSync3,
2011
+ mkdtempSync as mkdtempSync2,
2012
+ readdirSync as readdirSync5,
2013
+ readFileSync as readFileSync10,
2014
+ rmSync as rmSync3,
2015
+ writeFileSync as writeFileSync4
2016
+ } from "fs";
2017
+ import { tmpdir as tmpdir2 } from "os";
2018
+ import { join as join7, resolve as resolve9 } from "path";
1993
2019
  var BASELINE_FILE = "baseline.json";
1994
2020
  var INSTALL = {
1995
2021
  bun: ["install"],
1996
2022
  pnpm: ["install", "--no-frozen-lockfile"]
1997
2023
  };
2024
+ var FROZEN_INSTALL = {
2025
+ bun: ["install", "--frozen-lockfile"],
2026
+ pnpm: ["install", "--frozen-lockfile"]
2027
+ };
2028
+ var defaultInstallLine = (manager, frozen) => `${manager} ${(frozen ? FROZEN_INSTALL : INSTALL)[manager].join(" ")}`;
1998
2029
  var MANAGER_CONFIG = [".npmrc", "bunfig.toml", "pnpm-workspace.yaml"];
1999
2030
  var configBytesOf = (work) => Object.fromEntries(
2000
2031
  MANAGER_CONFIG.filter((file) => existsSync7(join7(work, file))).map((file) => [
@@ -2093,7 +2124,7 @@ var TOP_LEVEL = /^\S/;
2093
2124
  var KEYED = /^\s+["']?(.+?)["']?\s*:/;
2094
2125
  var pinsInWorkspaceYaml = (work, pins) => {
2095
2126
  const at = join7(work, "pnpm-workspace.yaml");
2096
- const written = Object.entries(pins).map(([name, spec]) => ` '${name}': '${spec}'`);
2127
+ const written = Object.entries(pins).filter((entry) => entry[1] !== void 0).map(([name, spec]) => ` '${name}': '${spec}'`);
2097
2128
  const lines2 = existsSync7(at) ? readFileSync10(at, "utf8").split("\n") : [];
2098
2129
  const kept = [];
2099
2130
  let inside = false;
@@ -2106,10 +2137,10 @@ var pinsInWorkspaceYaml = (work, pins) => {
2106
2137
  continue;
2107
2138
  }
2108
2139
  const key = inside ? KEYED.exec(line)?.[1] : void 0;
2109
- if (key !== void 0 && pins[key] !== void 0) continue;
2140
+ if (key !== void 0 && Object.hasOwn(pins, key)) continue;
2110
2141
  kept.push(line);
2111
2142
  }
2112
- const body = found ? kept : [...kept, OVERRIDES, ...written, ""];
2143
+ const body = found ? kept : written.length === 0 ? kept : [...kept, OVERRIDES, ...written, ""];
2113
2144
  writeFileSync4(at, body.join("\n"));
2114
2145
  };
2115
2146
  var pinsInManifest = (work, pins) => {
@@ -2117,13 +2148,42 @@ var pinsInManifest = (work, pins) => {
2117
2148
  if (!existsSync7(at)) return;
2118
2149
  const manifest = readJson(at);
2119
2150
  if (!isRecord5(manifest)) return;
2120
- manifest["overrides"] = {
2121
- ...isRecord5(manifest["overrides"]) ? manifest["overrides"] : {},
2122
- ...pins
2123
- };
2151
+ const next = isRecord5(manifest["overrides"]) ? { ...manifest["overrides"] } : {};
2152
+ for (const [name, spec] of Object.entries(pins)) {
2153
+ if (spec === void 0) delete next[name];
2154
+ else next[name] = spec;
2155
+ }
2156
+ manifest["overrides"] = next;
2124
2157
  writeFileSync4(at, `${JSON.stringify(manifest, void 0, 2)}
2125
2158
  `);
2126
2159
  };
2160
+ var CANDIDATE_CLI = "@geonosis/cli";
2161
+ var candidateDirIn = (into, packed) => {
2162
+ const cli = packed.find((one) => one.name === CANDIDATE_CLI);
2163
+ if (cli === void 0) return void 0;
2164
+ const dir = join7(into, "candidate");
2165
+ mkdirSync3(dir, { recursive: true });
2166
+ const pins = Object.fromEntries(packed.map((one) => [one.name, `file:${one.tarball}`]));
2167
+ writeFileSync4(
2168
+ join7(dir, "package.json"),
2169
+ `${JSON.stringify(
2170
+ {
2171
+ devDependencies: { [CANDIDATE_CLI]: `file:${cli.tarball}` },
2172
+ name: "geonosis-candidate",
2173
+ overrides: pins,
2174
+ private: true
2175
+ },
2176
+ void 0,
2177
+ 2
2178
+ )}
2179
+ `
2180
+ );
2181
+ pinsInWorkspaceYaml(dir, pins);
2182
+ return {
2183
+ line: `candidate CLI: (cd ${dir} && pnpm install --ignore-scripts && pnpm exec geonosis update --registry ${into} --to ${cli.version} \u2026)`,
2184
+ path: dir
2185
+ };
2186
+ };
2127
2187
  var overrideEveryCopy = (work, manager, packed) => {
2128
2188
  if (packed.length === 0) return;
2129
2189
  const pins = Object.fromEntries(packed.map((one) => [one.name, `file:${one.tarball}`]));
@@ -2155,9 +2215,62 @@ var rewriteManifests = (tree, manifests, packed) => {
2155
2215
  }
2156
2216
  return swapped;
2157
2217
  };
2158
- var shellRun = (command, cwd, env) => {
2159
- const done = spawnSync4(command, { cwd, encoding: "utf8", env, shell: true });
2160
- return { code: done.status ?? -1, output: `${done.stdout ?? ""}${done.stderr ?? ""}` };
2218
+ var DEFAULT_PHASE_TIMEOUT_MS = 20 * 6e4;
2219
+ var KILL_GRACE_MS = 5e3;
2220
+ var groupsThisProcessOwns = /* @__PURE__ */ new Set();
2221
+ var killGroup = (pid, signal) => {
2222
+ try {
2223
+ process.kill(-pid, signal);
2224
+ } catch {
2225
+ }
2226
+ };
2227
+ var ownsItsExit = false;
2228
+ var ownEveryGroupOnExit = () => {
2229
+ if (ownsItsExit) return;
2230
+ ownsItsExit = true;
2231
+ const sweep = () => {
2232
+ for (const pid of groupsThisProcessOwns) killGroup(pid, "SIGKILL");
2233
+ };
2234
+ process.once("exit", sweep);
2235
+ process.once("SIGINT", sweep);
2236
+ process.once("SIGTERM", sweep);
2237
+ };
2238
+ var shellRun = (command, cwd, env, timeoutMs = DEFAULT_PHASE_TIMEOUT_MS) => {
2239
+ ownEveryGroupOnExit();
2240
+ return new Promise((settle) => {
2241
+ const child = spawn2(command, {
2242
+ cwd,
2243
+ detached: true,
2244
+ env,
2245
+ shell: true,
2246
+ stdio: ["ignore", "pipe", "pipe"]
2247
+ });
2248
+ const pid = child.pid;
2249
+ if (pid !== void 0) groupsThisProcessOwns.add(pid);
2250
+ let output = "";
2251
+ child.stdout?.on("data", (chunk) => {
2252
+ output += chunk.toString();
2253
+ });
2254
+ child.stderr?.on("data", (chunk) => {
2255
+ output += chunk.toString();
2256
+ });
2257
+ let timedOut = false;
2258
+ let graceTimer;
2259
+ const deadline = setTimeout(() => {
2260
+ timedOut = true;
2261
+ if (pid === void 0) return;
2262
+ killGroup(pid, "SIGTERM");
2263
+ graceTimer = setTimeout(() => killGroup(pid, "SIGKILL"), KILL_GRACE_MS);
2264
+ }, timeoutMs);
2265
+ const finish = (code) => {
2266
+ clearTimeout(deadline);
2267
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
2268
+ if (pid !== void 0) groupsThisProcessOwns.delete(pid);
2269
+ settle({ code: code ?? -1, output, timedOut });
2270
+ };
2271
+ child.on("error", () => finish(-1));
2272
+ child.on("close", (code) => finish(code));
2273
+ });
2161
2274
  };
2162
2275
  var INTERESTING = /\b(error|Error|ERROR|FAIL|failed|refused|✗|✘)\b/;
2163
2276
  var quotable = (output) => {
@@ -2166,14 +2279,30 @@ var quotable = (output) => {
2166
2279
  return (named.length > 0 ? named : lines2.slice(-10)).slice(0, 20).map((one) => one.slice(0, 300));
2167
2280
  };
2168
2281
  var commandLine = (command, manager) => "run" in command ? `${manager} run ${command.run}` : "exec" in command ? command.exec : "";
2169
- var runPhase = (phase, command, cwd, manager) => {
2282
+ var GIT_REFUSAL = /fatal: not a git repository|Command failed: git |git .+ failed/;
2283
+ var looksLikeAGitRefusal = (output) => output.split("\n").some((line) => GIT_REFUSAL.test(line));
2284
+ var hasNoCommit = (dir) => spawnSync4("git", ["-C", dir, "rev-parse", "--verify", "-q", "HEAD"], { encoding: "utf8" }).status !== 0;
2285
+ var NOT_MEASURED_REASON = "this gate asks git and a snapshot has none";
2286
+ var runPhase = async (phase, command, cwd, manager, timeoutMs = DEFAULT_PHASE_TIMEOUT_MS) => {
2170
2287
  if ("why" in command) return { phase, why: command.why };
2171
2288
  const line = commandLine(command, manager);
2172
2289
  const env = {
2173
2290
  ...process.env,
2174
2291
  PATH: `${join7(cwd, "node_modules/.bin")}:${process.env["PATH"] ?? ""}`
2175
2292
  };
2176
- const { code, output } = shellRun(line, cwd, env);
2293
+ const { code, output, timedOut } = await shellRun(line, cwd, env, timeoutMs);
2294
+ if (timedOut) {
2295
+ return {
2296
+ code: 124,
2297
+ command: line,
2298
+ lines: [...quotable(output), `timed out after ${timeoutMs} ms \u2014 killed`],
2299
+ ok: false,
2300
+ phase
2301
+ };
2302
+ }
2303
+ if (code !== 0 && looksLikeAGitRefusal(output) && hasNoCommit(cwd)) {
2304
+ return { command: line, phase, unmeasurable: NOT_MEASURED_REASON };
2305
+ }
2177
2306
  return { code, command: line, lines: quotable(output), ok: code === 0, phase };
2178
2307
  };
2179
2308
  var commandsToRun = (record, declared) => Object.fromEntries(
@@ -2182,46 +2311,70 @@ var commandsToRun = (record, declared) => Object.fromEntries(
2182
2311
  return [phase, named === void 0 ? record.commands[phase] : { exec: named }];
2183
2312
  })
2184
2313
  );
2185
- var smokeOver = (input) => {
2314
+ var smokeOver = async (input) => {
2315
+ const timeoutMs = readReleaseConfig(input.root).smoke?.timeoutMs ?? DEFAULT_PHASE_TIMEOUT_MS;
2186
2316
  const record = readSnapshot(input.root, input.name);
2187
2317
  const at = snapshotDir(input.root, input.name);
2188
- const work = join7(at, "run");
2189
- rmSync3(work, { force: true, recursive: true });
2190
- copyInto(join7(at, "tree"), work, new Set(EXCLUDED_DIRS));
2191
- ownRepository(work);
2192
- const packed = input.rc === void 0 ? [] : packRc(resolve9(input.rc), join7(at, "rc"));
2193
- const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve9(input.rc), packed);
2194
- const swapped = rewriteManifests(
2195
- work,
2196
- record.manifests.map((one) => one.path),
2197
- packed
2198
- );
2199
- if (input.rc !== void 0 && swapped.length === 0) {
2200
- throw new CannotRun(
2201
- `no manifest in "${input.name}" depends on anything ${input.rc} packs \u2014 this run would have installed the published versions and called them the release candidate`
2318
+ const work = mkdtempSync2(join7(tmpdir2(), "geonosis-smoke-"));
2319
+ try {
2320
+ copyInto(join7(at, "tree"), work, new Set(EXCLUDED_DIRS));
2321
+ ownRepository(work);
2322
+ const rcDir = join7(at, "rc");
2323
+ const packed = input.rc === void 0 ? [] : packRc(resolve9(input.rc), rcDir);
2324
+ const candidate = input.rc === void 0 ? void 0 : candidateDirIn(rcDir, packed)?.line;
2325
+ const findings = input.rc === void 0 ? [] : promisedButNotPacked(resolve9(input.rc), packed);
2326
+ const swapped = rewriteManifests(
2327
+ work,
2328
+ record.manifests.map((one) => one.path),
2329
+ packed
2202
2330
  );
2203
- }
2204
- overrideEveryCopy(work, record.manager, packed);
2205
- const install = input.install ?? `${record.manager} ${INSTALL[record.manager].join(" ")}`;
2206
- const configBefore = configBytesOf(work);
2207
- const installed = shellRun(install, work, process.env);
2208
- if (installed.code !== 0) {
2209
- throw new CannotRun(
2210
- `\`${install}\` exited ${installed.code} in the snapshot \u2014 the release was never installed, so nothing below it was measured:
2331
+ if (input.rc !== void 0 && swapped.length === 0) {
2332
+ throw new CannotRun(
2333
+ `no manifest in "${input.name}" depends on anything ${input.rc} packs \u2014 this run would have installed the published versions and called them the release candidate`
2334
+ );
2335
+ }
2336
+ overrideEveryCopy(work, record.manager, packed);
2337
+ const frozen = input.rc === void 0;
2338
+ const install = input.install ?? defaultInstallLine(record.manager, frozen);
2339
+ const configBefore = configBytesOf(work);
2340
+ const installed = await shellRun(install, work, process.env, timeoutMs);
2341
+ if (installed.timedOut) {
2342
+ throw new CannotRun(
2343
+ `\`${install}\` timed out after ${timeoutMs} ms in the snapshot and was killed \u2014 nothing below it was measured:
2211
2344
  ${quotable(installed.output).join("\n")}`
2212
- );
2213
- }
2214
- for (const file of editedByInstall(configBefore, configBytesOf(work))) {
2215
- findings.push(
2216
- `\`${install}\` rewrote ${file} in the copy \u2014 every phase below ran against a tree that is no longer byte-for-byte "${input.name}", and the same install in their own tree would edit theirs`
2217
- );
2345
+ );
2346
+ }
2347
+ if (installed.code !== 0) {
2348
+ if (frozen) {
2349
+ throw new CannotRun(
2350
+ `\`${install}\` exited ${installed.code} in the snapshot \u2014 the baseline is the consumer's own tree and their lockfile does not describe it \u2014 record it after they install:
2351
+ ${quotable(installed.output).join("\n")}`
2352
+ );
2353
+ }
2354
+ throw new CannotRun(
2355
+ `\`${install}\` exited ${installed.code} in the snapshot \u2014 the release was never installed, so nothing below it was measured:
2356
+ ${quotable(installed.output).join("\n")}`
2357
+ );
2358
+ }
2359
+ for (const file of editedByInstall(configBefore, configBytesOf(work))) {
2360
+ findings.push(
2361
+ `\`${install}\` rewrote ${file} in the copy \u2014 every phase below ran against a tree that is no longer byte-for-byte "${input.name}", and the same install in their own tree would edit theirs`
2362
+ );
2363
+ }
2364
+ const commands = commandsToRun(record, input.commands ?? {});
2365
+ const outcomes = [];
2366
+ for (const phase of PHASES) {
2367
+ outcomes.push(await runPhase(phase, commands[phase], work, record.manager, timeoutMs));
2368
+ }
2369
+ if (outcomes.every((one) => !("ok" in one) || one.ok)) {
2370
+ rmSync3(work, { force: true, recursive: true });
2371
+ }
2372
+ return { ...candidate === void 0 ? {} : { candidate }, findings, outcomes, packed, work };
2373
+ } catch (error) {
2374
+ const message = error instanceof Error ? error.message : String(error);
2375
+ throw new CannotRun(`${message}
2376
+ run copy: ${work}`);
2218
2377
  }
2219
- const commands = commandsToRun(record, input.commands ?? {});
2220
- return {
2221
- findings,
2222
- outcomes: PHASES.map((phase) => runPhase(phase, commands[phase], work, record.manager)),
2223
- packed
2224
- };
2225
2378
  };
2226
2379
  var ownRepository = (work) => {
2227
2380
  const done = spawnSync4("git", ["init", "-q"], { cwd: work });
@@ -2232,13 +2385,15 @@ var ownRepository = (work) => {
2232
2385
  }
2233
2386
  };
2234
2387
  var baselinePath = (root, name) => join7(snapshotDir(root, name), BASELINE_FILE);
2235
- var recordBaseline = (input) => {
2236
- const { outcomes, packed } = smokeOver(input);
2388
+ var recordBaseline = async (input) => {
2389
+ const { candidate, outcomes, packed, work } = await smokeOver(input);
2237
2390
  const baseline = {
2238
2391
  at: (/* @__PURE__ */ new Date()).toISOString(),
2392
+ ...candidate === void 0 ? {} : { candidate },
2239
2393
  packed: packed.map((one) => ({ name: one.name, version: one.version })),
2240
2394
  phases: outcomes,
2241
- rc: input.rc === void 0 ? "the versions the tree names" : resolve9(input.rc)
2395
+ rc: input.rc === void 0 ? "the versions the tree names" : resolve9(input.rc),
2396
+ ...existsSync7(work) ? { runCopy: work } : {}
2242
2397
  };
2243
2398
  writeFileSync4(baselinePath(input.root, input.name), `${JSON.stringify(baseline, void 0, 2)}
2244
2399
  `);
@@ -2269,6 +2424,7 @@ var compareToBaseline = (name, baseline, outcomes, findings) => {
2269
2424
  continue;
2270
2425
  }
2271
2426
  if ("why" in now) continue;
2427
+ if ("unmeasurable" in now || "unmeasurable" in then) continue;
2272
2428
  if ("why" in then) {
2273
2429
  refused.push({
2274
2430
  phase: now.phase,
@@ -2295,10 +2451,14 @@ var compareToBaseline = (name, baseline, outcomes, findings) => {
2295
2451
  refused
2296
2452
  };
2297
2453
  };
2298
- var runSmoke = (input) => {
2454
+ var runSmoke = async (input) => {
2299
2455
  const baseline = readBaseline(input.root, input.name);
2300
- const { findings, outcomes } = smokeOver(input);
2301
- return compareToBaseline(input.name, baseline, outcomes, findings);
2456
+ const { candidate, findings, outcomes, work } = await smokeOver(input);
2457
+ return {
2458
+ ...compareToBaseline(input.name, baseline, outcomes, findings),
2459
+ ...candidate === void 0 ? {} : { candidate },
2460
+ ...existsSync7(work) ? { runCopy: work } : {}
2461
+ };
2302
2462
  };
2303
2463
  var CONSUMER_TREES_FILE = ".geonosis/consumer-trees.local.json";
2304
2464
  var localTrees = (root) => {
@@ -2336,28 +2496,31 @@ var dating = (root, name) => {
2336
2496
  if (!("sha" in now) || now.sha === commit.sha) return void 0;
2337
2497
  return { stale: `STALE ${name}: recorded at ${commit.sha}, the tree is at ${now.sha}` };
2338
2498
  };
2339
- var sweepSmoke = (input) => {
2499
+ var sweepSmoke = async (input) => {
2340
2500
  const absent = input.absent ?? [];
2341
2501
  if (input.skipMissing) {
2342
2502
  const owed = unrecordedButHere(input.root, input.wanted, absent);
2343
2503
  if (owed.length > 0) throw new CannotRun(owed.join("\n"));
2344
2504
  }
2345
- const entries = input.wanted.map((one) => {
2505
+ const entries = [];
2506
+ for (const one of input.wanted) {
2346
2507
  if (absent.includes(one.name)) {
2347
- return {
2508
+ entries.push({
2348
2509
  name: one.name,
2349
2510
  why: "--absent named it, so this release was not read against it"
2350
- };
2511
+ });
2512
+ continue;
2351
2513
  }
2352
2514
  if (input.skipMissing && !recordedAlready(input.root, one.name)) {
2353
- return {
2515
+ entries.push({
2354
2516
  name: one.name,
2355
2517
  why: `no snapshot of it here \u2014 record one with: geonosis-release smoke snapshot ${one.name} --from <their tree>`
2356
- };
2518
+ });
2519
+ continue;
2357
2520
  }
2358
2521
  const note = dating(input.root, one.name);
2359
- return {
2360
- comparison: runSmoke({
2522
+ entries.push({
2523
+ comparison: await runSmoke({
2361
2524
  ...one.commands === void 0 ? {} : { commands: one.commands },
2362
2525
  ...one.install === void 0 ? {} : { install: one.install },
2363
2526
  name: one.name,
@@ -2366,8 +2529,8 @@ var sweepSmoke = (input) => {
2366
2529
  }),
2367
2530
  name: one.name,
2368
2531
  ...note === void 0 ? {} : { note }
2369
- };
2370
- });
2532
+ });
2533
+ }
2371
2534
  return { entries };
2372
2535
  };
2373
2536
  var comparisonsIn = (sweep) => sweep.entries.flatMap((one) => "comparison" in one ? [one.comparison] : []);
@@ -2424,9 +2587,16 @@ var sweepEnvelope = (sweep, durationMs) => {
2424
2587
  version: versionOf(import.meta.url)
2425
2588
  };
2426
2589
  };
2427
- var outcomeLine = (outcome) => "why" in outcome ? ` ${outcome.phase}: nothing to run \u2014 ${outcome.why}
2428
- ` : ` ${outcome.phase}: \`${outcome.command}\` exited ${outcome.code}
2590
+ var outcomeLine = (outcome) => {
2591
+ if ("why" in outcome) return ` ${outcome.phase}: nothing to run \u2014 ${outcome.why}
2592
+ `;
2593
+ if ("unmeasurable" in outcome) {
2594
+ return ` NOT MEASURED ${outcome.phase}: ${outcome.unmeasurable} (\`${outcome.command}\`)
2429
2595
  `;
2596
+ }
2597
+ return ` ${outcome.phase}: \`${outcome.command}\` exited ${outcome.code}
2598
+ `;
2599
+ };
2430
2600
  var headline = (comparison) => {
2431
2601
  if (comparison.ok) {
2432
2602
  return `OK smoke ${comparison.name}: nothing this release changed reddened their tree
@@ -2441,6 +2611,10 @@ var headline = (comparison) => {
2441
2611
  };
2442
2612
  var formatSmoke = (comparison) => [
2443
2613
  headline(comparison),
2614
+ ...comparison.candidate === void 0 ? [] : [` ${comparison.candidate}
2615
+ `],
2616
+ ...comparison.runCopy === void 0 ? [] : [` run copy: ${comparison.runCopy}
2617
+ `],
2444
2618
  ...comparison.outcomes.map(outcomeLine),
2445
2619
  ...comparison.broken.flatMap((one) => [
2446
2620
  ` ${one.phase} passed at the versions they are on and fails under this release:
@@ -2464,19 +2638,26 @@ var formatSmoke = (comparison) => [
2464
2638
  var formatBaseline = (name, baseline) => [
2465
2639
  `OK baseline ${name}: recorded against ${baseline.rc}
2466
2640
  `,
2641
+ ...baseline.candidate === void 0 ? [] : [` ${baseline.candidate}
2642
+ `],
2643
+ ...baseline.runCopy === void 0 ? [] : [` run copy: ${baseline.runCopy}
2644
+ `],
2467
2645
  ...baseline.phases.map(outcomeLine)
2468
2646
  ].join("");
2469
2647
  var SMOKE_TOOL = "release-smoke";
2470
2648
  var SMOKE_NEXT = "geonosis-release smoke run --snapshot <name> --json and read `outcomes` \u2014 every phase the snapshot named leaves by exactly one door: read, excused with the sentence saying there was nothing of theirs to run, or refused because this run and the baseline are not comparable";
2471
2649
  var smokeEnvelope = (comparison, durationMs) => {
2472
2650
  const refused = new Set(comparison.refused.map((one) => one.phase));
2473
- const excused = comparison.outcomes.filter(
2474
- (one) => "why" in one && !refused.has(one.phase)
2475
- );
2651
+ const excused = comparison.outcomes.flatMap((one) => {
2652
+ if (refused.has(one.phase)) return [];
2653
+ if ("why" in one) return [{ path: one.phase, reason: one.why }];
2654
+ if ("unmeasurable" in one) return [{ path: one.phase, reason: one.unmeasurable }];
2655
+ return [];
2656
+ });
2476
2657
  return {
2477
2658
  considered: comparison.outcomes.length,
2478
2659
  durationMs,
2479
- excused: excused.map((one) => ({ path: one.phase, reason: one.why })),
2660
+ excused,
2480
2661
  findings: [
2481
2662
  ...comparison.broken.map((one) => ({ lines: one.lines, phase: one.phase })),
2482
2663
  ...comparison.findings.map((one) => ({ packed: one }))
@@ -2489,8 +2670,8 @@ var smokeEnvelope = (comparison, durationMs) => {
2489
2670
  };
2490
2671
 
2491
2672
  // src/changelog.ts
2492
- import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync11 } from "fs";
2493
- import { join as join8, relative as relative5, resolve as resolve10, sep as sep4 } from "path";
2673
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
2674
+ import { dirname as dirname3, join as join8, resolve as resolve10 } from "path";
2494
2675
  var CHANGELOG = "CHANGELOG.md";
2495
2676
  var VERSION_HEADING = /^##\s+v?(\d+\.\d+\.\d+.*)$/;
2496
2677
  var emptyHeadingsIn = (body, at) => {
@@ -2539,35 +2720,18 @@ var formatChangelogs = (report) => [
2539
2720
  ),
2540
2721
  report.empty.length === 0 ? `changelog PASS \u2014 ${report.versions} version heading(s) over ${report.read} file(s), every one of them says something` : `changelog FAIL \u2014 ${report.empty.length} of ${report.versions} version heading(s) over ${report.read} file(s) are empty`
2541
2722
  ].join("\n");
2542
- var NEVER_WALKED3 = /* @__PURE__ */ new Set(["build", "coverage", "dist", "node_modules", "storybook-static"]);
2543
2723
  var publishableDirs = (root) => {
2544
2724
  const found = [];
2545
- const walk = (dir) => {
2546
- let entries;
2725
+ for (const path of manifestsUnder(root)) {
2726
+ let manifest;
2547
2727
  try {
2548
- entries = readdirSync6(dir, { withFileTypes: true });
2728
+ manifest = JSON.parse(readFileSync11(path, "utf8"));
2549
2729
  } catch {
2550
- return;
2551
- }
2552
- for (const entry of entries) {
2553
- if (entry.isDirectory()) {
2554
- if (!entry.name.startsWith(".") && !NEVER_WALKED3.has(entry.name)) {
2555
- walk(join8(dir, entry.name));
2556
- }
2557
- continue;
2558
- }
2559
- if (entry.name !== "package.json") continue;
2560
- let manifest;
2561
- try {
2562
- manifest = JSON.parse(readFileSync11(join8(dir, entry.name), "utf8"));
2563
- } catch {
2564
- continue;
2565
- }
2566
- if (manifest.private === true || typeof manifest.name !== "string") continue;
2567
- found.push(relative5(root, dir).split(sep4).join("/"));
2730
+ continue;
2568
2731
  }
2569
- };
2570
- walk(root);
2732
+ if (manifest.private === true || typeof manifest.name !== "string") continue;
2733
+ found.push(pathOf(root, dirname3(path)));
2734
+ }
2571
2735
  return found.filter((one) => one !== "").toSorted();
2572
2736
  };
2573
2737
 
package/dist/index.d.ts CHANGED
@@ -197,6 +197,8 @@ type SmokeSnapshotConfig = {
197
197
  type SmokeConfig = {
198
198
  exclude?: string[];
199
199
  snapshots?: SmokeSnapshotConfig[];
200
+ /** How long an install or a phase may run before the runner kills its whole process group (#368). */
201
+ timeoutMs?: number;
200
202
  };
201
203
  /**
202
204
  * Law 6: every directory, dialect, secret name and worker is an OPTION. The defaults are empty, and
@@ -524,6 +526,10 @@ type PhaseOutcome = {
524
526
  lines: string[];
525
527
  ok: boolean;
526
528
  phase: SmokePhase;
529
+ } | {
530
+ command: string;
531
+ phase: SmokePhase;
532
+ unmeasurable: string;
527
533
  } | {
528
534
  phase: SmokePhase;
529
535
  why: string;
@@ -536,18 +542,24 @@ type Packed = {
536
542
  };
537
543
  type SmokeBaseline = {
538
544
  at: string;
545
+ /** The candidate's first step (#389) — absent when this run packed no `@geonosis/cli`. */
546
+ candidate?: string;
539
547
  packed: {
540
548
  name: string;
541
549
  version: string;
542
550
  }[];
543
551
  phases: PhaseOutcome[];
544
552
  rc: string;
553
+ /** Where a phase that failed left its copy — absent when every phase that ran passed. */
554
+ runCopy?: string;
545
555
  };
546
556
  type SmokeComparison = {
547
557
  broken: {
548
558
  lines: string[];
549
559
  phase: SmokePhase;
550
560
  }[];
561
+ /** The candidate's first step (#389) — absent when this run packed no `@geonosis/cli`. */
562
+ candidate?: string;
551
563
  findings: string[];
552
564
  known: SmokePhase[];
553
565
  mended: SmokePhase[];
@@ -558,6 +570,8 @@ type SmokeComparison = {
558
570
  phase: SmokePhase;
559
571
  why: string;
560
572
  }[];
573
+ /** Where a phase that failed left its copy — absent when every phase that ran passed. */
574
+ runCopy?: string;
561
575
  };
562
576
  declare const BASELINE_FILE = "baseline.json";
563
577
  /**
@@ -587,10 +601,10 @@ type RunInput = {
587
601
  rc?: string;
588
602
  root: string;
589
603
  };
590
- declare const recordBaseline: (input: RunInput) => SmokeBaseline;
604
+ declare const recordBaseline: (input: RunInput) => Promise<SmokeBaseline>;
591
605
  declare const readBaseline: (root: string, name: string) => SmokeBaseline;
592
606
  declare const compareToBaseline: (name: string, baseline: SmokeBaseline, outcomes: PhaseOutcome[], findings: string[]) => SmokeComparison;
593
- declare const runSmoke: (input: RunInput) => SmokeComparison;
607
+ declare const runSmoke: (input: RunInput) => Promise<SmokeComparison>;
594
608
  /**
595
609
  * What the sweep can say about WHEN a recording was taken: that its source has moved on, or that
596
610
  * the question cannot be asked of it at all.
@@ -645,7 +659,7 @@ declare const sweepSmoke: (input: {
645
659
  */
646
660
  skipMissing: boolean;
647
661
  wanted: WantedSnapshot[];
648
- }) => SmokeSweep;
662
+ }) => Promise<SmokeSweep>;
649
663
  declare const comparisonsIn: (sweep: SmokeSweep) => SmokeComparison[];
650
664
  declare const formatSweep: (sweep: SmokeSweep) => string;
651
665
  declare const sweepEnvelope: (sweep: SmokeSweep, durationMs: number) => ReportEnvelope;
package/dist/index.js CHANGED
@@ -74,7 +74,7 @@ import {
74
74
  statementsOf,
75
75
  sweepEnvelope,
76
76
  sweepSmoke
77
- } from "./chunk-CACOG4I5.js";
77
+ } from "./chunk-RQOZEZSB.js";
78
78
  export {
79
79
  ADOPTION_NEXT,
80
80
  ADOPTION_TOOL,
@@ -40,11 +40,111 @@ import {
40
40
  sweepEnvelope,
41
41
  sweepSmoke,
42
42
  writeEnvelope
43
- } from "./chunk-CACOG4I5.js";
43
+ } from "./chunk-RQOZEZSB.js";
44
44
 
45
45
  // src/release-cli.ts
46
46
  import { existsSync } from "fs";
47
- import { join, resolve } from "path";
47
+ import { join as join2, resolve } from "path";
48
+
49
+ // src/heavy-slot.ts
50
+ import { randomUUID } from "crypto";
51
+ import { linkSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
52
+ import { tmpdir } from "os";
53
+ import { dirname, join } from "path";
54
+ var HEAVY_SLOTS = 2;
55
+ var DEFAULT_WAIT_MS = 20 * 6e4;
56
+ var heavySlotDir = () => {
57
+ const named = process.env.GEONOSIS_HEAVY_SLOT_DIR;
58
+ if (named !== void 0 && named !== "") return named;
59
+ return join(tmpdir(), "geonosis-heavy");
60
+ };
61
+ var slotPath = (dir, at) => join(dir, `slot-${at}.json`);
62
+ var alive = (pid) => {
63
+ try {
64
+ process.kill(pid, 0);
65
+ return true;
66
+ } catch (error) {
67
+ return error.code === "EPERM";
68
+ }
69
+ };
70
+ var holderOf = (path) => {
71
+ try {
72
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
73
+ return typeof parsed.pid === "number" ? {
74
+ command: parsed.command ?? "unknown",
75
+ cwd: parsed.cwd ?? "somewhere",
76
+ pid: parsed.pid,
77
+ startedAt: parsed.startedAt ?? "unknown"
78
+ } : void 0;
79
+ } catch {
80
+ return void 0;
81
+ }
82
+ };
83
+ var reapIfDead = (path) => {
84
+ const holder = holderOf(path);
85
+ if (holder !== void 0 && !alive(holder.pid)) rmSync(path, { force: true });
86
+ };
87
+ var claim = (path, holder) => {
88
+ mkdirSync(dirname(path), { recursive: true });
89
+ const staging = `${path}.${holder.pid}.${randomUUID()}`;
90
+ try {
91
+ writeFileSync(staging, JSON.stringify(holder));
92
+ linkSync(staging, path);
93
+ return true;
94
+ } catch (error) {
95
+ if (error.code === "EEXIST") return false;
96
+ throw error;
97
+ } finally {
98
+ rmSync(staging, { force: true });
99
+ }
100
+ };
101
+ var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
102
+ var namedHolders = (dir) => Array.from({ length: HEAVY_SLOTS }, (_unused, at) => holderOf(slotPath(dir, at))).filter(
103
+ (holder) => holder !== void 0
104
+ );
105
+ var describeHolders = (holders) => holders.map((one) => `${one.pid} ${one.command} since ${one.startedAt}`).join(", ");
106
+ var HeavySlotTimeout = class extends Error {
107
+ };
108
+ var takeSlot = async ({
109
+ command,
110
+ cwd = process.cwd(),
111
+ dir = heavySlotDir(),
112
+ pollMs = 250,
113
+ say = (line) => process.stderr.write(`${line}
114
+ `),
115
+ waitMs = Number(process.env.GEONOSIS_HEAVY_WAIT_MS ?? "") || DEFAULT_WAIT_MS
116
+ }) => {
117
+ const until = Date.now() + waitMs;
118
+ let told = false;
119
+ for (; ; ) {
120
+ for (let at = 0; at < HEAVY_SLOTS; at += 1) {
121
+ const path = slotPath(dir, at);
122
+ reapIfDead(path);
123
+ const mine = {
124
+ command,
125
+ cwd,
126
+ pid: process.pid,
127
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
128
+ };
129
+ if (claim(path, mine)) {
130
+ return () => {
131
+ if (holderOf(path)?.pid === process.pid) rmSync(path, { force: true });
132
+ };
133
+ }
134
+ }
135
+ const holders = namedHolders(dir);
136
+ if (!told) {
137
+ told = true;
138
+ say(`waiting for a heavy slot: ${holders.length} held \u2014 ${describeHolders(holders)}`);
139
+ }
140
+ if (Date.now() >= until) {
141
+ throw new HeavySlotTimeout(
142
+ `waited ${waitMs} ms for a heavy slot: ${holders.length} held \u2014 ${describeHolders(holders)}`
143
+ );
144
+ }
145
+ await sleep(pollMs);
146
+ }
147
+ };
48
148
 
49
149
  // src/since.ts
50
150
  var MEANS = {
@@ -416,32 +516,37 @@ var wantedSnapshots = (parsed, root) => {
416
516
  var rcOf = (parsed, root, cwd) => {
417
517
  const named = parsed.read["--rc-dir"];
418
518
  if (named !== void 0) return resolve(cwd, named);
419
- if (existsSync(join(root, "package.json"))) return root;
519
+ if (existsSync(join2(root, "package.json"))) return root;
420
520
  throw new CannotRun(
421
521
  "nothing here is a release candidate \u2014 pass --rc-dir <the workspace whose packages this release publishes>, because a smoke with nothing packed compares a tree to itself"
422
522
  );
423
523
  };
424
- var baseline = (parsed, root, cwd) => {
524
+ var baseline = async (parsed, root, cwd) => {
425
525
  const wanted = wantedSnapshots(parsed, root);
426
526
  const one = wanted[0];
427
527
  if (parsed.read["--snapshot"] === void 0 || one === void 0) {
428
528
  throw new CannotRun("smoke baseline needs --snapshot <name>");
429
529
  }
430
- const rc = parsed.read["--rc-dir"];
431
- const recorded = recordBaseline({
432
- ...one.commands === void 0 ? {} : { commands: one.commands },
433
- ...one.install === void 0 ? {} : { install: one.install },
434
- name: one.name,
435
- ...rc === void 0 ? {} : { rc: resolve(cwd, rc) },
436
- root
437
- });
438
- process.stdout.write(
439
- parsed.json ? `${JSON.stringify(recorded, void 0, 2)}
530
+ const releaseSlot = await takeSlot({ command: "geonosis-release smoke baseline", cwd: root });
531
+ try {
532
+ const rc = parsed.read["--rc-dir"];
533
+ const recorded = await recordBaseline({
534
+ ...one.commands === void 0 ? {} : { commands: one.commands },
535
+ ...one.install === void 0 ? {} : { install: one.install },
536
+ name: one.name,
537
+ ...rc === void 0 ? {} : { rc: resolve(cwd, rc) },
538
+ root
539
+ });
540
+ process.stdout.write(
541
+ parsed.json ? `${JSON.stringify(recorded, void 0, 2)}
440
542
  ` : formatBaseline(one.name, recorded)
441
- );
442
- return 0;
543
+ );
544
+ return 0;
545
+ } finally {
546
+ releaseSlot();
547
+ }
443
548
  };
444
- var smokeRun = (parsed, root, cwd) => {
549
+ var smokeRun = async (parsed, root, cwd) => {
445
550
  const startedAt = Date.now();
446
551
  const wanted = wantedSnapshots(parsed, root);
447
552
  if (wanted.length === 0) {
@@ -449,13 +554,19 @@ var smokeRun = (parsed, root, cwd) => {
449
554
  "nothing here names a consumer to read this release against \u2014 geonosis.json \u2192 release.smoke.snapshots names them, or pass --snapshot <name>. A release check over zero consumers is not a green release check."
450
555
  );
451
556
  }
452
- const swept = sweepSmoke({
453
- absent: parsed.many["--absent"] ?? [],
454
- rc: rcOf(parsed, root, cwd),
455
- root,
456
- skipMissing: parsed.read["--snapshot"] === void 0,
457
- wanted
458
- });
557
+ const releaseSlot = await takeSlot({ command: "geonosis-release smoke run", cwd: root });
558
+ let swept;
559
+ try {
560
+ swept = await sweepSmoke({
561
+ absent: parsed.many["--absent"] ?? [],
562
+ rc: rcOf(parsed, root, cwd),
563
+ root,
564
+ skipMissing: parsed.read["--snapshot"] === void 0,
565
+ wanted
566
+ });
567
+ } finally {
568
+ releaseSlot();
569
+ }
459
570
  process.stdout.write(
460
571
  parsed.json ? `${JSON.stringify(swept, void 0, 2)}
461
572
  ` : formatSweep(swept)
@@ -481,7 +592,7 @@ var smokeRun = (parsed, root, cwd) => {
481
592
  }
482
593
  return comparisons.every((one) => one.ok) ? 0 : 1;
483
594
  };
484
- var smoke = (parsed, cwd) => {
595
+ var smoke = async (parsed, cwd) => {
485
596
  const root = rootOf(parsed, cwd);
486
597
  const verb = parsed.rest[0];
487
598
  if (verb === "snapshot") return snapshot(parsed, root);
@@ -507,6 +618,8 @@ geonosis.json \u2192 "release" every key optional; a repo that configured n
507
618
  that tree is installed with when the manager's default is not it,
508
619
  and { doctor?, lint?, typecheck? } when their gates are not the
509
620
  scripts of those names
621
+ timeoutMs number? how long an install or a phase may run before the runner kills its
622
+ whole process group (default 1200000 \u2014 20 minutes)
510
623
  secrets string[]? the secret names a deployment carries
511
624
  steps string[]? the release steps
512
625
  workers string[]? the workers it deploys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/release",
3
- "version": "2.4.3",
3
+ "version": "2.5.1",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "The release contract with its proofs: an expand-only migration gate over three dialects, a smoke that must name the version that answered it, and declared ≠ deployed.",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "squawk-cli": "2.63.0",
49
- "@geonosis/ratchet": "2.4.3"
49
+ "@geonosis/ratchet": "2.5.1"
50
50
  },
51
51
  "engines": {
52
52
  "node": ">=22"