breakage 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/dist/breakage-B7dJsgcu.js +1235 -0
- package/dist/breakage-B7dJsgcu.js.map +1 -0
- package/dist/breakage.d.ts +113 -1
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +1 -1
- package/dist/breakage.npm.check.schema.json +16 -0
- package/dist/breakage.npm.help.schema.json +6 -0
- package/dist/breakage.npm.schema.json +6 -0
- package/dist/breakage.npm.test.$peer.$range.schema.json +16 -0
- package/dist/breakage.npm.test.schema.json +16 -0
- package/dist/breakage.x.js +109 -5
- package/dist/breakage.x.js.map +1 -1
- package/npm.md +214 -0
- package/package.json +9 -7
- package/src/breakage.ts +14 -0
- package/src/breakage.x.ts +30 -3
- package/src/npm/check.ts +448 -0
- package/src/npm/cli.ts +126 -0
- package/src/npm/command.ts +52 -0
- package/src/npm/config.ts +86 -0
- package/src/npm/environment.ts +661 -0
- package/src/npm/manifest.ts +134 -0
- package/src/npm/release.ts +189 -0
- package/src/npm/types.ts +104 -0
- package/dist/breakage-Dc4IldeW.js +0 -435
- package/dist/breakage-Dc4IldeW.js.map +0 -1
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { minimatch } from "minimatch";
|
|
4
|
-
import { simpleGit } from "simple-git";
|
|
5
|
-
import logger from "takua";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { chmod, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
|
|
8
|
-
import { setTimeout } from "node:timers/promises";
|
|
9
|
-
//#region src/directory-lock.ts
|
|
10
|
-
async function withDirectoryLock(lock, action, waitForCleanup = false) {
|
|
11
|
-
const deadline = Date.now() + 6e4;
|
|
12
|
-
for (;;) try {
|
|
13
|
-
await mkdir(lock);
|
|
14
|
-
break;
|
|
15
|
-
} catch (thrown) {
|
|
16
|
-
if (thrown.code !== `EEXIST`) throw thrown;
|
|
17
|
-
if (!waitForCleanup && Date.now() >= deadline) throw new Error(`Timed out waiting for ${lock}. Check for an interrupted operation before removing this lock.`);
|
|
18
|
-
await setTimeout(20);
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
return await action();
|
|
22
|
-
} finally {
|
|
23
|
-
await rmdir(lock);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
|
-
//#region src/release-tag.ts
|
|
28
|
-
function compareInteger(a, b) {
|
|
29
|
-
return a.length - b.length || (a > b ? 1 : a < b ? -1 : 0);
|
|
30
|
-
}
|
|
31
|
-
function compareRelease(a, b) {
|
|
32
|
-
for (let index = 0; index < 3; index++) {
|
|
33
|
-
const difference = compareInteger(a.core[index], b.core[index]);
|
|
34
|
-
if (difference) return difference;
|
|
35
|
-
}
|
|
36
|
-
if (!a.prerelease.length || !b.prerelease.length) return Number(!a.prerelease.length) - Number(!b.prerelease.length);
|
|
37
|
-
for (let index = 0; index < Math.min(a.prerelease.length, b.prerelease.length); index++) {
|
|
38
|
-
const left = a.prerelease[index];
|
|
39
|
-
const right = b.prerelease[index];
|
|
40
|
-
const leftNumeric = /^\d+$/.test(left);
|
|
41
|
-
const rightNumeric = /^\d+$/.test(right);
|
|
42
|
-
const difference = leftNumeric && rightNumeric ? compareInteger(left, right) : leftNumeric !== rightNumeric ? Number(rightNumeric) - Number(leftNumeric) : left > right ? 1 : left < right ? -1 : 0;
|
|
43
|
-
if (difference) return difference;
|
|
44
|
-
}
|
|
45
|
-
return a.prerelease.length - b.prerelease.length;
|
|
46
|
-
}
|
|
47
|
-
function latestReleaseTag(remoteTags, tagPattern) {
|
|
48
|
-
const pattern = tagPattern ? new RegExp(tagPattern) : void 0;
|
|
49
|
-
let latest;
|
|
50
|
-
for (const line of remoteTags.split(`\n`)) {
|
|
51
|
-
if (pattern && !pattern.test(line)) continue;
|
|
52
|
-
const ref = line.split(`\t`)[1]?.replace(/\^\{\}$/, ``);
|
|
53
|
-
if (!ref?.startsWith(`refs/tags/`)) continue;
|
|
54
|
-
const name = ref.slice(`refs/tags/`.length);
|
|
55
|
-
const version = name.slice(name.lastIndexOf(`@`) + 1);
|
|
56
|
-
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
|
|
57
|
-
if (!match) continue;
|
|
58
|
-
const prerelease = match[4]?.split(`.`) ?? [];
|
|
59
|
-
if (prerelease.some((identifier) => /^0\d+$/.test(identifier))) continue;
|
|
60
|
-
const release = {
|
|
61
|
-
ref,
|
|
62
|
-
core: match.slice(1, 4),
|
|
63
|
-
prerelease
|
|
64
|
-
};
|
|
65
|
-
if (!latest || compareRelease(release, latest) > 0) latest = release;
|
|
66
|
-
}
|
|
67
|
-
return latest?.ref;
|
|
68
|
-
}
|
|
69
|
-
//#endregion
|
|
70
|
-
//#region src/baseline.ts
|
|
71
|
-
const objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
|
|
72
|
-
function isBaseline(value) {
|
|
73
|
-
if (typeof value !== `object` || value === null) return false;
|
|
74
|
-
const record = value;
|
|
75
|
-
return record.version === 1 && (record.tagPattern === null || typeof record.tagPattern === `string`) && typeof record.packageDirectory === `string` && typeof record.ref === `string` && record.ref.startsWith(`refs/tags/`) && typeof record.commit === `string` && objectId.test(record.commit);
|
|
76
|
-
}
|
|
77
|
-
/** Resolve and fetch a release without running tests or changing the worktree. */
|
|
78
|
-
async function breakagePrelude({ out, tagPattern, baseDirname = process.cwd() }) {
|
|
79
|
-
const filename = path.resolve(baseDirname, out);
|
|
80
|
-
const temporaryPath = `${filename}.${randomUUID()}.tmp`;
|
|
81
|
-
let canInvalidate = false;
|
|
82
|
-
try {
|
|
83
|
-
const git = simpleGit(baseDirname);
|
|
84
|
-
if (!(await git.raw([
|
|
85
|
-
`check-ignore`,
|
|
86
|
-
`--`,
|
|
87
|
-
filename
|
|
88
|
-
])).trim()) throw new Error(`Baseline output must be gitignored: ${filename}.`);
|
|
89
|
-
canInvalidate = true;
|
|
90
|
-
const remoteTags = await git.listRemote([`--tags`, `origin`]);
|
|
91
|
-
const ref = latestReleaseTag(remoteTags, tagPattern);
|
|
92
|
-
if (!ref) throw new Error(`No tags found matching the pattern "${tagPattern}".`);
|
|
93
|
-
const refs = new Map(remoteTags.trim().split(`\n`).map((line) => {
|
|
94
|
-
const [sha, name] = line.split(`\t`);
|
|
95
|
-
return [name, sha];
|
|
96
|
-
}));
|
|
97
|
-
const commit = refs.get(`${ref}^{}`) ?? refs.get(ref);
|
|
98
|
-
if (!commit || !objectId.test(commit)) throw new Error(`Could not resolve the release commit for ${ref}.`);
|
|
99
|
-
const commonGitDirectory = await git.revparse([`--path-format=absolute`, `--git-common-dir`]);
|
|
100
|
-
await withDirectoryLock(path.join(commonGitDirectory, `breakage-fetch.lock`), () => git.fetch([
|
|
101
|
-
`origin`,
|
|
102
|
-
commit,
|
|
103
|
-
`--no-tags`,
|
|
104
|
-
`--no-write-fetch-head`
|
|
105
|
-
]));
|
|
106
|
-
if ((await git.raw([
|
|
107
|
-
`cat-file`,
|
|
108
|
-
`-t`,
|
|
109
|
-
commit
|
|
110
|
-
])).trim() !== `commit`) throw new Error(`Release ${ref} does not resolve to a commit.`);
|
|
111
|
-
const baseline = {
|
|
112
|
-
version: 1,
|
|
113
|
-
tagPattern: tagPattern ?? null,
|
|
114
|
-
packageDirectory: await git.revparse([`--show-prefix`]),
|
|
115
|
-
ref,
|
|
116
|
-
commit
|
|
117
|
-
};
|
|
118
|
-
await mkdir(path.dirname(filename), { recursive: true });
|
|
119
|
-
await writeFile(temporaryPath, `${JSON.stringify(baseline, null, 2)}\n`, { flag: `wx` });
|
|
120
|
-
await rename(temporaryPath, filename);
|
|
121
|
-
return baseline;
|
|
122
|
-
} catch (cause) {
|
|
123
|
-
if (canInvalidate) await rm(filename, { force: true });
|
|
124
|
-
throw new Error(`Failed to prepare baseline ${filename}.`, { cause });
|
|
125
|
-
} finally {
|
|
126
|
-
await rm(temporaryPath, { force: true });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
async function readBaseline(git, filename, { tagPattern }) {
|
|
130
|
-
try {
|
|
131
|
-
const baseline = JSON.parse(await readFile(filename, `utf8`));
|
|
132
|
-
if (!isBaseline(baseline)) throw new Error(`Invalid baseline fields.`);
|
|
133
|
-
if (baseline.tagPattern !== (tagPattern ?? null) || baseline.packageDirectory !== await git.revparse([`--show-prefix`])) throw new Error(`Baseline belongs to a different package or tag pattern.`);
|
|
134
|
-
if ((await git.raw([
|
|
135
|
-
`check-ref-format`,
|
|
136
|
-
`--normalize`,
|
|
137
|
-
baseline.ref
|
|
138
|
-
])).trim() !== baseline.ref) throw new Error(`Invalid release ref.`);
|
|
139
|
-
if ((await git.raw([
|
|
140
|
-
`cat-file`,
|
|
141
|
-
`-t`,
|
|
142
|
-
baseline.commit
|
|
143
|
-
])).trim() !== `commit`) throw new Error(`Pinned object is not a locally available commit.`);
|
|
144
|
-
return baseline;
|
|
145
|
-
} catch (cause) {
|
|
146
|
-
throw new Error(`Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`, { cause });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
//#endregion
|
|
150
|
-
//#region src/test-file-state.ts
|
|
151
|
-
function isActiveCheck(value) {
|
|
152
|
-
if (typeof value !== `object` || value === null) return false;
|
|
153
|
-
const record = value;
|
|
154
|
-
return typeof record.pid === `number` && Number.isSafeInteger(record.pid) && record.pid > 0 && typeof record.head === `string` && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(record.head) && (record.recoveryRequired === void 0 || typeof record.recoveryRequired === `boolean`) && Array.isArray(record.paths) && record.paths.length > 0 && record.paths.every((file) => typeof file === `string` && !path.isAbsolute(file) && !path.win32.isAbsolute(file) && file.split(`/`).every((part) => part !== `` && part !== `.` && part !== `..`));
|
|
155
|
-
}
|
|
156
|
-
async function readRecoveryRecord(recordPath) {
|
|
157
|
-
try {
|
|
158
|
-
const record = JSON.parse(await readFile(recordPath, `utf8`));
|
|
159
|
-
if (!isActiveCheck(record)) throw new TypeError(`Invalid recovery record fields`);
|
|
160
|
-
return record;
|
|
161
|
-
} catch (cause) {
|
|
162
|
-
throw new Error(`A breakage run needs recovery. The record ${recordPath} is unreadable or invalid. Verify no check is running and recover its test files before removing this record.`, { cause });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
async function writeRecoveryRecord(recordPath, record) {
|
|
166
|
-
const temporaryPath = `${recordPath}.${randomUUID()}.tmp`;
|
|
167
|
-
try {
|
|
168
|
-
await writeFile(temporaryPath, JSON.stringify(record), { flag: `wx` });
|
|
169
|
-
await rename(temporaryPath, recordPath);
|
|
170
|
-
} finally {
|
|
171
|
-
await rm(temporaryPath, { force: true });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
function assertContained(root, filename) {
|
|
175
|
-
const relative = path.relative(root, filename);
|
|
176
|
-
if (relative === `..` || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`Cannot replace tests outside the worktree: ${filename}.`);
|
|
177
|
-
}
|
|
178
|
-
async function validateParents(root, filenames) {
|
|
179
|
-
const checked = /* @__PURE__ */ new Set();
|
|
180
|
-
for (const filename of filenames) {
|
|
181
|
-
assertContained(root, filename);
|
|
182
|
-
for (let parent = path.dirname(filename);; parent = path.dirname(parent)) {
|
|
183
|
-
if (checked.has(parent)) break;
|
|
184
|
-
const stats = await lstat(parent).catch((thrown) => {
|
|
185
|
-
if (thrown.code !== `ENOENT`) throw thrown;
|
|
186
|
-
});
|
|
187
|
-
if (stats && !stats.isDirectory()) throw new Error(`Cannot replace tests through ${parent}.`);
|
|
188
|
-
checked.add(parent);
|
|
189
|
-
if (parent === root) break;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
async function withTestFileState(git, baseDirname, action) {
|
|
194
|
-
const root = await realpath(await git.revparse([`--show-toplevel`]));
|
|
195
|
-
baseDirname = await realpath(baseDirname);
|
|
196
|
-
assertContained(root, baseDirname);
|
|
197
|
-
const stateDirectory = path.join(await git.revparse([`--absolute-git-dir`]), `breakage`);
|
|
198
|
-
await mkdir(stateDirectory, { recursive: true });
|
|
199
|
-
const lock = path.join(stateDirectory, `lock`);
|
|
200
|
-
return withDirectoryLock(lock, async () => {
|
|
201
|
-
const activeChecks = [];
|
|
202
|
-
for (const entry of await readdir(stateDirectory)) {
|
|
203
|
-
if (!entry.endsWith(`.json`)) continue;
|
|
204
|
-
const recordPath = path.join(stateDirectory, entry);
|
|
205
|
-
const record = await readRecoveryRecord(recordPath);
|
|
206
|
-
let interrupted = record.recoveryRequired;
|
|
207
|
-
try {
|
|
208
|
-
process.kill(record.pid, 0);
|
|
209
|
-
} catch (thrown) {
|
|
210
|
-
if (thrown.code !== `EPERM`) interrupted = true;
|
|
211
|
-
}
|
|
212
|
-
if (interrupted) throw new Error(`A breakage run needs recovery. Restore the test paths recorded in ${recordPath} before removing that record.`);
|
|
213
|
-
activeChecks.push(record);
|
|
214
|
-
}
|
|
215
|
-
const activePaths = activeChecks.flatMap((record) => record.paths);
|
|
216
|
-
return action({
|
|
217
|
-
isClean: async () => !await simpleGit(root).raw([
|
|
218
|
-
`--no-optional-locks`,
|
|
219
|
-
`status`,
|
|
220
|
-
`--porcelain`,
|
|
221
|
-
`--untracked-files=all`,
|
|
222
|
-
`--`,
|
|
223
|
-
`.`,
|
|
224
|
-
...activePaths.map((file) => `:(top,literal,exclude)${file}`)
|
|
225
|
-
]),
|
|
226
|
-
replaceTests: async (release, files) => {
|
|
227
|
-
const paths = files.map((file) => path.relative(root, path.resolve(baseDirname, file)).split(path.sep).join(`/`));
|
|
228
|
-
for (const file of paths) if (activePaths.some((active) => file === active || file.startsWith(`${active}/`) || active.startsWith(`${file}/`))) throw new Error(`Another breakage run is using ${file}. Concurrent checks must use disjoint test paths.`);
|
|
229
|
-
const head = await git.revparse([`HEAD`]);
|
|
230
|
-
const currentFiles = new Set((await git.raw([
|
|
231
|
-
`ls-tree`,
|
|
232
|
-
`-r`,
|
|
233
|
-
`--name-only`,
|
|
234
|
-
`-z`,
|
|
235
|
-
head
|
|
236
|
-
])).split(`\0`).filter(Boolean));
|
|
237
|
-
const existing = files.filter((file) => currentFiles.has(file));
|
|
238
|
-
const absent = files.filter((file) => !currentFiles.has(file));
|
|
239
|
-
const modes = /* @__PURE__ */ new Map();
|
|
240
|
-
const filenames = files.map((file) => path.resolve(baseDirname, file));
|
|
241
|
-
await validateParents(root, filenames);
|
|
242
|
-
for (const file of files) {
|
|
243
|
-
const filename = path.resolve(baseDirname, file);
|
|
244
|
-
const stats = await lstat(filename).catch((thrown) => {
|
|
245
|
-
if (thrown.code !== `ENOENT`) throw thrown;
|
|
246
|
-
});
|
|
247
|
-
if (!currentFiles.has(file) && stats) throw new Error(`Cannot overwrite the existing untracked path ${filename}.`);
|
|
248
|
-
if (stats?.isFile()) modes.set(filename, stats.mode);
|
|
249
|
-
}
|
|
250
|
-
const recordPath = path.join(stateDirectory, `${randomUUID()}.json`);
|
|
251
|
-
const record = {
|
|
252
|
-
pid: process.pid,
|
|
253
|
-
paths,
|
|
254
|
-
head
|
|
255
|
-
};
|
|
256
|
-
await writeRecoveryRecord(recordPath, record);
|
|
257
|
-
const restore = async () => {
|
|
258
|
-
await writeRecoveryRecord(recordPath, {
|
|
259
|
-
...record,
|
|
260
|
-
recoveryRequired: true
|
|
261
|
-
});
|
|
262
|
-
await validateParents(root, filenames);
|
|
263
|
-
if (existing.length) await git.raw([
|
|
264
|
-
`--literal-pathspecs`,
|
|
265
|
-
`restore`,
|
|
266
|
-
`--source=${head}`,
|
|
267
|
-
`--worktree`,
|
|
268
|
-
`--`,
|
|
269
|
-
...existing
|
|
270
|
-
]);
|
|
271
|
-
for (const file of absent) await rm(path.resolve(baseDirname, file), { force: true });
|
|
272
|
-
for (const [filename, mode] of modes) await chmod(filename, mode);
|
|
273
|
-
await rm(recordPath);
|
|
274
|
-
};
|
|
275
|
-
try {
|
|
276
|
-
await git.raw([
|
|
277
|
-
`--literal-pathspecs`,
|
|
278
|
-
`restore`,
|
|
279
|
-
`--source=${release}`,
|
|
280
|
-
`--worktree`,
|
|
281
|
-
`--`,
|
|
282
|
-
...files
|
|
283
|
-
]);
|
|
284
|
-
} catch (thrown) {
|
|
285
|
-
await restore();
|
|
286
|
-
throw thrown;
|
|
287
|
-
}
|
|
288
|
-
return () => withDirectoryLock(lock, restore, true);
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
//#endregion
|
|
294
|
-
//#region src/breakage.ts
|
|
295
|
-
async function breakage({ tagPattern, baselineFile, testPattern, testCommand, certifyCommand, baseDirname = process.cwd(), verbose = false }) {
|
|
296
|
-
let mark;
|
|
297
|
-
let logMarks;
|
|
298
|
-
if (verbose) {
|
|
299
|
-
const chronicle = logger.makeChronicle();
|
|
300
|
-
mark = chronicle.mark;
|
|
301
|
-
logMarks = chronicle.logMarks;
|
|
302
|
-
}
|
|
303
|
-
mark?.(`breakage`);
|
|
304
|
-
const git = simpleGit(baseDirname);
|
|
305
|
-
mark?.(`spawn git`);
|
|
306
|
-
const isGitRepo = await git.checkIsRepo();
|
|
307
|
-
mark?.(`is git repository`);
|
|
308
|
-
if (!isGitRepo) return {
|
|
309
|
-
summary: `The git repository must be clean to run this command.`,
|
|
310
|
-
gitWasClean: false
|
|
311
|
-
};
|
|
312
|
-
if (!await withTestFileState(git, baseDirname, (state) => state.isClean())) return {
|
|
313
|
-
summary: `The git repository must be clean to run this command.`,
|
|
314
|
-
gitWasClean: false
|
|
315
|
-
};
|
|
316
|
-
let latestReleaseTag$1;
|
|
317
|
-
let releaseCommit;
|
|
318
|
-
if (baselineFile !== void 0) {
|
|
319
|
-
const baseline = await readBaseline(git, path.resolve(baseDirname, baselineFile), { tagPattern });
|
|
320
|
-
latestReleaseTag$1 = baseline.ref;
|
|
321
|
-
releaseCommit = baseline.commit;
|
|
322
|
-
mark?.(`read pinned release baseline`);
|
|
323
|
-
} else {
|
|
324
|
-
const tagsRemote = await git.listRemote([`--tags`, `origin`]);
|
|
325
|
-
mark?.(`list remote tags`);
|
|
326
|
-
latestReleaseTag$1 = latestReleaseTag(tagsRemote, tagPattern);
|
|
327
|
-
mark?.(`found latest release tag`);
|
|
328
|
-
if (!latestReleaseTag$1) return {
|
|
329
|
-
summary: `No tags found matching the pattern "${tagPattern}".`,
|
|
330
|
-
gitWasClean: true,
|
|
331
|
-
lastReleaseFound: false
|
|
332
|
-
};
|
|
333
|
-
const commonGitDirectory = await git.revparse([`--path-format=absolute`, `--git-common-dir`]);
|
|
334
|
-
await withDirectoryLock(path.join(commonGitDirectory, `breakage-fetch.lock`), () => git.fetch([
|
|
335
|
-
`origin`,
|
|
336
|
-
`${latestReleaseTag$1}:${latestReleaseTag$1}`,
|
|
337
|
-
`--no-tags`,
|
|
338
|
-
`--no-write-fetch-head`
|
|
339
|
-
]));
|
|
340
|
-
releaseCommit = await git.revparse([`${latestReleaseTag$1}^{commit}`]);
|
|
341
|
-
mark?.(`fetched latest release tag`);
|
|
342
|
-
}
|
|
343
|
-
let productionFiles;
|
|
344
|
-
try {
|
|
345
|
-
productionFiles = (await git.raw([
|
|
346
|
-
`ls-tree`,
|
|
347
|
-
`-r`,
|
|
348
|
-
`--name-only`,
|
|
349
|
-
`-z`,
|
|
350
|
-
releaseCommit
|
|
351
|
-
])).split(`\0`).filter(Boolean);
|
|
352
|
-
} catch (thrown) {
|
|
353
|
-
mark?.(`failed to list production files`);
|
|
354
|
-
const message = thrown instanceof Error ? thrown.message : typeof thrown === `string` ? thrown : void 0;
|
|
355
|
-
return {
|
|
356
|
-
summary: `Failed to list production files${message ? `: ${message}` : `.`}`,
|
|
357
|
-
gitWasClean: true,
|
|
358
|
-
lastReleaseFound: true,
|
|
359
|
-
lastReleaseTag: latestReleaseTag$1,
|
|
360
|
-
testsWereFound: false
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
mark?.(`listed all files checked into git`);
|
|
364
|
-
const productionTestFiles = productionFiles.filter((file) => minimatch(file, testPattern));
|
|
365
|
-
mark?.(`filtered to public test files`);
|
|
366
|
-
const prepared = await withTestFileState(git, baseDirname, async (state) => {
|
|
367
|
-
if (!await state.isClean()) return {
|
|
368
|
-
summary: `The git repository must be clean to run this command.`,
|
|
369
|
-
gitWasClean: false
|
|
370
|
-
};
|
|
371
|
-
if (productionTestFiles.length === 0) return {
|
|
372
|
-
summary: `No tests were found matching the pattern "${testPattern}".`,
|
|
373
|
-
gitWasClean: true,
|
|
374
|
-
lastReleaseFound: true,
|
|
375
|
-
lastReleaseTag: latestReleaseTag$1,
|
|
376
|
-
testsWereFound: false
|
|
377
|
-
};
|
|
378
|
-
const restore = await state.replaceTests(releaseCommit, productionTestFiles);
|
|
379
|
-
mark?.(`restored public tests from latest release tag`);
|
|
380
|
-
return {
|
|
381
|
-
latestReleaseTag: latestReleaseTag$1,
|
|
382
|
-
productionTestFiles,
|
|
383
|
-
restore
|
|
384
|
-
};
|
|
385
|
-
});
|
|
386
|
-
if (prepared.summary !== void 0) return prepared;
|
|
387
|
-
const { restore } = prepared;
|
|
388
|
-
try {
|
|
389
|
-
const noBreakingChangesDetected = await new Promise((resolve, reject) => {
|
|
390
|
-
const result = exec(testCommand, { cwd: baseDirname }, (_, __, stderr) => {
|
|
391
|
-
if (result.exitCode === 0) resolve({
|
|
392
|
-
summary: `No breaking changes were detected.`,
|
|
393
|
-
gitWasClean: true,
|
|
394
|
-
lastReleaseFound: true,
|
|
395
|
-
lastReleaseTag: latestReleaseTag$1,
|
|
396
|
-
testsWereFound: true,
|
|
397
|
-
testsFound: productionTestFiles,
|
|
398
|
-
breakingChangesFound: false
|
|
399
|
-
});
|
|
400
|
-
else reject(stderr);
|
|
401
|
-
});
|
|
402
|
-
});
|
|
403
|
-
mark?.(`passed tests`);
|
|
404
|
-
return noBreakingChangesDetected;
|
|
405
|
-
} catch (thrown) {
|
|
406
|
-
mark?.(`failed tests`);
|
|
407
|
-
return await new Promise((resolve) => {
|
|
408
|
-
const result = exec(certifyCommand, { cwd: baseDirname }, (_, stdout, stderr) => {
|
|
409
|
-
const wereCertified = result.exitCode === 0;
|
|
410
|
-
mark?.(`${wereCertified ? `passed` : `failed`} certification`);
|
|
411
|
-
resolve({
|
|
412
|
-
summary: wereCertified ? `Breaking changes were found and certified.` : `Breaking changes were found, but not certified.`,
|
|
413
|
-
gitWasClean: true,
|
|
414
|
-
lastReleaseFound: true,
|
|
415
|
-
lastReleaseTag: latestReleaseTag$1,
|
|
416
|
-
testsWereFound: true,
|
|
417
|
-
testsFound: productionTestFiles,
|
|
418
|
-
breakingChangesFound: true,
|
|
419
|
-
testResult: thrown,
|
|
420
|
-
breakingChangesCertified: wereCertified,
|
|
421
|
-
certificationStdout: stdout,
|
|
422
|
-
certificationStderr: stderr
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
} finally {
|
|
427
|
-
await restore();
|
|
428
|
-
mark?.(`restored original public tests`);
|
|
429
|
-
logMarks?.();
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
//#endregion
|
|
433
|
-
export { breakagePrelude as n, breakage as t };
|
|
434
|
-
|
|
435
|
-
//# sourceMappingURL=breakage-Dc4IldeW.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"breakage-Dc4IldeW.js","names":["latestReleaseTag","selectLatestReleaseTag"],"sources":["../src/directory-lock.ts","../src/release-tag.ts","../src/baseline.ts","../src/test-file-state.ts","../src/breakage.ts"],"sourcesContent":["import { mkdir, rmdir } from \"node:fs/promises\"\nimport { setTimeout } from \"node:timers/promises\"\n\nexport async function withDirectoryLock<T>(\n\tlock: string,\n\taction: () => Promise<T>,\n\twaitForCleanup = false,\n): Promise<T> {\n\tconst deadline = Date.now() + 60_000\n\tfor (;;) {\n\t\ttry {\n\t\t\tawait mkdir(lock)\n\t\t\tbreak\n\t\t} catch (thrown) {\n\t\t\tif ((thrown as NodeJS.ErrnoException).code !== `EEXIST`) throw thrown\n\t\t\tif (!waitForCleanup && Date.now() >= deadline) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Timed out waiting for ${lock}. Check for an interrupted operation before removing this lock.`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tawait setTimeout(20)\n\t\t}\n\t}\n\ttry {\n\t\treturn await action()\n\t} finally {\n\t\tawait rmdir(lock)\n\t}\n}\n","type Release = { ref: string; core: string[]; prerelease: string[] }\n\nfunction compareInteger(a: string, b: string): number {\n\treturn a.length - b.length || (a > b ? 1 : a < b ? -1 : 0)\n}\n\nfunction compareRelease(a: Release, b: Release): number {\n\tfor (let index = 0; index < 3; index++) {\n\t\tconst difference = compareInteger(a.core[index]!, b.core[index]!)\n\t\tif (difference) return difference\n\t}\n\tif (!a.prerelease.length || !b.prerelease.length) {\n\t\treturn Number(!a.prerelease.length) - Number(!b.prerelease.length)\n\t}\n\tfor (\n\t\tlet index = 0;\n\t\tindex < Math.min(a.prerelease.length, b.prerelease.length);\n\t\tindex++\n\t) {\n\t\tconst left = a.prerelease[index]!\n\t\tconst right = b.prerelease[index]!\n\t\tconst leftNumeric = /^\\d+$/.test(left)\n\t\tconst rightNumeric = /^\\d+$/.test(right)\n\t\tconst difference =\n\t\t\tleftNumeric && rightNumeric\n\t\t\t\t? compareInteger(left, right)\n\t\t\t\t: leftNumeric !== rightNumeric\n\t\t\t\t\t? Number(rightNumeric) - Number(leftNumeric)\n\t\t\t\t\t: left > right\n\t\t\t\t\t\t? 1\n\t\t\t\t\t\t: left < right\n\t\t\t\t\t\t\t? -1\n\t\t\t\t\t\t\t: 0\n\t\tif (difference) return difference\n\t}\n\treturn a.prerelease.length - b.prerelease.length\n}\n\nexport function latestReleaseTag(\n\tremoteTags: string,\n\ttagPattern?: string,\n): string | undefined {\n\tconst pattern = tagPattern ? new RegExp(tagPattern) : undefined\n\tlet latest: Release | undefined\n\tfor (const line of remoteTags.split(`\\n`)) {\n\t\tif (pattern && !pattern.test(line)) continue\n\t\tconst ref = line.split(`\\t`)[1]?.replace(/\\^\\{\\}$/, ``)\n\t\tif (!ref?.startsWith(`refs/tags/`)) continue\n\t\tconst name = ref.slice(`refs/tags/`.length)\n\t\tconst version = name.slice(name.lastIndexOf(`@`) + 1)\n\t\tconst match =\n\t\t\t/^v?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/.exec(\n\t\t\t\tversion,\n\t\t\t)\n\t\tif (!match) continue\n\t\tconst prerelease = match[4]?.split(`.`) ?? []\n\t\tif (prerelease.some((identifier) => /^0\\d+$/.test(identifier))) continue\n\t\tconst release: Release = { ref, core: match.slice(1, 4), prerelease }\n\t\tif (!latest || compareRelease(release, latest) > 0) latest = release\n\t}\n\treturn latest?.ref\n}\n","import { randomUUID } from \"node:crypto\"\nimport { mkdir, readFile, rename, rm, writeFile } from \"node:fs/promises\"\nimport path from \"node:path\"\n\nimport type { SimpleGit } from \"simple-git\"\nimport { simpleGit } from \"simple-git\"\n\nimport { withDirectoryLock } from \"./directory-lock.ts\"\nimport { latestReleaseTag } from \"./release-tag.ts\"\n\nexport type BaselineOptions = {\n\ttagPattern?: string | undefined\n\tbaseDirname?: string\n}\n\nexport type BreakagePreludeOptions = BaselineOptions & { out: string }\n\nexport type BreakageBaseline = {\n\tversion: 1\n\ttagPattern: string | null\n\tpackageDirectory: string\n\tref: string\n\tcommit: string\n}\n\nconst objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/\n\nfunction isBaseline(value: unknown): value is BreakageBaseline {\n\tif (typeof value !== `object` || value === null) return false\n\tconst record = value as Partial<BreakageBaseline>\n\treturn (\n\t\trecord.version === 1 &&\n\t\t(record.tagPattern === null || typeof record.tagPattern === `string`) &&\n\t\ttypeof record.packageDirectory === `string` &&\n\t\ttypeof record.ref === `string` &&\n\t\trecord.ref.startsWith(`refs/tags/`) &&\n\t\ttypeof record.commit === `string` &&\n\t\tobjectId.test(record.commit)\n\t)\n}\n\n/** Resolve and fetch a release without running tests or changing the worktree. */\nexport async function breakagePrelude({\n\tout,\n\ttagPattern,\n\tbaseDirname = process.cwd(),\n}: BreakagePreludeOptions): Promise<BreakageBaseline> {\n\tconst filename = path.resolve(baseDirname, out)\n\tconst temporaryPath = `${filename}.${randomUUID()}.tmp`\n\tlet canInvalidate = false\n\ttry {\n\t\tconst git = simpleGit(baseDirname)\n\t\t// An ignored, untracked snapshot cannot make the compatibility check dirty.\n\t\tif (!(await git.raw([`check-ignore`, `--`, filename])).trim()) {\n\t\t\tthrow new Error(`Baseline output must be gitignored: ${filename}.`)\n\t\t}\n\t\tcanInvalidate = true\n\t\tconst remoteTags = await git.listRemote([`--tags`, `origin`])\n\t\tconst ref = latestReleaseTag(remoteTags, tagPattern)\n\t\tif (!ref) {\n\t\t\tthrow new Error(`No tags found matching the pattern \"${tagPattern}\".`)\n\t\t}\n\t\tconst refs = new Map(\n\t\t\tremoteTags\n\t\t\t\t.trim()\n\t\t\t\t.split(`\\n`)\n\t\t\t\t.map((line) => {\n\t\t\t\t\tconst [sha, name] = line.split(`\\t`)\n\t\t\t\t\treturn [name, sha]\n\t\t\t\t}),\n\t\t)\n\t\t// ls-remote supplies the peeled commit for annotated tags.\n\t\tconst commit = refs.get(`${ref}^{}`) ?? refs.get(ref)\n\t\tif (!commit || !objectId.test(commit)) {\n\t\t\tthrow new Error(`Could not resolve the release commit for ${ref}.`)\n\t\t}\n\t\tconst commonGitDirectory = await git.revparse([\n\t\t\t`--path-format=absolute`,\n\t\t\t`--git-common-dir`,\n\t\t])\n\t\tawait withDirectoryLock(\n\t\t\tpath.join(commonGitDirectory, `breakage-fetch.lock`),\n\t\t\t() => git.fetch([`origin`, commit, `--no-tags`, `--no-write-fetch-head`]),\n\t\t)\n\t\tif ((await git.raw([`cat-file`, `-t`, commit])).trim() !== `commit`) {\n\t\t\tthrow new Error(`Release ${ref} does not resolve to a commit.`)\n\t\t}\n\t\tconst baseline: BreakageBaseline = {\n\t\t\tversion: 1,\n\t\t\ttagPattern: tagPattern ?? null,\n\t\t\tpackageDirectory: await git.revparse([`--show-prefix`]),\n\t\t\tref,\n\t\t\tcommit,\n\t\t}\n\t\tawait mkdir(path.dirname(filename), { recursive: true })\n\t\tawait writeFile(temporaryPath, `${JSON.stringify(baseline, null, 2)}\\n`, {\n\t\t\tflag: `wx`,\n\t\t})\n\t\tawait rename(temporaryPath, filename)\n\t\treturn baseline\n\t} catch (cause) {\n\t\t// A failed refresh must never leave an older snapshot available as fallback.\n\t\tif (canInvalidate) await rm(filename, { force: true })\n\t\tthrow new Error(`Failed to prepare baseline ${filename}.`, { cause })\n\t} finally {\n\t\tawait rm(temporaryPath, { force: true })\n\t}\n}\n\nexport async function readBaseline(\n\tgit: SimpleGit,\n\tfilename: string,\n\t{ tagPattern }: BaselineOptions,\n): Promise<BreakageBaseline> {\n\ttry {\n\t\tconst baseline: unknown = JSON.parse(await readFile(filename, `utf8`))\n\t\tif (!isBaseline(baseline)) throw new Error(`Invalid baseline fields.`)\n\t\tif (\n\t\t\tbaseline.tagPattern !== (tagPattern ?? null) ||\n\t\t\tbaseline.packageDirectory !== (await git.revparse([`--show-prefix`]))\n\t\t) {\n\t\t\tthrow new Error(`Baseline belongs to a different package or tag pattern.`)\n\t\t}\n\t\tif (\n\t\t\t(\n\t\t\t\tawait git.raw([`check-ref-format`, `--normalize`, baseline.ref])\n\t\t\t).trim() !== baseline.ref\n\t\t) {\n\t\t\tthrow new Error(`Invalid release ref.`)\n\t\t}\n\t\tif (\n\t\t\t(await git.raw([`cat-file`, `-t`, baseline.commit])).trim() !== `commit`\n\t\t) {\n\t\t\tthrow new Error(`Pinned object is not a locally available commit.`)\n\t\t}\n\t\treturn baseline\n\t} catch (cause) {\n\t\tthrow new Error(\n\t\t\t`Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`,\n\t\t\t{ cause },\n\t\t)\n\t}\n}\n","import { randomUUID } from \"node:crypto\"\nimport {\n\tchmod,\n\tlstat,\n\tmkdir,\n\treaddir,\n\treadFile,\n\trealpath,\n\trename,\n\trm,\n\twriteFile,\n} from \"node:fs/promises\"\nimport path from \"node:path\"\n\nimport type { SimpleGit } from \"simple-git\"\nimport { simpleGit } from \"simple-git\"\n\nimport { withDirectoryLock } from \"./directory-lock.ts\"\n\ntype ActiveCheck = {\n\tpid: number\n\tpaths: string[]\n\thead: string\n\trecoveryRequired?: boolean\n}\n\nfunction isActiveCheck(value: unknown): value is ActiveCheck {\n\tif (typeof value !== `object` || value === null) return false\n\tconst record = value as Partial<ActiveCheck>\n\treturn (\n\t\ttypeof record.pid === `number` &&\n\t\tNumber.isSafeInteger(record.pid) &&\n\t\trecord.pid > 0 &&\n\t\ttypeof record.head === `string` &&\n\t\t/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(record.head) &&\n\t\t(record.recoveryRequired === undefined ||\n\t\t\ttypeof record.recoveryRequired === `boolean`) &&\n\t\tArray.isArray(record.paths) &&\n\t\trecord.paths.length > 0 &&\n\t\trecord.paths.every(\n\t\t\t(file) =>\n\t\t\t\ttypeof file === `string` &&\n\t\t\t\t!path.isAbsolute(file) &&\n\t\t\t\t!path.win32.isAbsolute(file) &&\n\t\t\t\tfile\n\t\t\t\t\t.split(`/`)\n\t\t\t\t\t.every((part) => part !== `` && part !== `.` && part !== `..`),\n\t\t)\n\t)\n}\n\nasync function readRecoveryRecord(recordPath: string): Promise<ActiveCheck> {\n\ttry {\n\t\tconst record: unknown = JSON.parse(await readFile(recordPath, `utf8`))\n\t\tif (!isActiveCheck(record))\n\t\t\tthrow new TypeError(`Invalid recovery record fields`)\n\t\treturn record\n\t} catch (cause) {\n\t\tthrow new Error(\n\t\t\t`A breakage run needs recovery. The record ${recordPath} is unreadable or invalid. Verify no check is running and recover its test files before removing this record.`,\n\t\t\t{ cause },\n\t\t)\n\t}\n}\n\nasync function writeRecoveryRecord(\n\trecordPath: string,\n\trecord: ActiveCheck,\n): Promise<void> {\n\tconst temporaryPath = `${recordPath}.${randomUUID()}.tmp`\n\ttry {\n\t\tawait writeFile(temporaryPath, JSON.stringify(record), { flag: `wx` })\n\t\tawait rename(temporaryPath, recordPath)\n\t} finally {\n\t\tawait rm(temporaryPath, { force: true })\n\t}\n}\n\ntype TestFileState = {\n\tisClean: () => Promise<boolean>\n\treplaceTests: (\n\t\trelease: string,\n\t\tfiles: string[],\n\t) => Promise<() => Promise<void>>\n}\n\nfunction assertContained(root: string, filename: string): void {\n\tconst relative = path.relative(root, filename)\n\tif (\n\t\trelative === `..` ||\n\t\trelative.startsWith(`..${path.sep}`) ||\n\t\tpath.isAbsolute(relative)\n\t) {\n\t\tthrow new Error(`Cannot replace tests outside the worktree: ${filename}.`)\n\t}\n}\n\nasync function validateParents(\n\troot: string,\n\tfilenames: string[],\n): Promise<void> {\n\tconst checked = new Set<string>()\n\tfor (const filename of filenames) {\n\t\tassertContained(root, filename)\n\t\tfor (let parent = path.dirname(filename); ; parent = path.dirname(parent)) {\n\t\t\tif (checked.has(parent)) break\n\t\t\tconst stats = await lstat(parent).catch(\n\t\t\t\t(thrown: NodeJS.ErrnoException) => {\n\t\t\t\t\tif (thrown.code !== `ENOENT`) throw thrown\n\t\t\t\t},\n\t\t\t)\n\t\t\tif (stats && !stats.isDirectory())\n\t\t\t\tthrow new Error(`Cannot replace tests through ${parent}.`)\n\t\t\tchecked.add(parent)\n\t\t\tif (parent === root) break\n\t\t}\n\t}\n}\n\n// This lock covers Git setup and restoration only. Commands run after it is released.\nexport async function withTestFileState<T>(\n\tgit: SimpleGit,\n\tbaseDirname: string,\n\taction: (state: TestFileState) => Promise<T>,\n): Promise<T> {\n\tconst root = await realpath(await git.revparse([`--show-toplevel`]))\n\tbaseDirname = await realpath(baseDirname)\n\tassertContained(root, baseDirname)\n\tconst stateDirectory = path.join(\n\t\tawait git.revparse([`--absolute-git-dir`]),\n\t\t`breakage`,\n\t)\n\tawait mkdir(stateDirectory, { recursive: true })\n\tconst lock = path.join(stateDirectory, `lock`)\n\treturn withDirectoryLock(lock, async () => {\n\t\tconst activeChecks: ActiveCheck[] = []\n\t\tfor (const entry of await readdir(stateDirectory)) {\n\t\t\tif (!entry.endsWith(`.json`)) continue\n\t\t\tconst recordPath = path.join(stateDirectory, entry)\n\t\t\tconst record = await readRecoveryRecord(recordPath)\n\t\t\tlet interrupted = record.recoveryRequired\n\t\t\ttry {\n\t\t\t\tprocess.kill(record.pid, 0)\n\t\t\t} catch (thrown) {\n\t\t\t\tif ((thrown as NodeJS.ErrnoException).code !== `EPERM`)\n\t\t\t\t\tinterrupted = true\n\t\t\t}\n\t\t\tif (interrupted)\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`A breakage run needs recovery. Restore the test paths recorded in ${recordPath} before removing that record.`,\n\t\t\t\t)\n\t\t\tactiveChecks.push(record)\n\t\t}\n\t\tconst activePaths = activeChecks.flatMap((record) => record.paths)\n\t\treturn action({\n\t\t\tisClean: async () =>\n\t\t\t\t!(await simpleGit(root).raw([\n\t\t\t\t\t`--no-optional-locks`,\n\t\t\t\t\t`status`,\n\t\t\t\t\t`--porcelain`,\n\t\t\t\t\t`--untracked-files=all`,\n\t\t\t\t\t`--`,\n\t\t\t\t\t`.`,\n\t\t\t\t\t...activePaths.map((file) => `:(top,literal,exclude)${file}`),\n\t\t\t\t])),\n\t\t\treplaceTests: async (release, files) => {\n\t\t\t\tconst paths = files.map((file) =>\n\t\t\t\t\tpath\n\t\t\t\t\t\t.relative(root, path.resolve(baseDirname, file))\n\t\t\t\t\t\t.split(path.sep)\n\t\t\t\t\t\t.join(`/`),\n\t\t\t\t)\n\t\t\t\tfor (const file of paths) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tactivePaths.some(\n\t\t\t\t\t\t\t(active) =>\n\t\t\t\t\t\t\t\tfile === active ||\n\t\t\t\t\t\t\t\tfile.startsWith(`${active}/`) ||\n\t\t\t\t\t\t\t\tactive.startsWith(`${file}/`),\n\t\t\t\t\t\t)\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`Another breakage run is using ${file}. Concurrent checks must use disjoint test paths.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst head = await git.revparse([`HEAD`])\n\t\t\t\tconst currentFiles = new Set(\n\t\t\t\t\t(await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, head]))\n\t\t\t\t\t\t.split(`\\0`)\n\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t)\n\t\t\t\tconst existing = files.filter((file) => currentFiles.has(file))\n\t\t\t\tconst absent = files.filter((file) => !currentFiles.has(file))\n\t\t\t\tconst modes = new Map<string, number>()\n\t\t\t\tconst filenames = files.map((file) => path.resolve(baseDirname, file))\n\t\t\t\tawait validateParents(root, filenames)\n\t\t\t\tfor (const file of files) {\n\t\t\t\t\tconst filename = path.resolve(baseDirname, file)\n\n\t\t\t\t\tconst stats = await lstat(filename).catch(\n\t\t\t\t\t\t(thrown: NodeJS.ErrnoException) => {\n\t\t\t\t\t\t\tif (thrown.code !== `ENOENT`) throw thrown\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t\tif (!currentFiles.has(file) && stats)\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`Cannot overwrite the existing untracked path ${filename}.`,\n\t\t\t\t\t\t)\n\t\t\t\t\tif (stats?.isFile()) modes.set(filename, stats.mode)\n\t\t\t\t}\n\t\t\t\tconst recordPath = path.join(stateDirectory, `${randomUUID()}.json`)\n\t\t\t\tconst record: ActiveCheck = { pid: process.pid, paths, head }\n\t\t\t\tawait writeRecoveryRecord(recordPath, record)\n\t\t\t\tconst restore = async () => {\n\t\t\t\t\tawait writeRecoveryRecord(recordPath, {\n\t\t\t\t\t\t...record,\n\t\t\t\t\t\trecoveryRequired: true,\n\t\t\t\t\t})\n\t\t\t\t\tawait validateParents(root, filenames)\n\t\t\t\t\tif (existing.length)\n\t\t\t\t\t\tawait git.raw([\n\t\t\t\t\t\t\t`--literal-pathspecs`,\n\t\t\t\t\t\t\t`restore`,\n\t\t\t\t\t\t\t`--source=${head}`,\n\t\t\t\t\t\t\t`--worktree`,\n\t\t\t\t\t\t\t`--`,\n\t\t\t\t\t\t\t...existing,\n\t\t\t\t\t\t])\n\t\t\t\t\tfor (const file of absent)\n\t\t\t\t\t\tawait rm(path.resolve(baseDirname, file), { force: true })\n\t\t\t\t\tfor (const [filename, mode] of modes) await chmod(filename, mode)\n\t\t\t\t\tawait rm(recordPath)\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait git.raw([\n\t\t\t\t\t\t`--literal-pathspecs`,\n\t\t\t\t\t\t`restore`,\n\t\t\t\t\t\t`--source=${release}`,\n\t\t\t\t\t\t`--worktree`,\n\t\t\t\t\t\t`--`,\n\t\t\t\t\t\t...files,\n\t\t\t\t\t])\n\t\t\t\t} catch (thrown) {\n\t\t\t\t\tawait restore()\n\t\t\t\t\tthrow thrown\n\t\t\t\t}\n\t\t\t\treturn () => withDirectoryLock(lock, restore, true)\n\t\t\t},\n\t\t})\n\t})\n}\n","import { exec } from \"node:child_process\"\nimport path from \"node:path\"\n\nimport { minimatch } from \"minimatch\"\nimport { simpleGit } from \"simple-git\"\nimport type { Chronicle } from \"takua\"\nimport logger from \"takua\"\n\nimport { readBaseline } from \"./baseline.ts\"\nimport { withDirectoryLock } from \"./directory-lock.ts\"\nimport { latestReleaseTag as selectLatestReleaseTag } from \"./release-tag.ts\"\nimport { withTestFileState } from \"./test-file-state.ts\"\n\nexport type { BreakageBaseline, BreakagePreludeOptions } from \"./baseline.ts\"\nexport { breakagePrelude } from \"./baseline.ts\"\n\nexport type BreakageOptions = {\n\ttagPattern?: string | undefined\n\tbaselineFile?: string | undefined\n\ttestPattern: string\n\ttestCommand: string\n\tcertifyCommand: string\n\tbaseDirname?: string\n\tverbose?: boolean | undefined\n}\n\nexport type BreakageOutcome =\n\t| { gitWasClean: false }\n\t| {\n\t\t\tgitWasClean: true\n\t\t\tlastReleaseFound: false\n\t }\n\t| {\n\t\t\tgitWasClean: true\n\t\t\tlastReleaseFound: true\n\t\t\tlastReleaseTag: string\n\t\t\ttestsWereFound: false\n\t }\n\t| {\n\t\t\tgitWasClean: true\n\t\t\tlastReleaseFound: true\n\t\t\tlastReleaseTag: string\n\t\t\ttestsWereFound: true\n\t\t\ttestsFound: string[]\n\t\t\tbreakingChangesFound: false\n\t }\n\t| {\n\t\t\tgitWasClean: true\n\t\t\tlastReleaseFound: true\n\t\t\tlastReleaseTag: string\n\t\t\ttestsWereFound: true\n\t\t\ttestsFound: string[]\n\t\t\tbreakingChangesFound: true\n\t\t\ttestResult: string\n\t\t\tbreakingChangesCertified: boolean\n\t\t\tcertificationStdout: string\n\t\t\tcertificationStderr: string\n\t }\n\nexport async function breakage({\n\ttagPattern,\n\tbaselineFile,\n\ttestPattern,\n\ttestCommand,\n\tcertifyCommand,\n\tbaseDirname = process.cwd(),\n\tverbose = false,\n}: BreakageOptions): Promise<BreakageOutcome & { summary: string }> {\n\tlet mark: Chronicle[`mark`] | undefined\n\tlet logMarks: Chronicle[`logMarks`] | undefined\n\tif (verbose) {\n\t\tconst chronicle = logger.makeChronicle()\n\t\tmark = chronicle.mark\n\t\tlogMarks = chronicle.logMarks\n\t}\n\tmark?.(`breakage`)\n\tconst git = simpleGit(baseDirname)\n\tmark?.(`spawn git`)\n\tconst isGitRepo = await git.checkIsRepo()\n\tmark?.(`is git repository`)\n\tif (!isGitRepo) {\n\t\treturn {\n\t\t\tsummary: `The git repository must be clean to run this command.`,\n\t\t\tgitWasClean: false,\n\t\t}\n\t}\n\tif (\n\t\t!(await withTestFileState(git, baseDirname, (state) => state.isClean()))\n\t) {\n\t\treturn {\n\t\t\tsummary: `The git repository must be clean to run this command.`,\n\t\t\tgitWasClean: false,\n\t\t}\n\t}\n\tlet latestReleaseTag: string | undefined\n\tlet releaseCommit: string\n\tif (baselineFile !== undefined) {\n\t\tconst baseline = await readBaseline(\n\t\t\tgit,\n\t\t\tpath.resolve(baseDirname, baselineFile),\n\t\t\t{ tagPattern },\n\t\t)\n\t\tlatestReleaseTag = baseline.ref\n\t\treleaseCommit = baseline.commit\n\t\tmark?.(`read pinned release baseline`)\n\t} else {\n\t\tconst tagsRemote = await git.listRemote([`--tags`, `origin`])\n\n\t\tmark?.(`list remote tags`)\n\t\tlatestReleaseTag = selectLatestReleaseTag(tagsRemote, tagPattern)\n\t\tmark?.(`found latest release tag`)\n\t\tif (!latestReleaseTag) {\n\t\t\treturn {\n\t\t\t\tsummary: `No tags found matching the pattern \"${tagPattern}\".`,\n\t\t\t\tgitWasClean: true,\n\t\t\t\tlastReleaseFound: false,\n\t\t\t} as const\n\t\t}\n\t\tconst commonGitDirectory = await git.revparse([\n\t\t\t`--path-format=absolute`,\n\t\t\t`--git-common-dir`,\n\t\t])\n\t\tawait withDirectoryLock(\n\t\t\tpath.join(commonGitDirectory, `breakage-fetch.lock`),\n\t\t\t() =>\n\t\t\t\tgit.fetch([\n\t\t\t\t\t`origin`,\n\t\t\t\t\t`${latestReleaseTag}:${latestReleaseTag}`,\n\t\t\t\t\t`--no-tags`,\n\t\t\t\t\t`--no-write-fetch-head`,\n\t\t\t\t]),\n\t\t)\n\n\t\treleaseCommit = await git.revparse([`${latestReleaseTag}^{commit}`])\n\t\tmark?.(`fetched latest release tag`)\n\t}\n\n\tlet productionFiles: string[]\n\ttry {\n\t\tproductionFiles = (\n\t\t\tawait git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, releaseCommit])\n\t\t)\n\t\t\t.split(`\\0`)\n\t\t\t.filter(Boolean)\n\t} catch (thrown) {\n\t\tmark?.(`failed to list production files`)\n\t\tconst message =\n\t\t\tthrown instanceof Error\n\t\t\t\t? thrown.message\n\t\t\t\t: typeof thrown === `string`\n\t\t\t\t\t? thrown\n\t\t\t\t\t: undefined\n\t\tconst summary = `Failed to list production files${\n\t\t\tmessage ? `: ${message}` : `.`\n\t\t}`\n\t\treturn {\n\t\t\tsummary,\n\t\t\tgitWasClean: true,\n\t\t\tlastReleaseFound: true,\n\t\t\tlastReleaseTag: latestReleaseTag,\n\t\t\ttestsWereFound: false,\n\t\t} as const\n\t}\n\tmark?.(`listed all files checked into git`)\n\tconst productionTestFiles = productionFiles.filter((file) =>\n\t\tminimatch(file, testPattern),\n\t)\n\tmark?.(`filtered to public test files`)\n\n\tconst prepared = await withTestFileState(git, baseDirname, async (state) => {\n\t\tif (!(await state.isClean())) {\n\t\t\treturn {\n\t\t\t\tsummary: `The git repository must be clean to run this command.`,\n\t\t\t\tgitWasClean: false,\n\t\t\t} as const\n\t\t}\n\n\t\tif (productionTestFiles.length === 0) {\n\t\t\treturn {\n\t\t\t\tsummary: `No tests were found matching the pattern \"${testPattern}\".`,\n\t\t\t\tgitWasClean: true,\n\t\t\t\tlastReleaseFound: true,\n\t\t\t\tlastReleaseTag: latestReleaseTag,\n\t\t\t\ttestsWereFound: false,\n\t\t\t} as const\n\t\t}\n\t\tconst restore = await state.replaceTests(releaseCommit, productionTestFiles)\n\t\tmark?.(`restored public tests from latest release tag`)\n\t\treturn { latestReleaseTag, productionTestFiles, restore }\n\t})\n\tif (prepared.summary !== undefined) return prepared\n\tconst { restore } = prepared\n\n\ttry {\n\t\tconst noBreakingChangesDetected = await new Promise<\n\t\t\tBreakageOutcome & { summary: string }\n\t\t>((resolve, reject) => {\n\t\t\tconst result = exec(\n\t\t\t\ttestCommand,\n\t\t\t\t{ cwd: baseDirname },\n\t\t\t\t(_, __, stderr) => {\n\t\t\t\t\tif (result.exitCode === 0) {\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tsummary: `No breaking changes were detected.`,\n\t\t\t\t\t\t\tgitWasClean: true,\n\t\t\t\t\t\t\tlastReleaseFound: true,\n\t\t\t\t\t\t\tlastReleaseTag: latestReleaseTag,\n\t\t\t\t\t\t\ttestsWereFound: true,\n\t\t\t\t\t\t\ttestsFound: productionTestFiles,\n\t\t\t\t\t\t\tbreakingChangesFound: false,\n\t\t\t\t\t\t})\n\t\t\t\t\t} else {\n\t\t\t\t\t\treject(stderr)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t)\n\t\t})\n\t\tmark?.(`passed tests`)\n\t\treturn noBreakingChangesDetected\n\t} catch (thrown) {\n\t\tmark?.(`failed tests`)\n\t\tconst breakingChangesCertified = await new Promise<\n\t\t\tBreakageOutcome & { summary: string }\n\t\t>((resolve) => {\n\t\t\tconst result = exec(\n\t\t\t\tcertifyCommand,\n\t\t\t\t{ cwd: baseDirname },\n\t\t\t\t(_, stdout, stderr) => {\n\t\t\t\t\tconst wereCertified = result.exitCode === 0\n\t\t\t\t\tmark?.(`${wereCertified ? `passed` : `failed`} certification`)\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tsummary: wereCertified\n\t\t\t\t\t\t\t? `Breaking changes were found and certified.`\n\t\t\t\t\t\t\t: `Breaking changes were found, but not certified.`,\n\t\t\t\t\t\tgitWasClean: true,\n\t\t\t\t\t\tlastReleaseFound: true,\n\t\t\t\t\t\tlastReleaseTag: latestReleaseTag,\n\t\t\t\t\t\ttestsWereFound: true,\n\t\t\t\t\t\ttestsFound: productionTestFiles,\n\t\t\t\t\t\tbreakingChangesFound: true,\n\t\t\t\t\t\ttestResult: thrown as string,\n\t\t\t\t\t\tbreakingChangesCertified: wereCertified,\n\t\t\t\t\t\tcertificationStdout: stdout,\n\t\t\t\t\t\tcertificationStderr: stderr,\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t)\n\t\t})\n\t\treturn breakingChangesCertified\n\t} finally {\n\t\tawait restore()\n\t\tmark?.(`restored original public tests`)\n\t\tlogMarks?.()\n\t}\n}\n"],"mappings":"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"}
|