@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 +53 -0
- package/bin/geonosis-release.mjs +11 -3
- package/dist/{chunk-CACOG4I5.js → chunk-RQOZEZSB.js} +272 -108
- package/dist/index.d.ts +17 -3
- package/dist/index.js +1 -1
- package/dist/release-cli.js +138 -25
- package/package.json +2 -2
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
|
package/bin/geonosis-release.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 {
|
|
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([
|
|
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,
|
|
1182
|
-
const path = resolve4(root,
|
|
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 =
|
|
1203
|
+
parsed = relative4.endsWith(".toml") ? parseToml(source) : parseJsonc(source);
|
|
1187
1204
|
} catch (error) {
|
|
1188
|
-
throw new CannotRun(`${
|
|
1205
|
+
throw new CannotRun(`${relative4} could not be read: ${error.message}`);
|
|
1189
1206
|
}
|
|
1190
|
-
if (!isRecord3(parsed)) throw new CannotRun(`${
|
|
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 {
|
|
1992
|
-
|
|
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
|
|
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
|
-
|
|
2122
|
-
|
|
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
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
|
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(
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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
|
|
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 =
|
|
2505
|
+
const entries = [];
|
|
2506
|
+
for (const one of input.wanted) {
|
|
2346
2507
|
if (absent.includes(one.name)) {
|
|
2347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
2428
|
-
|
|
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.
|
|
2474
|
-
(
|
|
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
|
|
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,
|
|
2493
|
-
import {
|
|
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
|
|
2546
|
-
let
|
|
2725
|
+
for (const path of manifestsUnder(root)) {
|
|
2726
|
+
let manifest;
|
|
2547
2727
|
try {
|
|
2548
|
-
|
|
2728
|
+
manifest = JSON.parse(readFileSync11(path, "utf8"));
|
|
2549
2729
|
} catch {
|
|
2550
|
-
|
|
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
|
-
|
|
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
package/dist/release-cli.js
CHANGED
|
@@ -40,11 +40,111 @@ import {
|
|
|
40
40
|
sweepEnvelope,
|
|
41
41
|
sweepSmoke,
|
|
42
42
|
writeEnvelope
|
|
43
|
-
} from "./chunk-
|
|
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(
|
|
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
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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.
|
|
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.
|
|
49
|
+
"@geonosis/ratchet": "2.5.1"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">=22"
|