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 +74 -54
- package/dist/{breakage-D_nc-zEK.js → breakage-Dc4IldeW.js} +113 -23
- package/dist/breakage-Dc4IldeW.js.map +1 -0
- package/dist/breakage.$configPath.schema.json +3 -0
- package/dist/breakage.d.ts +22 -1
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +1 -1
- package/dist/breakage.main.schema.json +3 -0
- package/dist/breakage.prelude.$configPath.schema.json +31 -0
- package/dist/breakage.prelude.schema.json +31 -0
- package/dist/breakage.x.js +63 -4
- package/dist/breakage.x.js.map +1 -1
- package/package.json +4 -5
- package/src/baseline.ts +143 -0
- package/src/breakage.ts +48 -31
- package/src/breakage.x.ts +67 -4
- package/dist/breakage-D_nc-zEK.js.map +0 -1
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
|
|
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`
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
`
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
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":"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"}
|
package/dist/breakage.d.ts
CHANGED
|
@@ -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
|
package/dist/breakage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breakage.d.ts","names":[],"sources":["../src/breakage.ts"],"mappings":";
|
|
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-
|
|
1
|
+
export { t as breakage, n as breakagePrelude } from "./breakage-Dc4IldeW.js";
|
|
@@ -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
|
+
}
|
package/dist/breakage.x.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as breakage } from "./breakage-
|
|
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), {
|
package/dist/breakage.x.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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",
|
package/src/baseline.ts
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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`,
|
|
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({
|
|
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]
|
|
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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"}
|