breakage 0.0.0-stage → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Mozilla Public License Version 2.0
2
+
3
+ This Source Code Form is subject to the terms of the Mozilla Public
4
+ License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
package/README.md CHANGED
@@ -1,3 +1,235 @@
1
- # Temporary Holding Version
1
+ # breakage
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run yesterday’s tests against tomorrow’s release.
4
+
5
+ Command line tooling to detect breaking changes before you ship them.
6
+
7
+ ## migrating from break-check
8
+
9
+ Breakage continues [break-check](https://github.com/jeremybanka/wayforge/tree/break-check%400.8.1/packages/break-check) in its own repository. Install `breakage`, replace the `break-check` command with `breakage`, and rename `break-check.config.json` to `breakage.config.json`. Configuration keys and check outcomes are unchanged. Regenerate schemas with `breakage schema`; generated files now use the `breakage` prefix. Reinstall shell completions with `breakage completion install <shell>`.
10
+
11
+ For programmatic use, import `breakage`, `BreakageOptions`, and `BreakageOutcome` from `breakage` in place of `breakCheck`, `BreakCheckOptions`, and `BreakCheckOutcome` from `break-check`.
12
+
13
+ Finish active break-check runs and recover any interrupted runs before switching. Breakage stores new recovery records under `breakage/` in the worktree's Git directory and its fetch lock at `breakage-fetch.lock` in the common Git directory.
14
+
15
+ ## can i use breakage?
16
+
17
+ breakage is a tool for projects where
18
+
19
+ 1. releases must follow semantic versioning
20
+ 2. source code and tests are kept in separate files
21
+ 3. new releases are associated to git tags
22
+ 4. you can run tests from the command line
23
+
24
+ **if that sounds like your project, read on!**
25
+
26
+ breakage distinguishes two kinds of tests:
27
+
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
+ 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
+
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.
32
+
33
+ ## help
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.
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.
38
+
39
+ ## examples
40
+
41
+ ### single-project repository
42
+
43
+ ```bash
44
+ npx breakage \
45
+ --tagPattern="refs/tags/my-library@" \
46
+ --testPattern="tests/public/**/*" \
47
+ --testCommand="npm run test:public" \
48
+ --certifyCommand="false"
49
+ ```
50
+
51
+ Run this from the project root. It restores files matching `tests/public/**/*` from the newest matching `my-library@<version>` release tag and runs your project's `npm run test:public` command. `certifyCommand="false"` leaves any detected break uncertified; replace it with a project command that validates your release plan when you add intentional-break certification.
52
+
53
+ ### multi-project monorepo
54
+
55
+ ```bash
56
+ npx breakage \
57
+ --tagPattern="refs/tags/my-library@" \
58
+ --testPattern="packages/my-library/tests/public/**/*" \
59
+ --testCommand="cd packages/my-library && npm run test:public" \
60
+ --certifyCommand="false"
61
+ ```
62
+
63
+ Run this from the repository root. It restores the package's released public tests and runs its current public-test command. Patterns and commands are relative to the check's working directory; use `--baseDirname` if you want to run the check from a package directory instead.
64
+
65
+ In both examples, a failed test command remains uncertified and makes the CLI exit nonzero. Inspect the failure to distinguish a consumer regression from a build, runner, or setup problem.
66
+
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
+
69
+ ## writing consumer contracts
70
+
71
+ 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
+
73
+ ### choose the behavior deliberately
74
+
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.
76
+
77
+ 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
+
79
+ The exported object model can itself be public API. If consumers construct a document graph or inspect a returned dictionary, protect the promised identities, values, and relationships. Follow construction through to independently observed output; a correct JavaScript representation does not establish that the library writes it faithfully.
80
+
81
+ Private tests remain useful for implementation-specific assertions and visual baselines. An exact screenshot or historical byte sequence belongs in the public contract only when preserving that exact output is an intentional consumer promise. Same-run comparisons of repeated and equivalent inputs can protect determinism without requiring identical bytes across releases.
82
+
83
+ ### make the expected result independent
84
+
85
+ Derive expected values from the scenario's requested behavior, documented semantics, or an independent observer. Comparing two paths through the current implementation allows both to become identically wrong.
86
+
87
+ For example, reading the output of both a document builder and a standalone serializer and comparing the results will not catch both producing a 1×1 page. Assert the requested dimensions directly. Check document identifiers, font selection, text origins, and binary values when those are part of the scenario.
88
+
89
+ Choose inputs that make omissions visible. Request a nondefault stroke color; black cannot distinguish a working setter from a missing setter when black is already the default. To test visual regression detection, compare documents with different painted content but identical geometry and rendering options. Changing a background option may change a manifest and report a mismatch even if pixel comparison is disabled. Require meaningful page differences as well as the failure result.
90
+
91
+ Use malformed descriptions to exercise rejection by the exported validator and serializer, not just checks in a higher-level builder. A reader successfully opening a document may also conceal a serialization defect if it repairs malformed structures. Verify the relevant structural property independently when validity is part of the promise.
92
+
93
+ ### avoid accidental commitments
94
+
95
+ Keep setup inline and specific to each scenario, or use a narrowly named helper that explains exactly what it constructs. A generic shared "example document" can make unrelated tests depend on arbitrary page shapes and metadata. Include any fixture or helper that determines historical expectations in the restore pattern.
96
+
97
+ Preserve outcomes at the point consumers need them. If invalid input only needs to be rejected before serialization succeeds, encompass construction and serialization in the throwing assertion. Requiring rejection at a particular intermediate call is a stronger promise; make it only deliberately.
98
+
99
+ Inspect helpers for constraints too. A baseline-preservation helper should collect files recursively rather than assume the artifact directory is flat. A decoder should accept equivalent encodings rather than require the current serializer's spacing, dictionary order, or hexadecimal capitalization. Allow generated resource names and allocation order to vary unless consumers actually depend on them.
100
+
101
+ ### preserve the observation code
102
+
103
+ Released assertions need trustworthy readers. If today's writer and today's observation helper can change together, an unchanged historical assertion can still become weaker. Include the helper's source in `testPattern` when its behavior determines the meaning of those assertions.
104
+
105
+ Mondrian publishes its independent inspection tools through its testing entrypoint and restores their source alongside the public tests:
106
+
107
+ ```json
108
+ {
109
+ "tagPattern": "refs/tags/mondrian\\.pdf@",
110
+ "testPattern": "{tests/public,src/testing/inspection}/**/*",
111
+ "testCommand": "pnpm test:public",
112
+ "certifyCommand": "false"
113
+ }
114
+ ```
115
+
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.
117
+
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.
119
+
120
+ 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
+
122
+ ### check that the tests can fail for the right reason
123
+
124
+ Before releasing the suite, deliberately introduce representative regressions in a disposable checkout. Run the public command against each mutation and inspect the failure, then restore the checkout. Require the intended assertion or consumer-import/type check to fail; an unrelated build, setup, or test failure proves nothing about that contract.
125
+
126
+ | Mutation | Contract that should detect it |
127
+ | ------------------------------------------------- | -------------------------------------------------------- |
128
+ | Remove an exported function or declaration | Consumer imports and type checking |
129
+ | Ignore a requested position, color, or identifier | Independent expected output values |
130
+ | Give distinct resources the same generated name | Multiple distinguishable resources used together |
131
+ | Return success for every validation request | Malformed input requiring diagnostics and rejection |
132
+ | Disable page-difference detection | Changed painted content with identical rendering options |
133
+ | Drop byte-keyed entries during serialization | Independently decoded names and values |
134
+
135
+ Test the other direction too: harmless refactors should pass. Try moving an internal source entrypoint while preserving package exports, rejecting invalid input earlier or later within the promised boundary, nesting artifact files, or switching between equivalent encodings. This exposes accidental commitments before they become historical requirements.
136
+
137
+ Also verify the restoration mechanism once: change a current observation helper so the public command fails, commit the probe in a disposable checkout, and compare against a known release containing the original helper. Check that the historical helper is restored and used, and that the original working copy returns afterward. Use a local-only release for experiments rather than publishing a probe tag.
138
+
139
+ ### run current contracts and historical contracts
140
+
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.
142
+
143
+ 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
+
145
+ When the job fails, identify the phase before deciding on a breaking release. A failed current test, incompatible runner types, a build problem, a missing baseline, and a released assertion detecting a consumer regression require different responses. Certification should validate an intentional breaking-release plan after that review; using a successful certification command to bypass an unexplained infrastructure failure does not establish compatibility.
146
+
147
+ ### case study: mondrian.pdf
148
+
149
+ [Mondrian's adoption PR](https://github.com/jeremybanka/mondrian/pull/108) separated consumer contracts from implementation tests and exact visual proofs. Independent reviews introduced mutations that still passed the proposed public suite, including ignored positions and identifiers, collapsed resources, and disabled visual differences. Strengthening the corresponding scenarios made those regressions observable. Refactor probes also removed unintended commitments to rejection timing and flat artifact directories.
150
+
151
+ An October 5, 2026 audit examined 237 subsequent Breakage jobs: 186 passed, 48 failed, and 3 were cancelled. Of the failures, 13 were the initial missing-release-baseline result; the other 35 stopped in current-suite type checking with mismatched Vitest dependencies before historical comparison ran. After the baseline was established, the default branch had 64 passing checks and no failing checks. No failed job in that audit was traced to an arbitrary PDF expectation. See the [workflow history](https://github.com/jeremybanka/mondrian/actions/workflows/test.yml), a [baseline failure](https://github.com/jeremybanka/mondrian/actions/runs/35064496994/job/104691685448), and a [runner-alignment failure](https://github.com/jeremybanka/mondrian/actions/runs/36281497246/job/108514056194).
152
+
153
+ The contracts continued to pass through [PDFium changes](https://github.com/jeremybanka/mondrian/actions/runs/35280593318/job/105401343541) and an [aligned Vite Plus upgrade](https://github.com/jeremybanka/mondrian/actions/runs/36543626809/job/109324668309). That is evidence for the chosen boundaries over the audited changes, not a claim that the suite covers every possible consumer regression.
154
+
155
+ ## git environment
156
+
157
+ breakage uses simple-git v4, which filters inherited `GIT_*` environment variables and certain other Git-related variables, including `SSH_ASKPASS`, from its Git subprocesses. Environment-based settings such as `GIT_SSH_COMMAND`, `GIT_ASKPASS`, and `GIT_CONFIG_*` no longer configure tag discovery, fetching, or file restoration. Configure authentication and Git behavior through Git configuration files, SSH configuration, or an SSH agent instead. This filtering applies to breakage's Git operations; test and certification commands still inherit the normal process environment.
158
+
159
+ ## parallel checks and file restoration
160
+
161
+ Checks with disjoint public test paths can run their test and certification commands in parallel, in their original working directories. breakage coordinates Git setup and cleanup with a short-lived lock and records each running check's test paths. The clean-repository check ignores only the paths owned by active checks; unrelated uncommitted changes still prevent a new check from starting. Checks with overlapping test paths are rejected before replacing files. Remote tag discovery runs outside this lock, and fetches use a separate lock in the common Git directory. Cleanup waits for file-restoration access instead of giving up when a setup timeout expires.
162
+
163
+ Released tests are loaded with `git restore --worktree`. After testing and certification, including failures, breakage restores only those paths to their starting commit and removes tests that existed only in the release. The Git index and existing stashes are preserved. Unrelated files, command outputs, and changes made by other packages are left in place. There is no repository or dependency copy. Commands that themselves modify shared files or Git state still need their own coordination.
164
+
165
+ If a process is forcibly interrupted or restoration fails, breakage retains recovery information under `breakage/` in the worktree's Git directory (`git rev-parse --absolute-git-dir`). After confirming the process has stopped, restore each recorded path from the recorded `head` commit, remove any recorded paths absent from that commit, and remove the record. An interrupted setup or cleanup can also leave the `breakage/lock` directory; remove it only after confirming no setup or cleanup is still running. An interrupted fetch can leave `breakage-fetch.lock` in the common Git directory; remove it only after confirming no fetch is still running.
166
+
167
+ ## options
168
+
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-->
215
+
216
+ ## cli completion
217
+
218
+ 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
+
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.
221
+
222
+ ## cli option aliases and warnings
223
+
224
+ These aliases work alongside the original option names. Configuration file keys remain unchanged.
225
+
226
+ | Original option | Aliases |
227
+ | --------------------------- | ------------------------------ |
228
+ | `--tagPattern` | `--tag-pattern` |
229
+ | `--testPattern` | `--test-pattern`, `--pattern` |
230
+ | `--testCommand` | `--test-command` |
231
+ | `--certifyCommand` | `--certify-command` |
232
+ | `--baseDirname` | `--base-dir`, `--base-dirname` |
233
+ | `--outdir` (schema command) | `--out-dir` |
234
+
235
+ 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.
@@ -0,0 +1,345 @@
1
+ import { exec } from "node:child_process";
2
+ import path from "node:path";
3
+ import { minimatch } from "minimatch";
4
+ import { simpleGit } from "simple-git";
5
+ import logger from "takua";
6
+ import { chmod, lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir, writeFile } from "node:fs/promises";
7
+ import { setTimeout } from "node:timers/promises";
8
+ import { randomUUID } from "node:crypto";
9
+ //#region src/directory-lock.ts
10
+ async function withDirectoryLock(lock, action, waitForCleanup = false) {
11
+ const deadline = Date.now() + 6e4;
12
+ for (;;) try {
13
+ await mkdir(lock);
14
+ break;
15
+ } catch (thrown) {
16
+ if (thrown.code !== `EEXIST`) throw thrown;
17
+ if (!waitForCleanup && Date.now() >= deadline) throw new Error(`Timed out waiting for ${lock}. Check for an interrupted operation before removing this lock.`);
18
+ await setTimeout(20);
19
+ }
20
+ try {
21
+ return await action();
22
+ } finally {
23
+ await rmdir(lock);
24
+ }
25
+ }
26
+ //#endregion
27
+ //#region src/release-tag.ts
28
+ function compareInteger(a, b) {
29
+ return a.length - b.length || (a > b ? 1 : a < b ? -1 : 0);
30
+ }
31
+ function compareRelease(a, b) {
32
+ for (let index = 0; index < 3; index++) {
33
+ const difference = compareInteger(a.core[index], b.core[index]);
34
+ if (difference) return difference;
35
+ }
36
+ if (!a.prerelease.length || !b.prerelease.length) return Number(!a.prerelease.length) - Number(!b.prerelease.length);
37
+ for (let index = 0; index < Math.min(a.prerelease.length, b.prerelease.length); index++) {
38
+ const left = a.prerelease[index];
39
+ const right = b.prerelease[index];
40
+ const leftNumeric = /^\d+$/.test(left);
41
+ const rightNumeric = /^\d+$/.test(right);
42
+ const difference = leftNumeric && rightNumeric ? compareInteger(left, right) : leftNumeric !== rightNumeric ? Number(rightNumeric) - Number(leftNumeric) : left > right ? 1 : left < right ? -1 : 0;
43
+ if (difference) return difference;
44
+ }
45
+ return a.prerelease.length - b.prerelease.length;
46
+ }
47
+ function latestReleaseTag(remoteTags, tagPattern) {
48
+ const pattern = tagPattern ? new RegExp(tagPattern) : void 0;
49
+ let latest;
50
+ for (const line of remoteTags.split(`\n`)) {
51
+ if (pattern && !pattern.test(line)) continue;
52
+ const ref = line.split(`\t`)[1]?.replace(/\^\{\}$/, ``);
53
+ if (!ref?.startsWith(`refs/tags/`)) continue;
54
+ const name = ref.slice(`refs/tags/`.length);
55
+ const version = name.slice(name.lastIndexOf(`@`) + 1);
56
+ const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
57
+ if (!match) continue;
58
+ const prerelease = match[4]?.split(`.`) ?? [];
59
+ if (prerelease.some((identifier) => /^0\d+$/.test(identifier))) continue;
60
+ const release = {
61
+ ref,
62
+ core: match.slice(1, 4),
63
+ prerelease
64
+ };
65
+ if (!latest || compareRelease(release, latest) > 0) latest = release;
66
+ }
67
+ return latest?.ref;
68
+ }
69
+ //#endregion
70
+ //#region src/test-file-state.ts
71
+ function isActiveCheck(value) {
72
+ if (typeof value !== `object` || value === null) return false;
73
+ const record = value;
74
+ return typeof record.pid === `number` && Number.isSafeInteger(record.pid) && record.pid > 0 && typeof record.head === `string` && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(record.head) && (record.recoveryRequired === void 0 || typeof record.recoveryRequired === `boolean`) && Array.isArray(record.paths) && record.paths.length > 0 && record.paths.every((file) => typeof file === `string` && !path.isAbsolute(file) && !path.win32.isAbsolute(file) && file.split(`/`).every((part) => part !== `` && part !== `.` && part !== `..`));
75
+ }
76
+ async function readRecoveryRecord(recordPath) {
77
+ try {
78
+ const record = JSON.parse(await readFile(recordPath, `utf8`));
79
+ if (!isActiveCheck(record)) throw new TypeError(`Invalid recovery record fields`);
80
+ return record;
81
+ } catch (cause) {
82
+ throw new Error(`A breakage run needs recovery. The record ${recordPath} is unreadable or invalid. Verify no check is running and recover its test files before removing this record.`, { cause });
83
+ }
84
+ }
85
+ async function writeRecoveryRecord(recordPath, record) {
86
+ const temporaryPath = `${recordPath}.${randomUUID()}.tmp`;
87
+ try {
88
+ await writeFile(temporaryPath, JSON.stringify(record), { flag: `wx` });
89
+ await rename(temporaryPath, recordPath);
90
+ } finally {
91
+ await rm(temporaryPath, { force: true });
92
+ }
93
+ }
94
+ function assertContained(root, filename) {
95
+ const relative = path.relative(root, filename);
96
+ if (relative === `..` || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`Cannot replace tests outside the worktree: ${filename}.`);
97
+ }
98
+ async function validateParents(root, filenames) {
99
+ const checked = /* @__PURE__ */ new Set();
100
+ for (const filename of filenames) {
101
+ assertContained(root, filename);
102
+ for (let parent = path.dirname(filename);; parent = path.dirname(parent)) {
103
+ if (checked.has(parent)) break;
104
+ const stats = await lstat(parent).catch((thrown) => {
105
+ if (thrown.code !== `ENOENT`) throw thrown;
106
+ });
107
+ if (stats && !stats.isDirectory()) throw new Error(`Cannot replace tests through ${parent}.`);
108
+ checked.add(parent);
109
+ if (parent === root) break;
110
+ }
111
+ }
112
+ }
113
+ async function withTestFileState(git, baseDirname, action) {
114
+ const root = await realpath(await git.revparse([`--show-toplevel`]));
115
+ baseDirname = await realpath(baseDirname);
116
+ assertContained(root, baseDirname);
117
+ const stateDirectory = path.join(await git.revparse([`--absolute-git-dir`]), `breakage`);
118
+ await mkdir(stateDirectory, { recursive: true });
119
+ const lock = path.join(stateDirectory, `lock`);
120
+ return withDirectoryLock(lock, async () => {
121
+ const activeChecks = [];
122
+ for (const entry of await readdir(stateDirectory)) {
123
+ if (!entry.endsWith(`.json`)) continue;
124
+ const recordPath = path.join(stateDirectory, entry);
125
+ const record = await readRecoveryRecord(recordPath);
126
+ let interrupted = record.recoveryRequired;
127
+ try {
128
+ process.kill(record.pid, 0);
129
+ } catch (thrown) {
130
+ if (thrown.code !== `EPERM`) interrupted = true;
131
+ }
132
+ if (interrupted) throw new Error(`A breakage run needs recovery. Restore the test paths recorded in ${recordPath} before removing that record.`);
133
+ activeChecks.push(record);
134
+ }
135
+ const activePaths = activeChecks.flatMap((record) => record.paths);
136
+ return action({
137
+ isClean: async () => !await simpleGit(root).raw([
138
+ `--no-optional-locks`,
139
+ `status`,
140
+ `--porcelain`,
141
+ `--untracked-files=all`,
142
+ `--`,
143
+ `.`,
144
+ ...activePaths.map((file) => `:(top,literal,exclude)${file}`)
145
+ ]),
146
+ replaceTests: async (release, files) => {
147
+ const paths = files.map((file) => path.relative(root, path.resolve(baseDirname, file)).split(path.sep).join(`/`));
148
+ for (const file of paths) if (activePaths.some((active) => file === active || file.startsWith(`${active}/`) || active.startsWith(`${file}/`))) throw new Error(`Another breakage run is using ${file}. Concurrent checks must use disjoint test paths.`);
149
+ const head = await git.revparse([`HEAD`]);
150
+ const currentFiles = new Set((await git.raw([
151
+ `ls-tree`,
152
+ `-r`,
153
+ `--name-only`,
154
+ `-z`,
155
+ head
156
+ ])).split(`\0`).filter(Boolean));
157
+ const existing = files.filter((file) => currentFiles.has(file));
158
+ const absent = files.filter((file) => !currentFiles.has(file));
159
+ const modes = /* @__PURE__ */ new Map();
160
+ const filenames = files.map((file) => path.resolve(baseDirname, file));
161
+ await validateParents(root, filenames);
162
+ for (const file of files) {
163
+ const filename = path.resolve(baseDirname, file);
164
+ const stats = await lstat(filename).catch((thrown) => {
165
+ if (thrown.code !== `ENOENT`) throw thrown;
166
+ });
167
+ if (!currentFiles.has(file) && stats) throw new Error(`Cannot overwrite the existing untracked path ${filename}.`);
168
+ if (stats?.isFile()) modes.set(filename, stats.mode);
169
+ }
170
+ const recordPath = path.join(stateDirectory, `${randomUUID()}.json`);
171
+ const record = {
172
+ pid: process.pid,
173
+ paths,
174
+ head
175
+ };
176
+ await writeRecoveryRecord(recordPath, record);
177
+ const restore = async () => {
178
+ await writeRecoveryRecord(recordPath, {
179
+ ...record,
180
+ recoveryRequired: true
181
+ });
182
+ await validateParents(root, filenames);
183
+ if (existing.length) await git.raw([
184
+ `--literal-pathspecs`,
185
+ `restore`,
186
+ `--source=${head}`,
187
+ `--worktree`,
188
+ `--`,
189
+ ...existing
190
+ ]);
191
+ for (const file of absent) await rm(path.resolve(baseDirname, file), { force: true });
192
+ for (const [filename, mode] of modes) await chmod(filename, mode);
193
+ await rm(recordPath);
194
+ };
195
+ try {
196
+ await git.raw([
197
+ `--literal-pathspecs`,
198
+ `restore`,
199
+ `--source=${release}`,
200
+ `--worktree`,
201
+ `--`,
202
+ ...files
203
+ ]);
204
+ } catch (thrown) {
205
+ await restore();
206
+ throw thrown;
207
+ }
208
+ return () => withDirectoryLock(lock, restore, true);
209
+ }
210
+ });
211
+ });
212
+ }
213
+ //#endregion
214
+ //#region src/breakage.ts
215
+ async function breakage({ tagPattern, testPattern, testCommand, certifyCommand, baseDirname = process.cwd(), verbose = false }) {
216
+ let mark;
217
+ let logMarks;
218
+ if (verbose) {
219
+ const chronicle = logger.makeChronicle();
220
+ mark = chronicle.mark;
221
+ logMarks = chronicle.logMarks;
222
+ }
223
+ mark?.(`breakage`);
224
+ const git = simpleGit(baseDirname);
225
+ mark?.(`spawn git`);
226
+ const isGitRepo = await git.checkIsRepo();
227
+ mark?.(`is git repository`);
228
+ if (!isGitRepo) return {
229
+ summary: `The git repository must be clean to run this command.`,
230
+ gitWasClean: false
231
+ };
232
+ if (!await withTestFileState(git, baseDirname, (state) => state.isClean())) return {
233
+ summary: `The git repository must be clean to run this command.`,
234
+ gitWasClean: false
235
+ };
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`);
253
+ let productionFiles;
254
+ try {
255
+ productionFiles = (await git.raw([
256
+ `ls-tree`,
257
+ `-r`,
258
+ `--name-only`,
259
+ `-z`,
260
+ latestReleaseTag$1
261
+ ])).split(`\0`).filter(Boolean);
262
+ } catch (thrown) {
263
+ mark?.(`failed to list production files`);
264
+ const message = thrown instanceof Error ? thrown.message : typeof thrown === `string` ? thrown : void 0;
265
+ return {
266
+ summary: `Failed to list production files${message ? `: ${message}` : `.`}`,
267
+ gitWasClean: true,
268
+ lastReleaseFound: true,
269
+ lastReleaseTag: latestReleaseTag$1,
270
+ testsWereFound: false
271
+ };
272
+ }
273
+ mark?.(`listed all files checked into git`);
274
+ const productionTestFiles = productionFiles.filter((file) => minimatch(file, testPattern));
275
+ mark?.(`filtered to public test files`);
276
+ const prepared = await withTestFileState(git, baseDirname, async (state) => {
277
+ if (!await state.isClean()) return {
278
+ summary: `The git repository must be clean to run this command.`,
279
+ gitWasClean: false
280
+ };
281
+ if (productionTestFiles.length === 0) return {
282
+ summary: `No tests were found matching the pattern "${testPattern}".`,
283
+ gitWasClean: true,
284
+ lastReleaseFound: true,
285
+ lastReleaseTag: latestReleaseTag$1,
286
+ testsWereFound: false
287
+ };
288
+ const restore = await state.replaceTests(latestReleaseTag$1, productionTestFiles);
289
+ mark?.(`restored public tests from latest release tag`);
290
+ return {
291
+ latestReleaseTag: latestReleaseTag$1,
292
+ productionTestFiles,
293
+ restore
294
+ };
295
+ });
296
+ if (prepared.summary !== void 0) return prepared;
297
+ const { restore } = prepared;
298
+ try {
299
+ const noBreakingChangesDetected = await new Promise((resolve, reject) => {
300
+ const result = exec(testCommand, { cwd: baseDirname }, (_, __, stderr) => {
301
+ if (result.exitCode === 0) resolve({
302
+ summary: `No breaking changes were detected.`,
303
+ gitWasClean: true,
304
+ lastReleaseFound: true,
305
+ lastReleaseTag: latestReleaseTag$1,
306
+ testsWereFound: true,
307
+ testsFound: productionTestFiles,
308
+ breakingChangesFound: false
309
+ });
310
+ else reject(stderr);
311
+ });
312
+ });
313
+ mark?.(`passed tests`);
314
+ return noBreakingChangesDetected;
315
+ } catch (thrown) {
316
+ mark?.(`failed tests`);
317
+ return await new Promise((resolve) => {
318
+ const result = exec(certifyCommand, { cwd: baseDirname }, (_, stdout, stderr) => {
319
+ const wereCertified = result.exitCode === 0;
320
+ mark?.(`${wereCertified ? `passed` : `failed`} certification`);
321
+ resolve({
322
+ summary: wereCertified ? `Breaking changes were found and certified.` : `Breaking changes were found, but not certified.`,
323
+ gitWasClean: true,
324
+ lastReleaseFound: true,
325
+ lastReleaseTag: latestReleaseTag$1,
326
+ testsWereFound: true,
327
+ testsFound: productionTestFiles,
328
+ breakingChangesFound: true,
329
+ testResult: thrown,
330
+ breakingChangesCertified: wereCertified,
331
+ certificationStdout: stdout,
332
+ certificationStderr: stderr
333
+ });
334
+ });
335
+ });
336
+ } finally {
337
+ await restore();
338
+ mark?.(`restored original public tests`);
339
+ logMarks?.();
340
+ }
341
+ }
342
+ //#endregion
343
+ export { breakage as t };
344
+
345
+ //# sourceMappingURL=breakage-D_nc-zEK.js.map
@@ -0,0 +1 @@
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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"}