breakage 0.8.1 → 0.8.2

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 CHANGED
@@ -28,13 +28,13 @@ breakage distinguishes two kinds of tests:
28
28
  1. Public tests record behavior consumers can rely on across releases. breakage restores their released versions so a proposed change cannot weaken an assertion alongside the implementation it checks.
29
29
  2. Private tests cover implementation details, development diagnostics, and other behavior you want to verify without preserving it as a release commitment. They can detect real bugs too; they are not used as the historical compatibility contract.
30
30
 
31
- Identify the public tests with a glob pattern and provide a command that builds the current implementation as needed and runs those tests once. A passing comparison means the selected released tests passed; its strength depends on the promises those tests actually protect.
31
+ Identify the public tests with a glob pattern and provide a command that runs those tests once against source. The tested package's build and current-test preflight checks belong in independent jobs. Upstream dependency builds, such as Turbo's `^build`, can run before compatibility checks. Individual public contracts may build a disposable package or fixture when published entries, declaration files, or compiled runtimes are themselves the behavior being verified. A passing comparison means the selected released tests passed; its strength depends on the promises those tests actually protect.
32
32
 
33
33
  ## help
34
34
 
35
- Run `breakage help` to show usage for the CLI, including the check options and `schema` command. Like `schema`, the `help` command skips configuration discovery; it works without required check options and even when `breakage.config.json` is malformed. It does not run checks or write schema files.
35
+ Run `breakage help` to show usage for the CLI, including the check options, `prelude`, and `schema` commands. Like `schema`, the `help` command skips configuration discovery; it works without required check options and even when `breakage.config.json` is malformed. It does not run checks or write schema files.
36
36
 
37
- Use `breakage help` instead of the former `--help` or `-h` options. Configuration no longer controls help. The empty route and a configuration path still run checks; if your configuration file is named `help`, pass `./help` to select it explicitly.
37
+ Use `breakage help` instead of the former `--help` or `-h` options. Configuration no longer controls help. The empty route and a configuration path still run checks; if your configuration file is named `help` or `prelude`, pass an explicit path such as `./help` to select it explicitly.
38
38
 
39
39
  ## examples
40
40
 
@@ -66,13 +66,67 @@ In both examples, a failed test command remains uncertified and makes the CLI ex
66
66
 
67
67
  Release tags may use `1.2.3`, `v1.2.3`, `package@1.2.3`, or `@scope/package@1.2.3`, including valid prerelease and build identifiers. The newest matching version is selected by semantic-version precedence; tags without a supported version are ignored.
68
68
 
69
+ ## caching compatibility checks
70
+
71
+ The ordinary `breakage` command discovers the newest release on `origin` each time it runs. Do not cache that whole command: a new release or a moved tag can change its baseline without changing any tracked checkout files.
72
+
73
+ For caching, run an uncached prelude that resolves and fetches the release, then cache the check with the generated snapshot as an input. Add `.breakage/` to your package's `.gitignore` first; the prelude requires a gitignored, untracked output path so the snapshot does not interfere with the clean-working-tree requirement.
74
+
75
+ ```sh
76
+ breakage prelude --out .breakage/baseline.json
77
+ breakage --baseline-file .breakage/baseline.json
78
+ ```
79
+
80
+ Both commands discover `breakage.config.json` and accept the same `--tag-pattern` and `--base-dir` options. An explicit configuration path works as `breakage prelude ./custom.config.json --out .breakage/baseline.json` and `breakage ./custom.config.json --baseline-file .breakage/baseline.json`. The prelude does not require test or certification options, and never runs those commands or builds the package. Existing full check configurations can be reused. Output and baseline-file paths are relative to `baseDirname`, which defaults to the current working directory. The default prelude output is `.breakage/baseline.json`.
81
+
82
+ The versioned JSON snapshot contains the selected tag ref, its resolved commit SHA, the tag pattern, and the package directory relative to the repository root. Annotated tags are peeled to commits. Contents are deterministic and written atomically, with no timestamp or nonce. Fetching uses the SHA from discovery rather than a mutable tag name. An unchanged baseline produces identical snapshot bytes; a newer release or a moved tag targeting another commit changes them. A failed discovery, fetch, or write fails the prelude and removes an older snapshot at the validated output path.
83
+
84
+ The pinned check reads that locally available commit without contacting `origin` or discovering tags. A release published between prelude and check does not change this invocation's baseline. Missing, malformed, incompatible, or unavailable snapshots fail the check; rerun the prelude to refresh them. Each package or configuration needs its own snapshot path. Test execution, certification, Git locking, and restoration follow the same lifecycle as the ordinary command.
85
+
86
+ ### Turbo
87
+
88
+ Add these scripts to the tested package, preserving your existing runner and certification configuration:
89
+
90
+ ```json
91
+ {
92
+ "scripts": {
93
+ "test:breaks:prelude": "breakage prelude --out .breakage/baseline.json",
94
+ "test:breaks": "breakage --baseline-file .breakage/baseline.json"
95
+ }
96
+ }
97
+ ```
98
+
99
+ Use [Turbo's deferred hashing](https://turborepo.dev/docs/reference/configuration#deferred-hashing) so the snapshot is hashed after the uncached prelude completes. This configuration is exercised with Turbo 2.11.7:
100
+
101
+ ```json
102
+ {
103
+ "tasks": {
104
+ "test:breaks:prelude": {
105
+ "cache": false
106
+ },
107
+ "test:breaks": {
108
+ "dependsOn": ["test:breaks:prelude", "^build"],
109
+ "inputs": [
110
+ "$TURBO_DEFAULT$",
111
+ { "mode": "jit", "globs": [".breakage/baseline.json"] }
112
+ ],
113
+ "cache": true
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ Run `turbo run test:breaks` as usual. The prelude runs even when the compatibility check hits its cache; a failed prelude prevents the downstream task from replaying a cached success. Keep all additional runner, helper, configuration, environment, and certification inputs in the compatibility task's hash, including release-plan files outside the package directory. Keep upstream `^build` prerequisites; the tested package does not need a self-build preflight. For runners without deferred hashing, finish the prelude before starting the task runner and include its snapshot in the check's cache key.
120
+
121
+ The JavaScript API exports `breakagePrelude({ out, tagPattern, baseDirname })` and accepts `baselineFile` in `breakage` options. Without `baselineFile`, the existing single-command discovery behavior is preserved.
122
+
69
123
  ## writing consumer contracts
70
124
 
71
125
  Before preserving an assertion in a release, answer two questions: which consumer capability would its failure demonstrate is broken, and which harmless implementation changes should it continue to allow? A test named "public API" is not enough. Read every assertion as a promise you may need to keep after its original author and implementation have changed.
72
126
 
73
127
  ### choose the behavior deliberately
74
128
 
75
- Test imports through the entrypoints consumers use. For a published library, build its distributable output and check declarations as well as runtime behavior where relevant. Importing an internal source file can miss broken package exports while accidentally requiring that source path to survive refactoring.
129
+ Test imports through the entrypoints consumers use. Resolve the tested package's consumer import names to source in the test runner so compatibility checks do not require its own build or freeze internal source paths. Upstream dependencies may use their built entrypoints. Check distributable output and declarations separately in the build and type-check jobs.
76
130
 
77
131
  Write focused scenarios for the capabilities you want to support. Include combinations that make resource independence observable: two fonts or images on one page, multiple registrations, or two distinct handles. Exercising a method once does not establish that its effect survives serialization or that it works alongside another instance.
78
132
 
@@ -113,9 +167,9 @@ Mondrian publishes its independent inspection tools through its testing entrypoi
113
167
  }
114
168
  ```
115
169
 
116
- This illustrates the restore boundary; `test:public` is a project-provided command that rebuilds the package, checks consumer types, and runs its public suite. `false` keeps detected breaks uncertified until the project supplies its release-plan check.
170
+ This illustrates the restore boundary; `test:public` is a project-provided command that only runs its public suite against source. Consumer type checks and builds have separate commands. `false` keeps detected breaks uncertified until the project supplies its release-plan check.
117
171
 
118
- Match the assertions and observation code, and leave the implementation they are meant to evaluate outside that boundary. When an observer also ships as a library feature, its restored implementation is trusted test infrastructure for this comparison; verify its current implementation separately too. Build after restoration so compiled observation code reflects the restored source.
172
+ Match the assertions and observation code, and leave the implementation they are meant to evaluate outside that boundary. When an observer also ships as a library feature, its restored implementation is trusted test infrastructure for this comparison; verify its current implementation separately too. Execute observation source directly after restoration so historical readers run without a build.
119
173
 
120
174
  breakage restores matched Git files, not installed dependencies. A shared package manifest and workspace lockfile keep setup simple, but dependency upgrades must keep restored readers runnable. If a reader needs its own dependency versions, include that dependency description in the restore boundary and make your test command install it. Merely matching a lockfile does not perform an installation. Protect runner configuration and other support files when they determine how historical assertions execute.
121
175
 
@@ -138,7 +192,7 @@ Also verify the restoration mechanism once: change a current observation helper
138
192
 
139
193
  ### run current contracts and historical contracts
140
194
 
141
- Run the current public suite before invoking `breakage`, so newly added contracts are checked too. Let the CLI run the released suite through `testCommand`. Configure the command to fail when it selects no tests, build and type-check the package when needed, and avoid cached test results that can bypass execution of restored files. Tests should run once and terminate.
195
+ Run the current suite in a parallel test job so newly added contracts are checked independently. Let the CLI run only the released suite against source through `testCommand`. Configure the command to fail when it selects no tests and avoid cached test results that can bypass execution of restored files. Builds and type checks have separate commands and jobs. Tests should run once and terminate.
142
196
 
143
197
  Run the check from a clean checkout with access to `origin` and release tags. An initial release without matched public tests provides no baseline: preserve the CLI's inconclusive result until a release includes them. A missing baseline is not evidence of compatibility.
144
198
 
@@ -166,58 +220,23 @@ If a process is forcibly interrupted or restoration fails, breakage retains reco
166
220
 
167
221
  ## options
168
222
 
169
- <!--gen-->
170
- <!--cli-options HASH-->
171
- <table>
172
- <tr>
173
- <th>option</th>
174
- <th>shorthand</th>
175
- <th>required</th>
176
- <th>description</th>
177
- <th>example</th>
178
- </tr>
179
- <tr>
180
- <td><code>--tagPattern</code></td>
181
- <td><code>-p</code></td>
182
- <td></td>
183
- <td>String which, if found in a git tag, will be considered a release tag for your library.</td>
184
- <td><code>--tagPattern="my-library"</code></td>
185
- </tr>
186
- <tr>
187
- <td><code>--testPattern</code></td>
188
- <td><code>-t</code></td>
189
- <td>✔</td>
190
- <td>Glob pattern to identify files containing public tests.</td>
191
- <td><code>--testPattern="*__public.test.ts"</code></td>
192
- </tr>
193
- <tr>
194
- <td><code>--testCommand</code></td>
195
- <td><code>-c</code></td>
196
- <td>✔</td>
197
- <td>Command to run to run the public tests.</td>
198
- <td><code>--testCommand="npm run test"</code></td>
199
- </tr>
200
- <tr>
201
- <td><code>--certifyCommand</code></td>
202
- <td><code>-C</code></td>
203
- <td></td>
204
- <td>Command to run to certify that breaking changes have been detected.</td>
205
- <td><code>--certifyCommand="grep -q '"my-library": major' $(find ${DIR_PATH}/.changesets -type f) && exit 0 || exit 1"</code></td>
206
- </tr>
207
- <tr>
208
- <td><code>--baseDirname</code></td>
209
- <td><code>-b</code></td>
210
- <td></td>
211
- <td>Directory in which to run the tests and certify the breaking changes.</td>
212
- <td><code>--baseDirname="."</code></td>
213
- </table>
214
- <!--gen-->
223
+ | Check option | Shorthand | Required | Purpose |
224
+ | ------------------ | --------- | -------- | ------------------------------------------------- |
225
+ | `--tagPattern` | `-g` | | RegExp selecting release tags. |
226
+ | `--testPattern` | `-p` | Yes | Glob selecting released public tests and helpers. |
227
+ | `--testCommand` | `-t` | Yes | Command running the restored public contracts. |
228
+ | `--certifyCommand` | `-c` | Yes | Command certifying intentional breaking changes. |
229
+ | `--baseDirname` | `-d` | | Working directory for Git and commands. |
230
+ | `--baselineFile` | | | Prelude snapshot pinning the release commit. |
231
+ | `--verbose` | `-v` | | Print timing information for the check. |
232
+
233
+ `prelude --out` sets its gitignored snapshot path. `schema --outdir` sets the schema output directory. Run `breakage help` for the full option descriptions and examples.
215
234
 
216
235
  ## cli completion
217
236
 
218
237
  With `breakage` installed on PATH, run `breakage completion install bash` to install Bash completion. Replace `bash` with `zsh`, `fish`, `nushell`, or `carapace` for the other supported integrations. Installation uses the shell's existing completion setup and does not edit shell profiles; open a new shell afterward. `breakage completion bash` prints the integration for manual installation. See [Comline's shell setup requirements](https://github.com/jeremybanka/wayforge/tree/main/packages/comline#shell-integrations).
219
238
 
220
- Completion suggests commands, option names, config file paths, and directories for `--base-dir` and `schema --out-dir`. It works without a valid config file or required option values and does not run checks. Singleton options disappear from suggestions after use; parsing behavior is unchanged. The completion transport reserves its management and protocol command names; use an explicit path such as `./completion` for a config file whose name collides with a reserved command.
239
+ Completion suggests commands, option names, config file paths, snapshot paths for `--baseline-file` and `prelude --out`, and directories for `--base-dir` and `schema --out-dir`. It works without a valid config file or required option values and does not run checks. Singleton options disappear from suggestions after use; parsing behavior is unchanged. The completion transport reserves its management and protocol command names; use an explicit path such as `./completion` for a config file whose name collides with a reserved command.
221
240
 
222
241
  ## cli option aliases and warnings
223
242
 
@@ -230,6 +249,7 @@ These aliases work alongside the original option names. Configuration file keys
230
249
  | `--testCommand` | `--test-command` |
231
250
  | `--certifyCommand` | `--certify-command` |
232
251
  | `--baseDirname` | `--base-dir`, `--base-dirname` |
252
+ | `--baselineFile` | `--baseline-file` |
233
253
  | `--outdir` (schema command) | `--out-dir` |
234
254
 
235
255
  After successful parsing, breakage warns on stderr about unknown options and options that do not apply to the selected command. Warnings appear before check output is captured and do not change the command's exit status.
@@ -3,9 +3,9 @@ import path from "node:path";
3
3
  import { minimatch } from "minimatch";
4
4
  import { simpleGit } from "simple-git";
5
5
  import logger from "takua";
6
+ import { randomUUID } from "node:crypto";
6
7
  import { chmod, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
7
8
  import { setTimeout } from "node:timers/promises";
8
- import { randomUUID } from "node:crypto";
9
9
  //#region src/directory-lock.ts
10
10
  async function withDirectoryLock(lock, action, waitForCleanup = false) {
11
11
  const deadline = Date.now() + 6e4;
@@ -67,6 +67,86 @@ function latestReleaseTag(remoteTags, tagPattern) {
67
67
  return latest?.ref;
68
68
  }
69
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
70
150
  //#region src/test-file-state.ts
71
151
  function isActiveCheck(value) {
72
152
  if (typeof value !== `object` || value === null) return false;
@@ -212,7 +292,7 @@ async function withTestFileState(git, baseDirname, action) {
212
292
  }
213
293
  //#endregion
214
294
  //#region src/breakage.ts
215
- async function breakage({ tagPattern, testPattern, testCommand, certifyCommand, baseDirname = process.cwd(), verbose = false }) {
295
+ async function breakage({ tagPattern, baselineFile, testPattern, testCommand, certifyCommand, baseDirname = process.cwd(), verbose = false }) {
216
296
  let mark;
217
297
  let logMarks;
218
298
  if (verbose) {
@@ -233,23 +313,33 @@ async function breakage({ tagPattern, testPattern, testCommand, certifyCommand,
233
313
  summary: `The git repository must be clean to run this command.`,
234
314
  gitWasClean: false
235
315
  };
236
- const tagsRemote = await git.listRemote([`--tags`, `origin`]);
237
- mark?.(`list remote tags`);
238
- const latestReleaseTag$1 = latestReleaseTag(tagsRemote, tagPattern);
239
- mark?.(`found latest release tag`);
240
- if (!latestReleaseTag$1) return {
241
- summary: `No tags found matching the pattern "${tagPattern}".`,
242
- gitWasClean: true,
243
- lastReleaseFound: false
244
- };
245
- const commonGitDirectory = await git.revparse([`--path-format=absolute`, `--git-common-dir`]);
246
- await withDirectoryLock(path.join(commonGitDirectory, `breakage-fetch.lock`), () => git.fetch([
247
- `origin`,
248
- `${latestReleaseTag$1}:${latestReleaseTag$1}`,
249
- `--no-tags`,
250
- `--no-write-fetch-head`
251
- ]));
252
- mark?.(`fetched latest release tag`);
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
+ }
253
343
  let productionFiles;
254
344
  try {
255
345
  productionFiles = (await git.raw([
@@ -257,7 +347,7 @@ async function breakage({ tagPattern, testPattern, testCommand, certifyCommand,
257
347
  `-r`,
258
348
  `--name-only`,
259
349
  `-z`,
260
- latestReleaseTag$1
350
+ releaseCommit
261
351
  ])).split(`\0`).filter(Boolean);
262
352
  } catch (thrown) {
263
353
  mark?.(`failed to list production files`);
@@ -285,7 +375,7 @@ async function breakage({ tagPattern, testPattern, testCommand, certifyCommand,
285
375
  lastReleaseTag: latestReleaseTag$1,
286
376
  testsWereFound: false
287
377
  };
288
- const restore = await state.replaceTests(latestReleaseTag$1, productionTestFiles);
378
+ const restore = await state.replaceTests(releaseCommit, productionTestFiles);
289
379
  mark?.(`restored public tests from latest release tag`);
290
380
  return {
291
381
  latestReleaseTag: latestReleaseTag$1,
@@ -340,6 +430,6 @@ async function breakage({ tagPattern, testPattern, testCommand, certifyCommand,
340
430
  }
341
431
  }
342
432
  //#endregion
343
- export { breakage as t };
433
+ export { breakagePrelude as n, breakage as t };
344
434
 
345
- //# sourceMappingURL=breakage-D_nc-zEK.js.map
435
+ //# sourceMappingURL=breakage-Dc4IldeW.js.map
@@ -0,0 +1 @@
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":";;;;;;;;;AAGA,eAAsB,kBACrB,MACA,QACA,iBAAiB,OACJ;CACb,MAAM,WAAW,KAAK,IAAI,IAAI;CAC9B,SACC,IAAI;EACH,MAAM,MAAM,IAAI;EAChB;CACD,SAAS,QAAQ;EAChB,IAAK,OAAiC,SAAS,UAAU,MAAM;EAC/D,IAAI,CAAC,kBAAkB,KAAK,IAAI,KAAK,UACpC,MAAM,IAAI,MACT,yBAAyB,KAAK,gEAC/B;EAED,MAAM,WAAW,EAAE;CACpB;CAED,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,UAAU;EACT,MAAM,MAAM,IAAI;CACjB;AACD;;;AC1BA,SAAS,eAAe,GAAW,GAAmB;CACrD,OAAO,EAAE,SAAS,EAAE,WAAW,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK;AACzD;AAEA,SAAS,eAAe,GAAY,GAAoB;CACvD,KAAK,IAAI,QAAQ,GAAG,QAAQ,GAAG,SAAS;EACvC,MAAM,aAAa,eAAe,EAAE,KAAK,QAAS,EAAE,KAAK,MAAO;EAChE,IAAI,YAAY,OAAO;CACxB;CACA,IAAI,CAAC,EAAE,WAAW,UAAU,CAAC,EAAE,WAAW,QACzC,OAAO,OAAO,CAAC,EAAE,WAAW,MAAM,IAAI,OAAO,CAAC,EAAE,WAAW,MAAM;CAElE,KACC,IAAI,QAAQ,GACZ,QAAQ,KAAK,IAAI,EAAE,WAAW,QAAQ,EAAE,WAAW,MAAM,GACzD,SACC;EACD,MAAM,OAAO,EAAE,WAAW;EAC1B,MAAM,QAAQ,EAAE,WAAW;EAC3B,MAAM,cAAc,QAAQ,KAAK,IAAI;EACrC,MAAM,eAAe,QAAQ,KAAK,KAAK;EACvC,MAAM,aACL,eAAe,eACZ,eAAe,MAAM,KAAK,IAC1B,gBAAgB,eACf,OAAO,YAAY,IAAI,OAAO,WAAW,IACzC,OAAO,QACN,IACA,OAAO,QACN,KACA;EACP,IAAI,YAAY,OAAO;CACxB;CACA,OAAO,EAAE,WAAW,SAAS,EAAE,WAAW;AAC3C;AAEA,SAAgB,iBACf,YACA,YACqB;CACrB,MAAM,UAAU,aAAa,IAAI,OAAO,UAAU,IAAI,KAAA;CACtD,IAAI;CACJ,KAAK,MAAM,QAAQ,WAAW,MAAM,IAAI,GAAG;EAC1C,IAAI,WAAW,CAAC,QAAQ,KAAK,IAAI,GAAG;EACpC,MAAM,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,EAAE,EAAE,QAAQ,WAAW,EAAE;EACtD,IAAI,CAAC,KAAK,WAAW,YAAY,GAAG;EACpC,MAAM,OAAO,IAAI,MAAM,aAAa,MAAM;EAC1C,MAAM,UAAU,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;EACpD,MAAM,QACL,gIAAgI,KAC/H,OACD;EACD,IAAI,CAAC,OAAO;EACZ,MAAM,aAAa,MAAM,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC;EAC5C,IAAI,WAAW,MAAM,eAAe,SAAS,KAAK,UAAU,CAAC,GAAG;EAChE,MAAM,UAAmB;GAAE;GAAK,MAAM,MAAM,MAAM,GAAG,CAAC;GAAG;EAAW;EACpE,IAAI,CAAC,UAAU,eAAe,SAAS,MAAM,IAAI,GAAG,SAAS;CAC9D;CACA,OAAO,QAAQ;AAChB;;;ACpCA,MAAM,WAAW;AAEjB,SAAS,WAAW,OAA2C;CAC9D,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,SAAS;CACf,OACC,OAAO,YAAY,MAClB,OAAO,eAAe,QAAQ,OAAO,OAAO,eAAe,aAC5D,OAAO,OAAO,qBAAqB,YACnC,OAAO,OAAO,QAAQ,YACtB,OAAO,IAAI,WAAW,YAAY,KAClC,OAAO,OAAO,WAAW,YACzB,SAAS,KAAK,OAAO,MAAM;AAE7B;;AAGA,eAAsB,gBAAgB,EACrC,KACA,YACA,cAAc,QAAQ,IAAI,KAC2B;CACrD,MAAM,WAAW,KAAK,QAAQ,aAAa,GAAG;CAC9C,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,EAAE;CAClD,IAAI,gBAAgB;CACpB,IAAI;EACH,MAAM,MAAM,UAAU,WAAW;EAEjC,IAAI,EAAE,MAAM,IAAI,IAAI;GAAC;GAAgB;GAAM;EAAQ,CAAC,EAAA,CAAG,KAAK,GAC3D,MAAM,IAAI,MAAM,uCAAuC,SAAS,EAAE;EAEnE,gBAAgB;EAChB,MAAM,aAAa,MAAM,IAAI,WAAW,CAAC,UAAU,QAAQ,CAAC;EAC5D,MAAM,MAAM,iBAAiB,YAAY,UAAU;EACnD,IAAI,CAAC,KACJ,MAAM,IAAI,MAAM,uCAAuC,WAAW,GAAG;EAEtE,MAAM,OAAO,IAAI,IAChB,WACE,KAAK,CAAC,CACN,MAAM,IAAI,CAAC,CACX,KAAK,SAAS;GACd,MAAM,CAAC,KAAK,QAAQ,KAAK,MAAM,IAAI;GACnC,OAAO,CAAC,MAAM,GAAG;EAClB,CAAC,CACH;EAEA,MAAM,SAAS,KAAK,IAAI,GAAG,IAAI,IAAI,KAAK,KAAK,IAAI,GAAG;EACpD,IAAI,CAAC,UAAU,CAAC,SAAS,KAAK,MAAM,GACnC,MAAM,IAAI,MAAM,4CAA4C,IAAI,EAAE;EAEnE,MAAM,qBAAqB,MAAM,IAAI,SAAS,CAC7C,0BACA,kBACD,CAAC;EACD,MAAM,kBACL,KAAK,KAAK,oBAAoB,qBAAqB,SAC7C,IAAI,MAAM;GAAC;GAAU;GAAQ;GAAa;EAAuB,CAAC,CACzE;EACA,KAAK,MAAM,IAAI,IAAI;GAAC;GAAY;GAAM;EAAM,CAAC,EAAA,CAAG,KAAK,MAAM,UAC1D,MAAM,IAAI,MAAM,WAAW,IAAI,+BAA+B;EAE/D,MAAM,WAA6B;GAClC,SAAS;GACT,YAAY,cAAc;GAC1B,kBAAkB,MAAM,IAAI,SAAS,CAAC,eAAe,CAAC;GACtD;GACA;EACD;EACA,MAAM,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;EACvD,MAAM,UAAU,eAAe,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE,KAAK,EACxE,MAAM,KACP,CAAC;EACD,MAAM,OAAO,eAAe,QAAQ;EACpC,OAAO;CACR,SAAS,OAAO;EAEf,IAAI,eAAe,MAAM,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;EACrD,MAAM,IAAI,MAAM,8BAA8B,SAAS,IAAI,EAAE,MAAM,CAAC;CACrE,UAAU;EACT,MAAM,GAAG,eAAe,EAAE,OAAO,KAAK,CAAC;CACxC;AACD;AAEA,eAAsB,aACrB,KACA,UACA,EAAE,cAC0B;CAC5B,IAAI;EACH,MAAM,WAAoB,KAAK,MAAM,MAAM,SAAS,UAAU,MAAM,CAAC;EACrE,IAAI,CAAC,WAAW,QAAQ,GAAG,MAAM,IAAI,MAAM,0BAA0B;EACrE,IACC,SAAS,gBAAgB,cAAc,SACvC,SAAS,qBAAsB,MAAM,IAAI,SAAS,CAAC,eAAe,CAAC,GAEnE,MAAM,IAAI,MAAM,yDAAyD;EAE1E,KAEE,MAAM,IAAI,IAAI;GAAC;GAAoB;GAAe,SAAS;EAAG,CAAC,EAAA,CAC9D,KAAK,MAAM,SAAS,KAEtB,MAAM,IAAI,MAAM,sBAAsB;EAEvC,KACE,MAAM,IAAI,IAAI;GAAC;GAAY;GAAM,SAAS;EAAM,CAAC,EAAA,CAAG,KAAK,MAAM,UAEhE,MAAM,IAAI,MAAM,kDAAkD;EAEnE,OAAO;CACR,SAAS,OAAO;EACf,MAAM,IAAI,MACT,0CAA0C,SAAS,gCACnD,EAAE,MAAM,CACT;CACD;AACD;;;ACpHA,SAAS,cAAc,OAAsC;CAC5D,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,SAAS;CACf,OACC,OAAO,OAAO,QAAQ,YACtB,OAAO,cAAc,OAAO,GAAG,KAC/B,OAAO,MAAM,KACb,OAAO,OAAO,SAAS,YACvB,kCAAkC,KAAK,OAAO,IAAI,MACjD,OAAO,qBAAqB,KAAA,KAC5B,OAAO,OAAO,qBAAqB,cACpC,MAAM,QAAQ,OAAO,KAAK,KAC1B,OAAO,MAAM,SAAS,KACtB,OAAO,MAAM,OACX,SACA,OAAO,SAAS,YAChB,CAAC,KAAK,WAAW,IAAI,KACrB,CAAC,KAAK,MAAM,WAAW,IAAI,KAC3B,KACE,MAAM,GAAG,CAAC,CACV,OAAO,SAAS,SAAS,MAAM,SAAS,OAAO,SAAS,IAAI,CAChE;AAEF;AAEA,eAAe,mBAAmB,YAA0C;CAC3E,IAAI;EACH,MAAM,SAAkB,KAAK,MAAM,MAAM,SAAS,YAAY,MAAM,CAAC;EACrE,IAAI,CAAC,cAAc,MAAM,GACxB,MAAM,IAAI,UAAU,gCAAgC;EACrD,OAAO;CACR,SAAS,OAAO;EACf,MAAM,IAAI,MACT,6CAA6C,WAAW,gHACxD,EAAE,MAAM,CACT;CACD;AACD;AAEA,eAAe,oBACd,YACA,QACgB;CAChB,MAAM,gBAAgB,GAAG,WAAW,GAAG,WAAW,EAAE;CACpD,IAAI;EACH,MAAM,UAAU,eAAe,KAAK,UAAU,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;EACrE,MAAM,OAAO,eAAe,UAAU;CACvC,UAAU;EACT,MAAM,GAAG,eAAe,EAAE,OAAO,KAAK,CAAC;CACxC;AACD;AAUA,SAAS,gBAAgB,MAAc,UAAwB;CAC9D,MAAM,WAAW,KAAK,SAAS,MAAM,QAAQ;CAC7C,IACC,aAAa,QACb,SAAS,WAAW,KAAK,KAAK,KAAK,KACnC,KAAK,WAAW,QAAQ,GAExB,MAAM,IAAI,MAAM,8CAA8C,SAAS,EAAE;AAE3E;AAEA,eAAe,gBACd,MACA,WACgB;CAChB,MAAM,0BAAU,IAAI,IAAY;CAChC,KAAK,MAAM,YAAY,WAAW;EACjC,gBAAgB,MAAM,QAAQ;EAC9B,KAAK,IAAI,SAAS,KAAK,QAAQ,QAAQ,IAAK,SAAS,KAAK,QAAQ,MAAM,GAAG;GAC1E,IAAI,QAAQ,IAAI,MAAM,GAAG;GACzB,MAAM,QAAQ,MAAM,MAAM,MAAM,CAAC,CAAC,OAChC,WAAkC;IAClC,IAAI,OAAO,SAAS,UAAU,MAAM;GACrC,CACD;GACA,IAAI,SAAS,CAAC,MAAM,YAAY,GAC/B,MAAM,IAAI,MAAM,gCAAgC,OAAO,EAAE;GAC1D,QAAQ,IAAI,MAAM;GAClB,IAAI,WAAW,MAAM;EACtB;CACD;AACD;AAGA,eAAsB,kBACrB,KACA,aACA,QACa;CACb,MAAM,OAAO,MAAM,SAAS,MAAM,IAAI,SAAS,CAAC,iBAAiB,CAAC,CAAC;CACnE,cAAc,MAAM,SAAS,WAAW;CACxC,gBAAgB,MAAM,WAAW;CACjC,MAAM,iBAAiB,KAAK,KAC3B,MAAM,IAAI,SAAS,CAAC,oBAAoB,CAAC,GACzC,UACD;CACA,MAAM,MAAM,gBAAgB,EAAE,WAAW,KAAK,CAAC;CAC/C,MAAM,OAAO,KAAK,KAAK,gBAAgB,MAAM;CAC7C,OAAO,kBAAkB,MAAM,YAAY;EAC1C,MAAM,eAA8B,CAAC;EACrC,KAAK,MAAM,SAAS,MAAM,QAAQ,cAAc,GAAG;GAClD,IAAI,CAAC,MAAM,SAAS,OAAO,GAAG;GAC9B,MAAM,aAAa,KAAK,KAAK,gBAAgB,KAAK;GAClD,MAAM,SAAS,MAAM,mBAAmB,UAAU;GAClD,IAAI,cAAc,OAAO;GACzB,IAAI;IACH,QAAQ,KAAK,OAAO,KAAK,CAAC;GAC3B,SAAS,QAAQ;IAChB,IAAK,OAAiC,SAAS,SAC9C,cAAc;GAChB;GACA,IAAI,aACH,MAAM,IAAI,MACT,qEAAqE,WAAW,8BACjF;GACD,aAAa,KAAK,MAAM;EACzB;EACA,MAAM,cAAc,aAAa,SAAS,WAAW,OAAO,KAAK;EACjE,OAAO,OAAO;GACb,SAAS,YACR,CAAE,MAAM,UAAU,IAAI,CAAC,CAAC,IAAI;IAC3B;IACA;IACA;IACA;IACA;IACA;IACA,GAAG,YAAY,KAAK,SAAS,yBAAyB,MAAM;GAC7D,CAAC;GACF,cAAc,OAAO,SAAS,UAAU;IACvC,MAAM,QAAQ,MAAM,KAAK,SACxB,KACE,SAAS,MAAM,KAAK,QAAQ,aAAa,IAAI,CAAC,CAAC,CAC/C,MAAM,KAAK,GAAG,CAAC,CACf,KAAK,GAAG,CACX;IACA,KAAK,MAAM,QAAQ,OAClB,IACC,YAAY,MACV,WACA,SAAS,UACT,KAAK,WAAW,GAAG,OAAO,EAAE,KAC5B,OAAO,WAAW,GAAG,KAAK,EAAE,CAC9B,GAEA,MAAM,IAAI,MACT,iCAAiC,KAAK,kDACvC;IAGF,MAAM,OAAO,MAAM,IAAI,SAAS,CAAC,MAAM,CAAC;IACxC,MAAM,eAAe,IAAI,KACvB,MAAM,IAAI,IAAI;KAAC;KAAW;KAAM;KAAe;KAAM;IAAI,CAAC,EAAA,CACzD,MAAM,IAAI,CAAC,CACX,OAAO,OAAO,CACjB;IACA,MAAM,WAAW,MAAM,QAAQ,SAAS,aAAa,IAAI,IAAI,CAAC;IAC9D,MAAM,SAAS,MAAM,QAAQ,SAAS,CAAC,aAAa,IAAI,IAAI,CAAC;IAC7D,MAAM,wBAAQ,IAAI,IAAoB;IACtC,MAAM,YAAY,MAAM,KAAK,SAAS,KAAK,QAAQ,aAAa,IAAI,CAAC;IACrE,MAAM,gBAAgB,MAAM,SAAS;IACrC,KAAK,MAAM,QAAQ,OAAO;KACzB,MAAM,WAAW,KAAK,QAAQ,aAAa,IAAI;KAE/C,MAAM,QAAQ,MAAM,MAAM,QAAQ,CAAC,CAAC,OAClC,WAAkC;MAClC,IAAI,OAAO,SAAS,UAAU,MAAM;KACrC,CACD;KACA,IAAI,CAAC,aAAa,IAAI,IAAI,KAAK,OAC9B,MAAM,IAAI,MACT,gDAAgD,SAAS,EAC1D;KACD,IAAI,OAAO,OAAO,GAAG,MAAM,IAAI,UAAU,MAAM,IAAI;IACpD;IACA,MAAM,aAAa,KAAK,KAAK,gBAAgB,GAAG,WAAW,EAAE,MAAM;IACnE,MAAM,SAAsB;KAAE,KAAK,QAAQ;KAAK;KAAO;IAAK;IAC5D,MAAM,oBAAoB,YAAY,MAAM;IAC5C,MAAM,UAAU,YAAY;KAC3B,MAAM,oBAAoB,YAAY;MACrC,GAAG;MACH,kBAAkB;KACnB,CAAC;KACD,MAAM,gBAAgB,MAAM,SAAS;KACrC,IAAI,SAAS,QACZ,MAAM,IAAI,IAAI;MACb;MACA;MACA,YAAY;MACZ;MACA;MACA,GAAG;KACJ,CAAC;KACF,KAAK,MAAM,QAAQ,QAClB,MAAM,GAAG,KAAK,QAAQ,aAAa,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;KAC1D,KAAK,MAAM,CAAC,UAAU,SAAS,OAAO,MAAM,MAAM,UAAU,IAAI;KAChE,MAAM,GAAG,UAAU;IACpB;IACA,IAAI;KACH,MAAM,IAAI,IAAI;MACb;MACA;MACA,YAAY;MACZ;MACA;MACA,GAAG;KACJ,CAAC;IACF,SAAS,QAAQ;KAChB,MAAM,QAAQ;KACd,MAAM;IACP;IACA,aAAa,kBAAkB,MAAM,SAAS,IAAI;GACnD;EACD,CAAC;CACF,CAAC;AACF;;;AChMA,eAAsB,SAAS,EAC9B,YACA,cACA,aACA,aACA,gBACA,cAAc,QAAQ,IAAI,GAC1B,UAAU,SACyD;CACnE,IAAI;CACJ,IAAI;CACJ,IAAI,SAAS;EACZ,MAAM,YAAY,OAAO,cAAc;EACvC,OAAO,UAAU;EACjB,WAAW,UAAU;CACtB;CACA,OAAO,UAAU;CACjB,MAAM,MAAM,UAAU,WAAW;CACjC,OAAO,WAAW;CAClB,MAAM,YAAY,MAAM,IAAI,YAAY;CACxC,OAAO,mBAAmB;CAC1B,IAAI,CAAC,WACJ,OAAO;EACN,SAAS;EACT,aAAa;CACd;CAED,IACC,CAAE,MAAM,kBAAkB,KAAK,cAAc,UAAU,MAAM,QAAQ,CAAC,GAEtE,OAAO;EACN,SAAS;EACT,aAAa;CACd;CAED,IAAIA;CACJ,IAAI;CACJ,IAAI,iBAAiB,KAAA,GAAW;EAC/B,MAAM,WAAW,MAAM,aACtB,KACA,KAAK,QAAQ,aAAa,YAAY,GACtC,EAAE,WAAW,CACd;EACA,qBAAmB,SAAS;EAC5B,gBAAgB,SAAS;EACzB,OAAO,8BAA8B;CACtC,OAAO;EACN,MAAM,aAAa,MAAM,IAAI,WAAW,CAAC,UAAU,QAAQ,CAAC;EAE5D,OAAO,kBAAkB;EACzB,qBAAmBC,iBAAuB,YAAY,UAAU;EAChE,OAAO,0BAA0B;EACjC,IAAI,CAACD,oBACJ,OAAO;GACN,SAAS,uCAAuC,WAAW;GAC3D,aAAa;GACb,kBAAkB;EACnB;EAED,MAAM,qBAAqB,MAAM,IAAI,SAAS,CAC7C,0BACA,kBACD,CAAC;EACD,MAAM,kBACL,KAAK,KAAK,oBAAoB,qBAAqB,SAElD,IAAI,MAAM;GACT;GACA,GAAGA,mBAAiB,GAAGA;GACvB;GACA;EACD,CAAC,CACH;EAEA,gBAAgB,MAAM,IAAI,SAAS,CAAC,GAAGA,mBAAiB,UAAU,CAAC;EACnE,OAAO,4BAA4B;CACpC;CAEA,IAAI;CACJ,IAAI;EACH,mBACC,MAAM,IAAI,IAAI;GAAC;GAAW;GAAM;GAAe;GAAM;EAAa,CAAC,EAAA,CAElE,MAAM,IAAI,CAAC,CACX,OAAO,OAAO;CACjB,SAAS,QAAQ;EAChB,OAAO,iCAAiC;EACxC,MAAM,UACL,kBAAkB,QACf,OAAO,UACP,OAAO,WAAW,WACjB,SACA,KAAA;EAIL,OAAO;GACN,SAAA,kCAHA,UAAU,KAAK,YAAY;GAI3B,aAAa;GACb,kBAAkB;GAClB,gBAAgBA;GAChB,gBAAgB;EACjB;CACD;CACA,OAAO,mCAAmC;CAC1C,MAAM,sBAAsB,gBAAgB,QAAQ,SACnD,UAAU,MAAM,WAAW,CAC5B;CACA,OAAO,+BAA+B;CAEtC,MAAM,WAAW,MAAM,kBAAkB,KAAK,aAAa,OAAO,UAAU;EAC3E,IAAI,CAAE,MAAM,MAAM,QAAQ,GACzB,OAAO;GACN,SAAS;GACT,aAAa;EACd;EAGD,IAAI,oBAAoB,WAAW,GAClC,OAAO;GACN,SAAS,6CAA6C,YAAY;GAClE,aAAa;GACb,kBAAkB;GAClB,gBAAgBA;GAChB,gBAAgB;EACjB;EAED,MAAM,UAAU,MAAM,MAAM,aAAa,eAAe,mBAAmB;EAC3E,OAAO,+CAA+C;EACtD,OAAO;GAAE,kBAAA;GAAkB;GAAqB;EAAQ;CACzD,CAAC;CACD,IAAI,SAAS,YAAY,KAAA,GAAW,OAAO;CAC3C,MAAM,EAAE,YAAY;CAEpB,IAAI;EACH,MAAM,4BAA4B,MAAM,IAAI,SAEzC,SAAS,WAAW;GACtB,MAAM,SAAS,KACd,aACA,EAAE,KAAK,YAAY,IAClB,GAAG,IAAI,WAAW;IAClB,IAAI,OAAO,aAAa,GACvB,QAAQ;KACP,SAAS;KACT,aAAa;KACb,kBAAkB;KAClB,gBAAgBA;KAChB,gBAAgB;KAChB,YAAY;KACZ,sBAAsB;IACvB,CAAC;SAED,OAAO,MAAM;GAEf,CACD;EACD,CAAC;EACD,OAAO,cAAc;EACrB,OAAO;CACR,SAAS,QAAQ;EAChB,OAAO,cAAc;EA4BrB,OAAO,MA3BgC,IAAI,SAExC,YAAY;GACd,MAAM,SAAS,KACd,gBACA,EAAE,KAAK,YAAY,IAClB,GAAG,QAAQ,WAAW;IACtB,MAAM,gBAAgB,OAAO,aAAa;IAC1C,OAAO,GAAG,gBAAgB,WAAW,SAAS,eAAe;IAC7D,QAAQ;KACP,SAAS,gBACN,+CACA;KACH,aAAa;KACb,kBAAkB;KAClB,gBAAgBA;KAChB,gBAAgB;KAChB,YAAY;KACZ,sBAAsB;KACtB,YAAY;KACZ,0BAA0B;KAC1B,qBAAqB;KACrB,qBAAqB;IACtB,CAAC;GACF,CACD;EACD,CAAC;CAEF,UAAU;EACT,MAAM,QAAQ;EACd,OAAO,gCAAgC;EACvC,WAAW;CACZ;AACD"}
@@ -15,6 +15,9 @@
15
15
  "baseDirname": {
16
16
  "type": "string"
17
17
  },
18
+ "baselineFile": {
19
+ "type": "string"
20
+ },
18
21
  "tagPattern": {
19
22
  "type": "string"
20
23
  },
@@ -1,6 +1,26 @@
1
+ import "simple-git";
2
+ //#region src/baseline.d.ts
3
+ type BaselineOptions = {
4
+ tagPattern?: string | undefined;
5
+ baseDirname?: string;
6
+ };
7
+ type BreakagePreludeOptions = BaselineOptions & {
8
+ out: string;
9
+ };
10
+ type BreakageBaseline = {
11
+ version: 1;
12
+ tagPattern: string | null;
13
+ packageDirectory: string;
14
+ ref: string;
15
+ commit: string;
16
+ };
17
+ /** Resolve and fetch a release without running tests or changing the worktree. */
18
+ export declare function breakagePrelude({ out, tagPattern, baseDirname }: BreakagePreludeOptions): Promise<BreakageBaseline>;
19
+ //#endregion
1
20
  //#region src/breakage.d.ts
2
21
  export type BreakageOptions = {
3
22
  tagPattern?: string | undefined;
23
+ baselineFile?: string | undefined;
4
24
  testPattern: string;
5
25
  testCommand: string;
6
26
  certifyCommand: string;
@@ -36,8 +56,9 @@ export type BreakageOutcome = {
36
56
  certificationStdout: string;
37
57
  certificationStderr: string;
38
58
  };
39
- export declare function breakage({ tagPattern, testPattern, testCommand, certifyCommand, baseDirname, verbose }: BreakageOptions): Promise<BreakageOutcome & {
59
+ export declare function breakage({ tagPattern, baselineFile, testPattern, testCommand, certifyCommand, baseDirname, verbose }: BreakageOptions): Promise<BreakageOutcome & {
40
60
  summary: string;
41
61
  }>;
42
62
  //#endregion
63
+ export type { BreakageBaseline, BreakagePreludeOptions };
43
64
  //# sourceMappingURL=breakage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"breakage.d.ts","names":[],"sources":["../src/breakage.ts"],"mappings":";YAYY;EACX;EACA;EACA;EACA;EACA;EACA;;YAGW;EACP;;EAEF;EACA;;EAGA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;wBAGmB,WACrB,YACA,aACA,aACA,gBACA,aACA,WACE,kBAAkB,QAAQ;EAAoB"}
1
+ {"version":3,"file":"breakage.d.ts","names":[],"sources":["../src/baseline.ts","../src/breakage.ts"],"mappings":";;KAUY;EACX;EACA;;KAGW,yBAAyB;EAAoB;;KAE7C;EACX;EACA;EACA;EACA;EACA;;;wBAoBqB,kBACrB,KACA,YACA,eACE,yBAAyB,QAAQ;;;YC9BxB;EACX;EACA;EACA;EACA;EACA;EACA;EACA;;YAGW;EACP;;EAEF;EACA;;EAGA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;wBAGmB,WACrB,YACA,cACA,aACA,aACA,gBACA,aACA,WACE,kBAAkB,QAAQ;EAAoB"}
package/dist/breakage.js CHANGED
@@ -1 +1 @@
1
- export { t as breakage } from "./breakage-D_nc-zEK.js";
1
+ export { t as breakage, n as breakagePrelude } from "./breakage-Dc4IldeW.js";
@@ -15,6 +15,9 @@
15
15
  "baseDirname": {
16
16
  "type": "string"
17
17
  },
18
+ "baselineFile": {
19
+ "type": "string"
20
+ },
18
21
  "tagPattern": {
19
22
  "type": "string"
20
23
  },
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "additionalProperties": false,
4
+ "type": "object",
5
+ "properties": {
6
+ "baseDirname": {
7
+ "type": "string"
8
+ },
9
+ "baselineFile": {
10
+ "type": "string"
11
+ },
12
+ "certifyCommand": {
13
+ "type": "string"
14
+ },
15
+ "out": {
16
+ "type": "string"
17
+ },
18
+ "tagPattern": {
19
+ "type": "string"
20
+ },
21
+ "testCommand": {
22
+ "type": "string"
23
+ },
24
+ "testPattern": {
25
+ "type": "string"
26
+ },
27
+ "verbose": {
28
+ "type": "boolean"
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "additionalProperties": false,
4
+ "type": "object",
5
+ "properties": {
6
+ "baseDirname": {
7
+ "type": "string"
8
+ },
9
+ "baselineFile": {
10
+ "type": "string"
11
+ },
12
+ "certifyCommand": {
13
+ "type": "string"
14
+ },
15
+ "out": {
16
+ "type": "string"
17
+ },
18
+ "tagPattern": {
19
+ "type": "string"
20
+ },
21
+ "testCommand": {
22
+ "type": "string"
23
+ },
24
+ "testPattern": {
25
+ "type": "string"
26
+ },
27
+ "verbose": {
28
+ "type": "boolean"
29
+ }
30
+ }
31
+ }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as breakage } from "./breakage-D_nc-zEK.js";
2
+ import { n as breakagePrelude, t as breakage } from "./breakage-Dc4IldeW.js";
3
3
  import * as path$1 from "node:path";
4
4
  import logger from "takua";
5
5
  import { inspect } from "node:util";
@@ -8,6 +8,7 @@ import { cli, completionResponse, encapsulate, help, logWarnings, noOptions, opt
8
8
  //#region src/breakage.x.ts
9
9
  const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type({
10
10
  "tagPattern?": `string`,
11
+ "baselineFile?": `string`,
11
12
  testPattern: `string`,
12
13
  testCommand: `string`,
13
14
  certifyCommand: `string`,
@@ -22,6 +23,16 @@ const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type
22
23
  description: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,
23
24
  example: `--tagPattern="my-library"`
24
25
  },
26
+ baselineFile: {
27
+ aliases: [`baseline-file`],
28
+ completion: {
29
+ repeatable: false,
30
+ fileSystem: `files`
31
+ },
32
+ required: false,
33
+ description: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,
34
+ example: `--baseline-file=.breakage/baseline.json`
35
+ },
25
36
  testPattern: {
26
37
  aliases: [`test-pattern`, `pattern`],
27
38
  completion: { repeatable: false },
@@ -66,6 +77,39 @@ const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type
66
77
  parse: parseBooleanOption
67
78
  }
68
79
  });
80
+ const PRELUDE_MANUAL = options(`Resolve and fetch the release baseline for a cacheable compatibility check.`, type({
81
+ "tagPattern?": `string`,
82
+ "baseDirname?": `string`,
83
+ "out?": `string`,
84
+ "testPattern?": `string`,
85
+ "testCommand?": `string`,
86
+ "certifyCommand?": `string`,
87
+ "baselineFile?": `string`,
88
+ "verbose?": `boolean`
89
+ }), {
90
+ ...BREAKAGE_MANUAL.optionConfigs,
91
+ testPattern: {
92
+ ...BREAKAGE_MANUAL.optionConfigs.testPattern,
93
+ required: false
94
+ },
95
+ testCommand: {
96
+ ...BREAKAGE_MANUAL.optionConfigs.testCommand,
97
+ required: false
98
+ },
99
+ certifyCommand: {
100
+ ...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
101
+ required: false
102
+ },
103
+ out: {
104
+ completion: {
105
+ repeatable: false,
106
+ fileSystem: `files`
107
+ },
108
+ required: false,
109
+ description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
110
+ example: `--out=.breakage/baseline.json`
111
+ }
112
+ });
69
113
  const SCHEMA_MANUAL = options(`Create a copy of the schema for configuring breakage.`, type({ "outdir?": `string` }), { outdir: {
70
114
  aliases: [`out-dir`],
71
115
  completion: {
@@ -82,18 +126,24 @@ const parse = cli({
82
126
  routes: optional({
83
127
  help: null,
84
128
  schema: null,
129
+ prelude: optional({ $configPath: null }),
85
130
  $configPath: null
86
131
  }),
87
132
  routeOptions: {
88
133
  "": BREAKAGE_MANUAL,
89
134
  $configPath: BREAKAGE_MANUAL,
90
135
  help: noOptions(`Show usage.`),
91
- schema: SCHEMA_MANUAL
136
+ schema: SCHEMA_MANUAL,
137
+ prelude: PRELUDE_MANUAL,
138
+ "prelude/$configPath": PRELUDE_MANUAL
139
+ },
140
+ positionalCompletions: {
141
+ $configPath: { fileSystem: `files` },
142
+ "prelude/$configPath": { fileSystem: `files` }
92
143
  },
93
- positionalCompletions: { $configPath: { fileSystem: `files` } },
94
144
  discoverConfigPath: (args) => {
95
145
  if (args[0] === `help` || args[0] === `schema`) return;
96
- return args[0] ?? path$1.join(process.cwd(), `breakage.config.json`);
146
+ return (args[0] === `prelude` ? args[1] : args[0]) ?? path$1.join(process.cwd(), `breakage.config.json`);
97
147
  }
98
148
  }, {
99
149
  info: (message, ...data) => logger.info(`breakage`, message, data),
@@ -115,6 +165,15 @@ else {
115
165
  process.stdout.write(`📝 Wrote json.schema.`);
116
166
  }
117
167
  break;
168
+ case `prelude`:
169
+ case `prelude/$configPath`: {
170
+ const baseline = await breakagePrelude({
171
+ ...inputs.opts,
172
+ out: inputs.opts.out ?? `.breakage/baseline.json`
173
+ });
174
+ process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`);
175
+ break;
176
+ }
118
177
  case ``:
119
178
  case `$configPath`: {
120
179
  const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
@@ -1 +1 @@
1
- {"version":3,"file":"breakage.x.js","names":["path"],"sources":["../src/breakage.x.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as path from \"node:path\"\nimport { inspect } from \"node:util\"\n\nimport { type } from \"arktype\"\nimport type { OptionsGroup } from \"comline\"\nimport {\n\tcli,\n\tcompletionResponse,\n\tencapsulate,\n\thelp,\n\tlogWarnings,\n\tnoOptions,\n\toptional,\n\toptions,\n\tparseBooleanOption,\n} from \"comline\"\nimport logger from \"takua\"\n\nimport type { BreakageOptions } from \"./breakage.ts\"\nimport { breakage } from \"./breakage.ts\"\n\nconst BREAKAGE_MANUAL = options(\n\t`Check for breaking changes in a package.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\ttestPattern: `string`,\n\t\ttestCommand: `string`,\n\t\tcertifyCommand: `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\ttagPattern: {\n\t\t\taliases: [`tag-pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `g`,\n\t\t\trequired: false,\n\t\t\tdescription: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,\n\t\t\texample: `--tagPattern=\"my-library\"`,\n\t\t},\n\t\ttestPattern: {\n\t\t\taliases: [`test-pattern`, `pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `p`,\n\t\t\trequired: true,\n\t\t\tdescription: `The pattern to match test files that test the public API of the library.`,\n\t\t\texample: `--pattern=\"*__public.test.ts\"`,\n\t\t},\n\t\ttestCommand: {\n\t\t\taliases: [`test-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `t`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that runs the tests for the library's public API.`,\n\t\t\texample: `--testCommand=\"npm run test\"`,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\taliases: [`certify-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `c`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that determines whether a major version bump for your package is indicated in the workspace. Exit code 0 indicates that a major version bump is indicated, and exit code 1 indicates that no major version bump is indicated.`,\n\t\t\texample: `--certifyCommand=\\tsx scripts/certify-major-version-bump.node`,\n\t\t},\n\t\tbaseDirname: {\n\t\t\taliases: [`base-dir`, `base-dirname`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `d`,\n\t\t\trequired: false,\n\t\t\tdescription: `The base directory to run the command from.`,\n\t\t\texample: `--baseDirname=./packages/my-package`,\n\t\t},\n\t\tverbose: {\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `v`,\n\t\t\trequired: false,\n\t\t\tdescription: `Prints out more information about the process.`,\n\t\t\texample: `--verbose`,\n\t\t\tparse: parseBooleanOption,\n\t\t},\n\t},\n) satisfies OptionsGroup<BreakageOptions>\n\nconst SCHEMA_MANUAL = options(\n\t`Create a copy of the schema for configuring breakage.`,\n\ttype({ \"outdir?\": `string` }),\n\t{\n\t\toutdir: {\n\t\t\taliases: [`out-dir`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `o`,\n\t\t\trequired: false,\n\t\t\tdescription: `Directory to write the schema to.`,\n\t\t\texample: `--outdir=./dist`,\n\t\t},\n\t},\n)\n\nconst parse = cli(\n\t{\n\t\tcliName: `breakage`,\n\t\troutes: optional({ help: null, schema: null, $configPath: null }),\n\t\trouteOptions: {\n\t\t\t\"\": BREAKAGE_MANUAL,\n\t\t\t$configPath: BREAKAGE_MANUAL,\n\t\t\thelp: noOptions(`Show usage.`),\n\t\t\tschema: SCHEMA_MANUAL,\n\t\t},\n\t\tpositionalCompletions: { $configPath: { fileSystem: `files` } },\n\t\tdiscoverConfigPath: (args: readonly string[]) => {\n\t\t\tif (args[0] === `help` || args[0] === `schema`) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst configPath =\n\t\t\t\targs[0] ?? path.join(process.cwd(), `breakage.config.json`)\n\t\t\treturn configPath\n\t\t},\n\t},\n\t{\n\t\tinfo: (message, ...data) => logger.info(`breakage`, message, data),\n\t\terror: (message, ...data) => logger.error(`breakage`, message, data),\n\t},\n)\n\nconst completion = await completionResponse(parse.definition, process.argv)\nif (completion !== undefined) {\n\tprocess.stdout.write(completion)\n} else {\n\tconst { inputs, warnings, writeJsonSchema } = parse(process.argv)\n\tlogWarnings(warnings)\n\n\tswitch (inputs.case) {\n\t\tcase `help`:\n\t\t\tconsole.log(help(parse.definition))\n\t\t\tbreak\n\t\tcase `schema`:\n\t\t\t{\n\t\t\t\tconst { outdir } = inputs.opts\n\t\t\t\twriteJsonSchema(outdir ?? `.`)\n\t\t\t\tprocess.stdout.write(`📝 Wrote json.schema.`)\n\t\t\t}\n\t\t\tbreak\n\t\tcase ``:\n\t\tcase `$configPath`: {\n\t\t\tconst { returnValue } = await encapsulate(() => breakage(inputs.opts), {\n\t\t\t\tconsole: true,\n\t\t\t\tstdout: true,\n\t\t\t})\n\t\t\tif (`testResult` in returnValue && returnValue.breakingChangesFound) {\n\t\t\t\tprocess.stdout.write(returnValue.testResult)\n\t\t\t}\n\t\t\tif (`breakingChangesFound` in returnValue) {\n\t\t\t\tif (returnValue.breakingChangesFound) {\n\t\t\t\t\tif (returnValue.breakingChangesCertified) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`👷 Breaking changes were found and certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(0)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`❌ Breaking changes were found, but not certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(1)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.stdout.write(`✅ No breaking changes were found.`)\n\t\t\t\t\tprocess.exit(0)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t`💥 Breakage failed to determine breaking changes.` +\n\t\t\t\t\t\t`\\n` +\n\t\t\t\t\t\tinspect(returnValue),\n\t\t\t\t)\n\t\t\t\tprocess.exit(2)\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"breakage.x.js","names":["path"],"sources":["../src/breakage.x.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as path from \"node:path\"\nimport { inspect } from \"node:util\"\n\nimport { type } from \"arktype\"\nimport type { OptionsGroup } from \"comline\"\nimport {\n\tcli,\n\tcompletionResponse,\n\tencapsulate,\n\thelp,\n\tlogWarnings,\n\tnoOptions,\n\toptional,\n\toptions,\n\tparseBooleanOption,\n} from \"comline\"\nimport logger from \"takua\"\n\nimport type { BreakageOptions } from \"./breakage.ts\"\nimport { breakage, breakagePrelude } from \"./breakage.ts\"\n\nconst BREAKAGE_MANUAL = options(\n\t`Check for breaking changes in a package.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\t\"baselineFile?\": `string`,\n\t\ttestPattern: `string`,\n\t\ttestCommand: `string`,\n\t\tcertifyCommand: `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\ttagPattern: {\n\t\t\taliases: [`tag-pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `g`,\n\t\t\trequired: false,\n\t\t\tdescription: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,\n\t\t\texample: `--tagPattern=\"my-library\"`,\n\t\t},\n\t\tbaselineFile: {\n\t\t\taliases: [`baseline-file`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `files` },\n\t\t\trequired: false,\n\t\t\tdescription: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,\n\t\t\texample: `--baseline-file=.breakage/baseline.json`,\n\t\t},\n\t\ttestPattern: {\n\t\t\taliases: [`test-pattern`, `pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `p`,\n\t\t\trequired: true,\n\t\t\tdescription: `The pattern to match test files that test the public API of the library.`,\n\t\t\texample: `--pattern=\"*__public.test.ts\"`,\n\t\t},\n\t\ttestCommand: {\n\t\t\taliases: [`test-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `t`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that runs the tests for the library's public API.`,\n\t\t\texample: `--testCommand=\"npm run test\"`,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\taliases: [`certify-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `c`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that determines whether a major version bump for your package is indicated in the workspace. Exit code 0 indicates that a major version bump is indicated, and exit code 1 indicates that no major version bump is indicated.`,\n\t\t\texample: `--certifyCommand=\\tsx scripts/certify-major-version-bump.node`,\n\t\t},\n\t\tbaseDirname: {\n\t\t\taliases: [`base-dir`, `base-dirname`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `d`,\n\t\t\trequired: false,\n\t\t\tdescription: `The base directory to run the command from.`,\n\t\t\texample: `--baseDirname=./packages/my-package`,\n\t\t},\n\t\tverbose: {\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `v`,\n\t\t\trequired: false,\n\t\t\tdescription: `Prints out more information about the process.`,\n\t\t\texample: `--verbose`,\n\t\t\tparse: parseBooleanOption,\n\t\t},\n\t},\n) satisfies OptionsGroup<BreakageOptions>\n\nconst PRELUDE_MANUAL = options(\n\t`Resolve and fetch the release baseline for a cacheable compatibility check.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"out?\": `string`,\n\t\t\"testPattern?\": `string`,\n\t\t\"testCommand?\": `string`,\n\t\t\"certifyCommand?\": `string`,\n\t\t\"baselineFile?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\t...BREAKAGE_MANUAL.optionConfigs,\n\t\ttestPattern: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.testPattern,\n\t\t\trequired: false,\n\t\t},\n\t\ttestCommand: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.testCommand,\n\t\t\trequired: false,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.certifyCommand,\n\t\t\trequired: false,\n\t\t},\n\t\tout: {\n\t\t\tcompletion: { repeatable: false, fileSystem: `files` },\n\t\t\trequired: false,\n\t\t\tdescription: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,\n\t\t\texample: `--out=.breakage/baseline.json`,\n\t\t},\n\t},\n)\n\nconst SCHEMA_MANUAL = options(\n\t`Create a copy of the schema for configuring breakage.`,\n\ttype({ \"outdir?\": `string` }),\n\t{\n\t\toutdir: {\n\t\t\taliases: [`out-dir`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `o`,\n\t\t\trequired: false,\n\t\t\tdescription: `Directory to write the schema to.`,\n\t\t\texample: `--outdir=./dist`,\n\t\t},\n\t},\n)\n\nconst parse = cli(\n\t{\n\t\tcliName: `breakage`,\n\t\troutes: optional({\n\t\t\thelp: null,\n\t\t\tschema: null,\n\t\t\tprelude: optional({ $configPath: null }),\n\t\t\t$configPath: null,\n\t\t}),\n\t\trouteOptions: {\n\t\t\t\"\": BREAKAGE_MANUAL,\n\t\t\t$configPath: BREAKAGE_MANUAL,\n\t\t\thelp: noOptions(`Show usage.`),\n\t\t\tschema: SCHEMA_MANUAL,\n\t\t\tprelude: PRELUDE_MANUAL,\n\t\t\t\"prelude/$configPath\": PRELUDE_MANUAL,\n\t\t},\n\t\tpositionalCompletions: {\n\t\t\t$configPath: { fileSystem: `files` },\n\t\t\t\"prelude/$configPath\": { fileSystem: `files` },\n\t\t},\n\t\tdiscoverConfigPath: (args: readonly string[]) => {\n\t\t\tif (args[0] === `help` || args[0] === `schema`) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst configPath =\n\t\t\t\t(args[0] === `prelude` ? args[1] : args[0]) ??\n\t\t\t\tpath.join(process.cwd(), `breakage.config.json`)\n\t\t\treturn configPath\n\t\t},\n\t},\n\t{\n\t\tinfo: (message, ...data) => logger.info(`breakage`, message, data),\n\t\terror: (message, ...data) => logger.error(`breakage`, message, data),\n\t},\n)\n\nconst completion = await completionResponse(parse.definition, process.argv)\nif (completion !== undefined) {\n\tprocess.stdout.write(completion)\n} else {\n\tconst { inputs, warnings, writeJsonSchema } = parse(process.argv)\n\tlogWarnings(warnings)\n\n\tswitch (inputs.case) {\n\t\tcase `help`:\n\t\t\tconsole.log(help(parse.definition))\n\t\t\tbreak\n\t\tcase `schema`:\n\t\t\t{\n\t\t\t\tconst { outdir } = inputs.opts\n\t\t\t\twriteJsonSchema(outdir ?? `.`)\n\t\t\t\tprocess.stdout.write(`📝 Wrote json.schema.`)\n\t\t\t}\n\t\t\tbreak\n\t\tcase `prelude`:\n\t\tcase `prelude/$configPath`: {\n\t\t\tconst baseline = await breakagePrelude({\n\t\t\t\t...inputs.opts,\n\t\t\t\tout: inputs.opts.out ?? `.breakage/baseline.json`,\n\t\t\t})\n\t\t\tprocess.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\\n`)\n\t\t\tbreak\n\t\t}\n\t\tcase ``:\n\t\tcase `$configPath`: {\n\t\t\tconst { returnValue } = await encapsulate(() => breakage(inputs.opts), {\n\t\t\t\tconsole: true,\n\t\t\t\tstdout: true,\n\t\t\t})\n\t\t\tif (`testResult` in returnValue && returnValue.breakingChangesFound) {\n\t\t\t\tprocess.stdout.write(returnValue.testResult)\n\t\t\t}\n\t\t\tif (`breakingChangesFound` in returnValue) {\n\t\t\t\tif (returnValue.breakingChangesFound) {\n\t\t\t\t\tif (returnValue.breakingChangesCertified) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`👷 Breaking changes were found and certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(0)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`❌ Breaking changes were found, but not certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(1)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.stdout.write(`✅ No breaking changes were found.`)\n\t\t\t\t\tprocess.exit(0)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t`💥 Breakage failed to determine breaking changes.` +\n\t\t\t\t\t\t`\\n` +\n\t\t\t\t\t\tinspect(returnValue),\n\t\t\t\t)\n\t\t\t\tprocess.exit(2)\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakage",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Run yesterday’s tests against tomorrow’s release.",
5
5
  "homepage": "https://github.com/jeremybanka/breakage#readme",
6
6
  "bugs": "https://github.com/jeremybanka/breakage/issues",
@@ -25,22 +25,21 @@
25
25
  "src"
26
26
  ],
27
27
  "dependencies": {
28
- "arktype": "2.2.7",
28
+ "arktype": "2.2.8",
29
29
  "comline": "0.9.0",
30
30
  "minimatch": "10.2.6",
31
31
  "simple-git": "4.0.2",
32
32
  "takua": "0.2.3"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/bun": "npm:bun-types@1.4.2",
36
- "tsdown": "0.23.0",
35
+ "@types/bun": "npm:bun-types@1.4.3",
37
36
  "typescript": "7.0.2"
38
37
  },
39
38
  "publishConfig": {
40
39
  "access": "public"
41
40
  },
42
41
  "scripts": {
43
- "build": "tsdown && pnpm run schema",
42
+ "build": "vp pack && pnpm run schema",
44
43
  "check": "pnpm run check:tsc",
45
44
  "check:tsc": "tsc --noEmit",
46
45
  "postversion": "dprint fmt package.json",
@@ -0,0 +1,143 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
3
+ import path from "node:path"
4
+
5
+ import type { SimpleGit } from "simple-git"
6
+ import { simpleGit } from "simple-git"
7
+
8
+ import { withDirectoryLock } from "./directory-lock.ts"
9
+ import { latestReleaseTag } from "./release-tag.ts"
10
+
11
+ export type BaselineOptions = {
12
+ tagPattern?: string | undefined
13
+ baseDirname?: string
14
+ }
15
+
16
+ export type BreakagePreludeOptions = BaselineOptions & { out: string }
17
+
18
+ export type BreakageBaseline = {
19
+ version: 1
20
+ tagPattern: string | null
21
+ packageDirectory: string
22
+ ref: string
23
+ commit: string
24
+ }
25
+
26
+ const objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/
27
+
28
+ function isBaseline(value: unknown): value is BreakageBaseline {
29
+ if (typeof value !== `object` || value === null) return false
30
+ const record = value as Partial<BreakageBaseline>
31
+ return (
32
+ record.version === 1 &&
33
+ (record.tagPattern === null || typeof record.tagPattern === `string`) &&
34
+ typeof record.packageDirectory === `string` &&
35
+ typeof record.ref === `string` &&
36
+ record.ref.startsWith(`refs/tags/`) &&
37
+ typeof record.commit === `string` &&
38
+ objectId.test(record.commit)
39
+ )
40
+ }
41
+
42
+ /** Resolve and fetch a release without running tests or changing the worktree. */
43
+ export async function breakagePrelude({
44
+ out,
45
+ tagPattern,
46
+ baseDirname = process.cwd(),
47
+ }: BreakagePreludeOptions): Promise<BreakageBaseline> {
48
+ const filename = path.resolve(baseDirname, out)
49
+ const temporaryPath = `${filename}.${randomUUID()}.tmp`
50
+ let canInvalidate = false
51
+ try {
52
+ const git = simpleGit(baseDirname)
53
+ // An ignored, untracked snapshot cannot make the compatibility check dirty.
54
+ if (!(await git.raw([`check-ignore`, `--`, filename])).trim()) {
55
+ throw new Error(`Baseline output must be gitignored: ${filename}.`)
56
+ }
57
+ canInvalidate = true
58
+ const remoteTags = await git.listRemote([`--tags`, `origin`])
59
+ const ref = latestReleaseTag(remoteTags, tagPattern)
60
+ if (!ref) {
61
+ throw new Error(`No tags found matching the pattern "${tagPattern}".`)
62
+ }
63
+ const refs = new Map(
64
+ remoteTags
65
+ .trim()
66
+ .split(`\n`)
67
+ .map((line) => {
68
+ const [sha, name] = line.split(`\t`)
69
+ return [name, sha]
70
+ }),
71
+ )
72
+ // ls-remote supplies the peeled commit for annotated tags.
73
+ const commit = refs.get(`${ref}^{}`) ?? refs.get(ref)
74
+ if (!commit || !objectId.test(commit)) {
75
+ throw new Error(`Could not resolve the release commit for ${ref}.`)
76
+ }
77
+ const commonGitDirectory = await git.revparse([
78
+ `--path-format=absolute`,
79
+ `--git-common-dir`,
80
+ ])
81
+ await withDirectoryLock(
82
+ path.join(commonGitDirectory, `breakage-fetch.lock`),
83
+ () => git.fetch([`origin`, commit, `--no-tags`, `--no-write-fetch-head`]),
84
+ )
85
+ if ((await git.raw([`cat-file`, `-t`, commit])).trim() !== `commit`) {
86
+ throw new Error(`Release ${ref} does not resolve to a commit.`)
87
+ }
88
+ const baseline: BreakageBaseline = {
89
+ version: 1,
90
+ tagPattern: tagPattern ?? null,
91
+ packageDirectory: await git.revparse([`--show-prefix`]),
92
+ ref,
93
+ commit,
94
+ }
95
+ await mkdir(path.dirname(filename), { recursive: true })
96
+ await writeFile(temporaryPath, `${JSON.stringify(baseline, null, 2)}\n`, {
97
+ flag: `wx`,
98
+ })
99
+ await rename(temporaryPath, filename)
100
+ return baseline
101
+ } catch (cause) {
102
+ // A failed refresh must never leave an older snapshot available as fallback.
103
+ if (canInvalidate) await rm(filename, { force: true })
104
+ throw new Error(`Failed to prepare baseline ${filename}.`, { cause })
105
+ } finally {
106
+ await rm(temporaryPath, { force: true })
107
+ }
108
+ }
109
+
110
+ export async function readBaseline(
111
+ git: SimpleGit,
112
+ filename: string,
113
+ { tagPattern }: BaselineOptions,
114
+ ): Promise<BreakageBaseline> {
115
+ try {
116
+ const baseline: unknown = JSON.parse(await readFile(filename, `utf8`))
117
+ if (!isBaseline(baseline)) throw new Error(`Invalid baseline fields.`)
118
+ if (
119
+ baseline.tagPattern !== (tagPattern ?? null) ||
120
+ baseline.packageDirectory !== (await git.revparse([`--show-prefix`]))
121
+ ) {
122
+ throw new Error(`Baseline belongs to a different package or tag pattern.`)
123
+ }
124
+ if (
125
+ (
126
+ await git.raw([`check-ref-format`, `--normalize`, baseline.ref])
127
+ ).trim() !== baseline.ref
128
+ ) {
129
+ throw new Error(`Invalid release ref.`)
130
+ }
131
+ if (
132
+ (await git.raw([`cat-file`, `-t`, baseline.commit])).trim() !== `commit`
133
+ ) {
134
+ throw new Error(`Pinned object is not a locally available commit.`)
135
+ }
136
+ return baseline
137
+ } catch (cause) {
138
+ throw new Error(
139
+ `Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`,
140
+ { cause },
141
+ )
142
+ }
143
+ }
package/src/breakage.ts CHANGED
@@ -6,12 +6,17 @@ import { simpleGit } from "simple-git"
6
6
  import type { Chronicle } from "takua"
7
7
  import logger from "takua"
8
8
 
9
+ import { readBaseline } from "./baseline.ts"
9
10
  import { withDirectoryLock } from "./directory-lock.ts"
10
11
  import { latestReleaseTag as selectLatestReleaseTag } from "./release-tag.ts"
11
12
  import { withTestFileState } from "./test-file-state.ts"
12
13
 
14
+ export type { BreakageBaseline, BreakagePreludeOptions } from "./baseline.ts"
15
+ export { breakagePrelude } from "./baseline.ts"
16
+
13
17
  export type BreakageOptions = {
14
18
  tagPattern?: string | undefined
19
+ baselineFile?: string | undefined
15
20
  testPattern: string
16
21
  testCommand: string
17
22
  certifyCommand: string
@@ -54,6 +59,7 @@ export type BreakageOutcome =
54
59
 
55
60
  export async function breakage({
56
61
  tagPattern,
62
+ baselineFile,
57
63
  testPattern,
58
64
  testCommand,
59
65
  certifyCommand,
@@ -86,39 +92,53 @@ export async function breakage({
86
92
  gitWasClean: false,
87
93
  }
88
94
  }
89
- const tagsRemote = await git.listRemote([`--tags`, `origin`])
95
+ let latestReleaseTag: string | undefined
96
+ let releaseCommit: string
97
+ if (baselineFile !== undefined) {
98
+ const baseline = await readBaseline(
99
+ git,
100
+ path.resolve(baseDirname, baselineFile),
101
+ { tagPattern },
102
+ )
103
+ latestReleaseTag = baseline.ref
104
+ releaseCommit = baseline.commit
105
+ mark?.(`read pinned release baseline`)
106
+ } else {
107
+ const tagsRemote = await git.listRemote([`--tags`, `origin`])
90
108
 
91
- mark?.(`list remote tags`)
92
- const latestReleaseTag = selectLatestReleaseTag(tagsRemote, tagPattern)
93
- mark?.(`found latest release tag`)
94
- if (!latestReleaseTag) {
95
- return {
96
- summary: `No tags found matching the pattern "${tagPattern}".`,
97
- gitWasClean: true,
98
- lastReleaseFound: false,
99
- } as const
100
- }
101
- const commonGitDirectory = await git.revparse([
102
- `--path-format=absolute`,
103
- `--git-common-dir`,
104
- ])
105
- await withDirectoryLock(
106
- path.join(commonGitDirectory, `breakage-fetch.lock`),
107
- () =>
108
- git.fetch([
109
- `origin`,
110
- `${latestReleaseTag}:${latestReleaseTag}`,
111
- `--no-tags`,
112
- `--no-write-fetch-head`,
113
- ]),
114
- )
109
+ mark?.(`list remote tags`)
110
+ latestReleaseTag = selectLatestReleaseTag(tagsRemote, tagPattern)
111
+ mark?.(`found latest release tag`)
112
+ if (!latestReleaseTag) {
113
+ return {
114
+ summary: `No tags found matching the pattern "${tagPattern}".`,
115
+ gitWasClean: true,
116
+ lastReleaseFound: false,
117
+ } as const
118
+ }
119
+ const commonGitDirectory = await git.revparse([
120
+ `--path-format=absolute`,
121
+ `--git-common-dir`,
122
+ ])
123
+ await withDirectoryLock(
124
+ path.join(commonGitDirectory, `breakage-fetch.lock`),
125
+ () =>
126
+ git.fetch([
127
+ `origin`,
128
+ `${latestReleaseTag}:${latestReleaseTag}`,
129
+ `--no-tags`,
130
+ `--no-write-fetch-head`,
131
+ ]),
132
+ )
115
133
 
116
- mark?.(`fetched latest release tag`)
134
+ releaseCommit = await git.revparse([`${latestReleaseTag}^{commit}`])
135
+ mark?.(`fetched latest release tag`)
136
+ }
117
137
 
118
138
  let productionFiles: string[]
119
139
  try {
120
140
  productionFiles = (
121
- await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, latestReleaseTag])
141
+ await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, releaseCommit])
122
142
  )
123
143
  .split(`\0`)
124
144
  .filter(Boolean)
@@ -164,10 +184,7 @@ export async function breakage({
164
184
  testsWereFound: false,
165
185
  } as const
166
186
  }
167
- const restore = await state.replaceTests(
168
- latestReleaseTag,
169
- productionTestFiles,
170
- )
187
+ const restore = await state.replaceTests(releaseCommit, productionTestFiles)
171
188
  mark?.(`restored public tests from latest release tag`)
172
189
  return { latestReleaseTag, productionTestFiles, restore }
173
190
  })
package/src/breakage.x.ts CHANGED
@@ -19,12 +19,13 @@ import {
19
19
  import logger from "takua"
20
20
 
21
21
  import type { BreakageOptions } from "./breakage.ts"
22
- import { breakage } from "./breakage.ts"
22
+ import { breakage, breakagePrelude } from "./breakage.ts"
23
23
 
24
24
  const BREAKAGE_MANUAL = options(
25
25
  `Check for breaking changes in a package.`,
26
26
  type({
27
27
  "tagPattern?": `string`,
28
+ "baselineFile?": `string`,
28
29
  testPattern: `string`,
29
30
  testCommand: `string`,
30
31
  certifyCommand: `string`,
@@ -40,6 +41,13 @@ const BREAKAGE_MANUAL = options(
40
41
  description: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,
41
42
  example: `--tagPattern="my-library"`,
42
43
  },
44
+ baselineFile: {
45
+ aliases: [`baseline-file`],
46
+ completion: { repeatable: false, fileSystem: `files` },
47
+ required: false,
48
+ description: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,
49
+ example: `--baseline-file=.breakage/baseline.json`,
50
+ },
43
51
  testPattern: {
44
52
  aliases: [`test-pattern`, `pattern`],
45
53
  completion: { repeatable: false },
@@ -83,6 +91,41 @@ const BREAKAGE_MANUAL = options(
83
91
  },
84
92
  ) satisfies OptionsGroup<BreakageOptions>
85
93
 
94
+ const PRELUDE_MANUAL = options(
95
+ `Resolve and fetch the release baseline for a cacheable compatibility check.`,
96
+ type({
97
+ "tagPattern?": `string`,
98
+ "baseDirname?": `string`,
99
+ "out?": `string`,
100
+ "testPattern?": `string`,
101
+ "testCommand?": `string`,
102
+ "certifyCommand?": `string`,
103
+ "baselineFile?": `string`,
104
+ "verbose?": `boolean`,
105
+ }),
106
+ {
107
+ ...BREAKAGE_MANUAL.optionConfigs,
108
+ testPattern: {
109
+ ...BREAKAGE_MANUAL.optionConfigs.testPattern,
110
+ required: false,
111
+ },
112
+ testCommand: {
113
+ ...BREAKAGE_MANUAL.optionConfigs.testCommand,
114
+ required: false,
115
+ },
116
+ certifyCommand: {
117
+ ...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
118
+ required: false,
119
+ },
120
+ out: {
121
+ completion: { repeatable: false, fileSystem: `files` },
122
+ required: false,
123
+ description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
124
+ example: `--out=.breakage/baseline.json`,
125
+ },
126
+ },
127
+ )
128
+
86
129
  const SCHEMA_MANUAL = options(
87
130
  `Create a copy of the schema for configuring breakage.`,
88
131
  type({ "outdir?": `string` }),
@@ -101,20 +144,31 @@ const SCHEMA_MANUAL = options(
101
144
  const parse = cli(
102
145
  {
103
146
  cliName: `breakage`,
104
- routes: optional({ help: null, schema: null, $configPath: null }),
147
+ routes: optional({
148
+ help: null,
149
+ schema: null,
150
+ prelude: optional({ $configPath: null }),
151
+ $configPath: null,
152
+ }),
105
153
  routeOptions: {
106
154
  "": BREAKAGE_MANUAL,
107
155
  $configPath: BREAKAGE_MANUAL,
108
156
  help: noOptions(`Show usage.`),
109
157
  schema: SCHEMA_MANUAL,
158
+ prelude: PRELUDE_MANUAL,
159
+ "prelude/$configPath": PRELUDE_MANUAL,
160
+ },
161
+ positionalCompletions: {
162
+ $configPath: { fileSystem: `files` },
163
+ "prelude/$configPath": { fileSystem: `files` },
110
164
  },
111
- positionalCompletions: { $configPath: { fileSystem: `files` } },
112
165
  discoverConfigPath: (args: readonly string[]) => {
113
166
  if (args[0] === `help` || args[0] === `schema`) {
114
167
  return
115
168
  }
116
169
  const configPath =
117
- args[0] ?? path.join(process.cwd(), `breakage.config.json`)
170
+ (args[0] === `prelude` ? args[1] : args[0]) ??
171
+ path.join(process.cwd(), `breakage.config.json`)
118
172
  return configPath
119
173
  },
120
174
  },
@@ -142,6 +196,15 @@ if (completion !== undefined) {
142
196
  process.stdout.write(`📝 Wrote json.schema.`)
143
197
  }
144
198
  break
199
+ case `prelude`:
200
+ case `prelude/$configPath`: {
201
+ const baseline = await breakagePrelude({
202
+ ...inputs.opts,
203
+ out: inputs.opts.out ?? `.breakage/baseline.json`,
204
+ })
205
+ process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`)
206
+ break
207
+ }
145
208
  case ``:
146
209
  case `$configPath`: {
147
210
  const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
@@ -1 +0,0 @@
1
- {"version":3,"file":"breakage-D_nc-zEK.js","names":["latestReleaseTag","selectLatestReleaseTag"],"sources":["../src/directory-lock.ts","../src/release-tag.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 {\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 { withDirectoryLock } from \"./directory-lock.ts\"\nimport { latestReleaseTag as selectLatestReleaseTag } from \"./release-tag.ts\"\nimport { withTestFileState } from \"./test-file-state.ts\"\n\nexport type BreakageOptions = {\n\ttagPattern?: 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\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\tconst tagsRemote = await git.listRemote([`--tags`, `origin`])\n\n\tmark?.(`list remote tags`)\n\tconst latestReleaseTag = selectLatestReleaseTag(tagsRemote, tagPattern)\n\tmark?.(`found latest release tag`)\n\tif (!latestReleaseTag) {\n\t\treturn {\n\t\t\tsummary: `No tags found matching the pattern \"${tagPattern}\".`,\n\t\t\tgitWasClean: true,\n\t\t\tlastReleaseFound: false,\n\t\t} as const\n\t}\n\tconst commonGitDirectory = await git.revparse([\n\t\t`--path-format=absolute`,\n\t\t`--git-common-dir`,\n\t])\n\tawait withDirectoryLock(\n\t\tpath.join(commonGitDirectory, `breakage-fetch.lock`),\n\t\t() =>\n\t\t\tgit.fetch([\n\t\t\t\t`origin`,\n\t\t\t\t`${latestReleaseTag}:${latestReleaseTag}`,\n\t\t\t\t`--no-tags`,\n\t\t\t\t`--no-write-fetch-head`,\n\t\t\t]),\n\t)\n\n\tmark?.(`fetched latest release tag`)\n\n\tlet productionFiles: string[]\n\ttry {\n\t\tproductionFiles = (\n\t\t\tawait git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, latestReleaseTag])\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(\n\t\t\tlatestReleaseTag,\n\t\t\tproductionTestFiles,\n\t\t)\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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"}