breakage 0.8.5 → 0.9.0
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 +163 -47
- package/dist/{breakage-B7dJsgcu.js → breakage-DIY49fzP.js} +331 -217
- package/dist/breakage-DIY49fzP.js.map +1 -0
- package/dist/breakage.d.ts +111 -55
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +3 -1
- package/dist/breakage.x.js +78 -80
- package/dist/breakage.x.js.map +1 -1
- package/dist/changesets.d.ts +8 -0
- package/dist/changesets.d.ts.map +1 -0
- package/dist/changesets.js +27 -0
- package/dist/changesets.js.map +1 -0
- package/dist/release-BsdIDT4g.js +69 -0
- package/dist/release-BsdIDT4g.js.map +1 -0
- package/dist/release-D47BqRuB.d.ts +40 -0
- package/dist/release-D47BqRuB.d.ts.map +1 -0
- package/npm.md +55 -33
- package/package.json +45 -6
- package/src/breakage.ts +125 -73
- package/src/breakage.x.ts +148 -127
- package/src/changesets.ts +51 -0
- package/src/config.ts +128 -0
- package/src/npm/check.ts +51 -50
- package/src/npm/cli.ts +26 -22
- package/src/npm/config.ts +38 -6
- package/src/npm/environment.ts +10 -10
- package/src/npm/release.ts +9 -25
- package/src/npm/types.ts +5 -45
- package/src/release.ts +153 -0
- package/src/{npm/command.ts → task.ts} +2 -0
- package/dist/breakage-B7dJsgcu.js.map +0 -1
- package/dist/breakage.$configPath.schema.json +0 -33
- package/dist/breakage.help.schema.json +0 -6
- package/dist/breakage.main.schema.json +0 -33
- package/dist/breakage.npm.check.schema.json +0 -16
- package/dist/breakage.npm.help.schema.json +0 -6
- package/dist/breakage.npm.schema.json +0 -6
- package/dist/breakage.npm.test.$peer.$range.schema.json +0 -16
- package/dist/breakage.npm.test.schema.json +0 -16
- package/dist/breakage.prelude.$configPath.schema.json +0 -31
- package/dist/breakage.prelude.schema.json +0 -31
- package/dist/breakage.schema.schema.json +0 -10
package/npm.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use `breakage npm` to test a packed package against the peer versions you declare. This is an optional npm-specific feature. The ordinary public check and its four adoption conditions remain the same; keep that check required alongside peer compatibility.
|
|
4
4
|
|
|
5
|
-
Consider an integration with `"atom.io": "^1 || ^2"`. Its current workspace tests may pass against an edited core, even if only the integration will ship. Representative environments let it prove compatibility with actual released versions. An explicitly registered candidate environment can additionally prove conditional compatibility with a core release your release plan
|
|
5
|
+
Consider an integration with `"atom.io": "^1 || ^2"`. Its current workspace tests may pass against an edited core, even if only the integration will ship. Representative environments let it prove compatibility with actual released versions. An explicitly registered candidate environment can additionally prove conditional compatibility with a core release your release plan identifies.
|
|
6
6
|
|
|
7
7
|
The npm commands require Node 22.18 or newer (including native erasable TypeScript for `.ts` configuration), Git, and your installation/test tools. They run on the filesystem, inside complete representative directories. They do not publish packages.
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ In the integration package, create complete environments for the peer formulatio
|
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Pin every dependency in an environment manifest to an exact version: `"host": "1.0.0"` and `"host": "2.1.4"` are valid; `"host": "^1"` and `"host": "^1 || ^2"` are setup errors. This applies to all entries in `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies`, including test runners and helpers. Exact prerelease and build versions are allowed. Ranges, wildcards, tags, aliases, workspace protocols, and file or URL specifiers are rejected before installation. The integration's own `peerDependencies` still express its supported eras with ranges. Enroll coordinated local releases through `breakage.candidates`; their
|
|
33
|
+
Pin every dependency in an environment manifest to an exact version: `"host": "1.0.0"` and `"host": "2.1.4"` are valid; `"host": "^1"` and `"host": "^1 || ^2"` are setup errors. This applies to all entries in `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies`, including test runners and helpers. Exact prerelease and build versions are allowed. Ranges, wildcards, tags, aliases, workspace protocols, and file or URL specifiers are rejected before installation. The integration's own `peerDependencies` still express its supported eras with ranges. Enroll coordinated local releases through `breakage.candidates`; their resolved intended versions supply the exact identities, and any accompanying dependency pins must match those versions.
|
|
34
34
|
|
|
35
35
|
Choose real published versions appropriate for your package. Install once and commit this directory's manifest and lock file. Add any test runner and helpers it needs to its own development dependencies with exact pins. Give each additional formulation its own directory and lock file. Every required peer of the integration must be present; several coupled peers share one environment. Include all intended representatives in the glob, including multiple representatives of the same era.
|
|
36
36
|
|
|
@@ -89,7 +89,7 @@ export default {
|
|
|
89
89
|
}
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Every file glob must select at least one regular file. Remove the fixture glob if you have no fixtures. `packageDirectory` defaults to the config file's directory; all npm paths resolve from that package directory. Glob discovery uses paths as labels and installed versions as evidence. Register local peer directories in `localPeers`; each declared local peer must have a suite, and its
|
|
92
|
+
Every file glob must select at least one regular file. Remove the fixture glob if you have no fixtures. `packageDirectory` defaults to the config file's directory; all npm paths resolve from that package directory. Glob discovery uses paths as labels and installed versions as evidence. Register local peer directories in `localPeers`; each declared local peer must have a suite, and its planned upcoming releases must be supported and exercised by a coordinated representative. External peer suites are an explicit opt-in. No matrix is inferred from registry metadata.
|
|
93
93
|
|
|
94
94
|
The previous manifest comes from the newest matching release tag. An existing core `baselineFile` may pin a fetched release. For a package's first publication, explicitly use `baseline: { firstRelease: true }` and remove that exception after publishing; missing baseline evidence never silently passes.
|
|
95
95
|
|
|
@@ -103,9 +103,9 @@ breakage npm test --representative peer-environments/core-1
|
|
|
103
103
|
breakage npm check --config ./breakage.config.ts --json
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
All matching representatives run. Every explicit `||` era needs coverage. Preview versions cover only an exact preview alternative, not stable ranges or other previews. Build metadata does not affect range matching, including exact preview selection; installed versions must still match their complete environment pins. Released representatives outside declared support warn; excluding a planned local release fails the release check. Focused requests can intentionally explore versions outside declared support. An audit reports declarations and release-policy findings without claiming assertions passed. Exit statuses are 0 for successful requested work, 1 for compatibility or
|
|
106
|
+
All matching representatives run. Every explicit `||` era needs coverage. Preview versions cover only an exact preview alternative, not stable ranges or other previews. Build metadata does not affect range matching, including exact preview selection; installed versions must still match their complete environment pins. Released representatives outside declared support warn; excluding a planned local release fails the release check. Focused requests can intentionally explore versions outside declared support. An audit reports declarations and release-policy findings without claiming assertions passed. Exit statuses are 0 for successful requested work, 1 for compatibility or unapproved support failures, and 2 for setup or coverage errors.
|
|
107
107
|
|
|
108
|
-
Focused peer/range requests validate manifest pins and skip versions outside the requested range before acquiring stages or preparing dependencies. Coordinated candidates are selected by their
|
|
108
|
+
Focused peer/range requests validate manifest pins and skip versions outside the requested range before acquiring stages or preparing dependencies. Coordinated candidates are selected by their resolved intended versions, and exact-preview selection follows the same rules as coverage. Excluded environments need no lockfile or available stage. Disjoint requests for the same peer can run independently in one checkout without parsing lock files. Requests that select the same environment still contend for its stage, including requests for different peers in a coupled environment. Selected installations retain all frozen-input, identity, and compatibility checks.
|
|
109
109
|
|
|
110
110
|
## Coordinated releases
|
|
111
111
|
|
|
@@ -122,54 +122,76 @@ Create a separate representative that explicitly names candidate peers. Leave an
|
|
|
122
122
|
}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
Set `release.resolveDelta` to a synchronous/asynchronous resolver receiving `packageName`, `packageDirectory`, and `currentVersion`. For Changesets, use the optional helper:
|
|
126
126
|
|
|
127
127
|
```ts
|
|
128
|
-
import {
|
|
129
|
-
import
|
|
128
|
+
import { changesetsReleaseDelta } from "breakage/changesets"
|
|
129
|
+
import { semverPolicy } from "breakage"
|
|
130
130
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
})
|
|
134
|
-
const plan = JSON.parse(
|
|
135
|
-
await readFile(new URL("./release-plan.json", import.meta.url), "utf8"),
|
|
136
|
-
)
|
|
137
|
-
const version = plan[packageName]
|
|
138
|
-
return version ? { status: "release", version } : { status: "none" }
|
|
131
|
+
const release = {
|
|
132
|
+
resolveDelta: changesetsReleaseDelta(),
|
|
133
|
+
policy: semverPolicy({ beforeV1: "minor" }),
|
|
139
134
|
}
|
|
140
135
|
```
|
|
141
136
|
|
|
142
|
-
Missing intent blocks that candidate environment. Inspection errors and invalid evidence are setup errors, not absent intent.
|
|
137
|
+
Assign this object to `npm.release`, or share it at the top level with `defineConfig`. A custom resolver returns `{ previousVersion, version, delta }` or `undefined`; errors must throw. Missing intent blocks that candidate environment. Inspection errors and invalid evidence are setup errors, not absent intent. Both transition versions must be exact, the target must be at least the current workspace version, and the delta must match the versions. Candidate identity uses the transition's `version`; no breaking-change policy is applied to candidate enrollment. The candidate archive may have its workspace version or the intended version. Breakage reports both versions. If they differ, it substitutes the intended version in the installed peer's disposable manifest using a fresh file and atomic replacement, preserving shared cache files, other environments, the archive, and the workspace. Archives already carrying their intended version need no substitution.
|
|
143
138
|
|
|
144
|
-
That substitution simulates release metadata; it does not rebuild the package or change version constants embedded in executable code. Your build owns those constants. To exercise the final version metadata directly, supply a peer archive already built with the
|
|
139
|
+
That substitution simulates release metadata; it does not rebuild the package or change version constants embedded in executable code. Your build owns those constants. To exercise the final version metadata directly, supply a peer archive already built with the resolved intended version.
|
|
145
140
|
|
|
146
|
-
Both `npm check` and its coverage-only audit inspect each currently declared `localPeers` package's release intent, even if you omitted its candidate environment. A
|
|
141
|
+
Both `npm check` and its coverage-only audit inspect each currently declared `localPeers` package's release intent, even if you omitted its candidate environment. A resolved intended version must be included in the integration's peer range and covered by a coordinated representative. Missing either fails with exit 2; an installed peer at the same version does not substitute for testing the local candidate. Optional local peers have the same obligation when declared. Resolver errors and invalid evidence fail setup. Resolvers are evaluated once per package and baseline context per invocation, and must return that package's own release intent.
|
|
147
142
|
|
|
148
|
-
For a core still labeled `0.53.3` with a planned `0.54.0` release, an integration declaring only `^0.53.3` fails. Adding `|| ^0.54.0` is recommended when retaining the older era. Admission uses version membership: `0.54.0`, `*`, and other ranges containing that version qualify too. Exact-preview enrollment, coverage of other declared eras, and
|
|
143
|
+
For a core still labeled `0.53.3` with a planned `0.54.0` release, an integration declaring only `^0.53.3` fails. Adding `|| ^0.54.0` is recommended when retaining the older era. Admission uses version membership: `0.54.0`, `*`, and other ranges containing that version qualify too. Exact-preview enrollment, coverage of other declared eras, and breaking release evidence for dropping old support still apply. Your delta resolver supplies the intended transition before a versioning PR is merged; Breakage does not infer it from an open PR.
|
|
149
144
|
|
|
150
145
|
Candidate results are conditional on publication. They can cover a new era, but cannot excuse an incompatible released version that the integration still advertises. Unreleased local edits with no release intent create no new coordination obligation. Registering a currently declared local peer means the integration must accommodate its planned releases. Registrations retained only for historical resolution do not impose this obligation after the peer declaration is removed. The repository owns publication order.
|
|
151
146
|
|
|
152
|
-
Every coordinated candidate must also accept the representative's peer versions in its own normalized `peerDependencies`. For example, a candidate declaring `minimatch: ^8` cannot be approved alongside Minimatch 9, even if the integration declares `^9` and its assertions pass. Both full checks and coverage audits reject that conflict before candidate installation with exit 2, identifying the candidate, required range, and observed version. The representative must supply required peers needed only by a coordinated candidate too; these additional identities do not require separate integration suites. Absent optional peers are allowed, but present optional peers must satisfy their ranges. Another coordinated peer contributes its
|
|
147
|
+
Every coordinated candidate must also accept the representative's peer versions in its own normalized `peerDependencies`. For example, a candidate declaring `minimatch: ^8` cannot be approved alongside Minimatch 9, even if the integration declares `^9` and its assertions pass. Both full checks and coverage audits reject that conflict before candidate installation with exit 2, identifying the candidate, required range, and observed version. The representative must supply required peers needed only by a coordinated candidate too; these additional identities do not require separate integration suites. Absent optional peers are allowed, but present optional peers must satisfy their ranges. Another coordinated peer contributes its resolved intended version; a peer on the integration itself uses the integration's current archive version.
|
|
153
148
|
|
|
154
149
|
Focused `npm test` commands remain available for exploration before advertising an upcoming version. They warn about coordinated candidates' incompatible peer ranges and still execute, while missing required peers remain setup errors. They check release intent for the selected candidate environments but do not audit every planned local release; require the full check or coverage audit as part of the release gate.
|
|
155
150
|
|
|
156
|
-
##
|
|
157
|
-
|
|
158
|
-
`npm check` compares semantic version sets with the previous release. Narrowing a supported range, adding a new required peer, or making an optional peer required produces an end-of-support finding. Equivalent range rewrites do not. Removing a peer requirement entirely relaxes consumer constraints. Preview support is compared too. Optional-peer absence testing is deferred; making an existing optional peer required is still detected as an installation-contract change.
|
|
151
|
+
## Shared release configuration
|
|
159
152
|
|
|
160
|
-
|
|
153
|
+
Use `defineConfig` to share a release resolver and policy with public checks:
|
|
161
154
|
|
|
162
155
|
```ts
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
156
|
+
import { defineConfig, semverPolicy } from "breakage"
|
|
157
|
+
import { changesetsReleaseDelta } from "breakage/changesets"
|
|
158
|
+
|
|
159
|
+
export default defineConfig({
|
|
160
|
+
testPattern: "tests/public/**/*",
|
|
161
|
+
test: "npm run test:public",
|
|
162
|
+
release: {
|
|
163
|
+
resolveDelta: changesetsReleaseDelta(),
|
|
164
|
+
policy: semverPolicy({ beforeV1: "minor" }),
|
|
165
|
+
},
|
|
166
|
+
npm: {
|
|
167
|
+
artifact: "artifacts/integration.tgz",
|
|
168
|
+
representatives: "peer-environments/*",
|
|
169
|
+
lockfile: "package-lock.json",
|
|
170
|
+
prepareCommand: "npm ci --ignore-scripts --workspaces=false",
|
|
171
|
+
suites: {
|
|
172
|
+
"atom.io": {
|
|
173
|
+
files: ["contracts/core/**/*.ts"],
|
|
174
|
+
command: "node --test contracts/core/*.test.ts",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
baseline: { tagPattern: "refs/tags/my-integration@" },
|
|
178
|
+
},
|
|
179
|
+
})
|
|
170
180
|
```
|
|
171
181
|
|
|
172
|
-
|
|
182
|
+
`defineConfig` inherits the top-level `release` into `npm.release`; an explicit `npm.release` overrides it. Programmatic callers can pass the resulting `config.npm` to `npmCheck` or `npmTest`. Both npm and public CLI commands use the same config loader. npm paths remain relative to the config file's directory, or to `npm.packageDirectory` resolved from it.
|
|
183
|
+
|
|
184
|
+
`npm.release.resolveDelta` replaces `releaseIntent` and returns `{ previousVersion, version, delta }` or `undefined`. Resolution is cached per package and baseline context for each npm invocation. Support checks supply `baseline: { version }` from the integration's historical manifest; Breakage rejects a transition whose `previousVersion` disagrees with that baseline. Local-peer candidate checks have no historical baseline and use the transition's target version for candidate identity and coordination obligations. `changesetsReleaseDelta()` is a factory that returns the resolver, comparing the supplied baseline (or current manifest) with the planned version before Changesets versioning and the manifest version afterward. Because the manifest fallback also supplies a target for an unchanged registered local peer, using it opts those peers into candidate coverage too. A custom resolver can return `undefined` for packages that should have no local release intent. See the [release configuration guide](README.md#typescript-configuration-and-release-intent) for optional Changesets dependencies and standalone helpers.
|
|
185
|
+
|
|
186
|
+
`npm.release.policy` replaces `certifyBreaking`. The default requires a major increment; `semverPolicy({ beforeV1: "minor" })` permits pre-1.0 minor increments too. Missing evidence or a compatible version delta leaves a support drop unapproved. Invalid evidence and resolver/policy errors fail setup. Compatibility failures against still-declared peers remain failures even when the integration has a breaking release. The old `releaseIntent` and `certifyBreaking` hooks are removed and fail with migration guidance. Policies must be functions and receive `{ previousVersion, version, delta }`.
|
|
187
|
+
|
|
188
|
+
`prepareCommand`, `installCommand`, and each suite's `command` are nonempty shell strings. Preparation runs in the representative directory; installation and suites run in its disposable `test/` directory. Breakage captures stdout and stderr for reporting. All nonzero preparation/installation results fail setup; suite exit 1 denotes incompatibility and other nonzero exits denote setup failure. Functions are reserved for the release-delta resolver and policy.
|
|
189
|
+
|
|
190
|
+
## Removing support
|
|
191
|
+
|
|
192
|
+
`npm check` compares semantic version sets with the previous release. Narrowing a supported range, adding a new required peer, or making an optional peer required produces an end-of-support finding. Equivalent range rewrites do not. Removing a peer requirement entirely relaxes consumer constraints. Preview support is compared too. Optional-peer absence testing is deferred; making an existing optional peer required is still detected as an installation-contract change.
|
|
193
|
+
|
|
194
|
+
The shared `release` model accepts intentional support changes when the integration has a breaking version delta against its previous release. Reports use support status `breaking-release` and include `{ previousVersion, version, delta, breaking }` in `support.release`. Missing intent or a compatible delta leaves support changes unapproved. Resolver or policy exceptions fail setup. Public checks use the same release model; compatibility failures against still-declared peers always remain failures.
|
|
173
195
|
|
|
174
196
|
## Installation, isolation, and recovery
|
|
175
197
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
"email": "hello@jeremybanka.com"
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/breakage.d.ts",
|
|
21
|
+
"default": "./dist/breakage.js"
|
|
22
|
+
},
|
|
23
|
+
"./changesets": {
|
|
24
|
+
"types": "./dist/changesets.d.ts",
|
|
25
|
+
"default": "./dist/changesets.js"
|
|
26
|
+
},
|
|
27
|
+
"./*": "./*"
|
|
28
|
+
},
|
|
18
29
|
"main": "dist/breakage.js",
|
|
19
30
|
"types": "dist/breakage.d.ts",
|
|
20
31
|
"bin": {
|
|
@@ -26,26 +37,54 @@
|
|
|
26
37
|
"src"
|
|
27
38
|
],
|
|
28
39
|
"dependencies": {
|
|
29
|
-
"arktype": "2.2.
|
|
30
|
-
"comline": "0.9.
|
|
40
|
+
"arktype": "2.2.9",
|
|
41
|
+
"comline": "0.9.3",
|
|
31
42
|
"minimatch": "10.2.6",
|
|
32
43
|
"semver": "7.8.5",
|
|
33
44
|
"simple-git": "4.0.2",
|
|
34
45
|
"takua": "0.2.3"
|
|
35
46
|
},
|
|
36
47
|
"devDependencies": {
|
|
37
|
-
"@
|
|
48
|
+
"@changesets/assemble-release-plan": "7.0.0",
|
|
49
|
+
"@changesets/config": "4.0.1",
|
|
50
|
+
"@changesets/pre": "3.0.0",
|
|
51
|
+
"@changesets/read": "1.0.1",
|
|
52
|
+
"@manypkg/get-packages": "3.1.0",
|
|
53
|
+
"@types/bun": "npm:bun-types@1.4.4",
|
|
38
54
|
"@types/semver": "7.8.0",
|
|
39
55
|
"typescript": "7.0.2"
|
|
40
56
|
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@changesets/assemble-release-plan": "^7.0.0",
|
|
59
|
+
"@changesets/config": "^4.0.1",
|
|
60
|
+
"@changesets/pre": "^3.0.0",
|
|
61
|
+
"@changesets/read": "^1.0.1",
|
|
62
|
+
"@manypkg/get-packages": "^3.1.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@changesets/assemble-release-plan": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"@changesets/config": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@changesets/pre": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@changesets/read": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"@manypkg/get-packages": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
41
81
|
"publishConfig": {
|
|
42
82
|
"access": "public"
|
|
43
83
|
},
|
|
44
84
|
"scripts": {
|
|
45
|
-
"build": "vp pack
|
|
85
|
+
"build": "vp pack",
|
|
46
86
|
"check": "vp run -w check",
|
|
47
87
|
"postversion": "dprint fmt package.json",
|
|
48
|
-
"schema": "bun src/breakage.x.ts --outdir=dist -- schema",
|
|
49
88
|
"test": "vp run test:run",
|
|
50
89
|
"test:public": "BREAKAGE_CLI_ENTRYPOINT=./src/breakage.x.ts bun test -- ./tests/public",
|
|
51
90
|
"test:watch": "vp run test:run --watch"
|
package/src/breakage.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { exec } from "node:child_process"
|
|
2
1
|
import path from "node:path"
|
|
3
2
|
|
|
4
3
|
import { minimatch } from "minimatch"
|
|
@@ -6,11 +5,40 @@ import { simpleGit } from "simple-git"
|
|
|
6
5
|
import type { Chronicle } from "takua"
|
|
7
6
|
import logger from "takua"
|
|
8
7
|
|
|
8
|
+
import { command, errorMessage } from "./task.ts"
|
|
9
|
+
import { readManifest, parseManifest } from "./npm/manifest.ts"
|
|
10
|
+
import {
|
|
11
|
+
resolveReleaseDelta,
|
|
12
|
+
versionDelta,
|
|
13
|
+
validateRelease,
|
|
14
|
+
rejectCertificationOptions,
|
|
15
|
+
} from "./release.ts"
|
|
16
|
+
import type { ReleaseConfig, VersionDelta } from "./release.ts"
|
|
9
17
|
import { readBaseline } from "./baseline.ts"
|
|
10
18
|
import { withDirectoryLock } from "./directory-lock.ts"
|
|
11
19
|
import { latestReleaseTag as selectLatestReleaseTag } from "./release-tag.ts"
|
|
12
20
|
import { withTestFileState } from "./test-file-state.ts"
|
|
13
21
|
|
|
22
|
+
export { defineConfig, loadConfig } from "./config.ts"
|
|
23
|
+
export type { BreakageConfig } from "./config.ts"
|
|
24
|
+
export {
|
|
25
|
+
semverPolicy,
|
|
26
|
+
versionTransition,
|
|
27
|
+
versionDelta,
|
|
28
|
+
resolveReleaseDelta,
|
|
29
|
+
} from "./release.ts"
|
|
30
|
+
export type {
|
|
31
|
+
ReleaseConfig,
|
|
32
|
+
ReleasePolicy,
|
|
33
|
+
ReleasePolicyFunction,
|
|
34
|
+
SemverPolicyOptions,
|
|
35
|
+
VersionTransition,
|
|
36
|
+
ReleaseDeltaResolver,
|
|
37
|
+
ReleaseDeltaContext,
|
|
38
|
+
VersionDelta,
|
|
39
|
+
} from "./release.ts"
|
|
40
|
+
export type { CommandResult } from "./task.ts"
|
|
41
|
+
|
|
14
42
|
export type { BreakageBaseline, BreakagePreludeOptions } from "./baseline.ts"
|
|
15
43
|
export { breakagePrelude } from "./baseline.ts"
|
|
16
44
|
export { formatNpmReport, npmCheck, npmTest } from "./npm/check.ts"
|
|
@@ -22,9 +50,6 @@ export type {
|
|
|
22
50
|
NpmSuite,
|
|
23
51
|
NpmSuiteResult,
|
|
24
52
|
NpmTestSelection,
|
|
25
|
-
ReleaseContext,
|
|
26
|
-
ReleaseEvidence,
|
|
27
|
-
ReleaseIntent,
|
|
28
53
|
SupportChange,
|
|
29
54
|
} from "./npm/types.ts"
|
|
30
55
|
|
|
@@ -32,14 +57,17 @@ export type BreakageOptions = {
|
|
|
32
57
|
tagPattern?: string | undefined
|
|
33
58
|
baselineFile?: string | undefined
|
|
34
59
|
testPattern: string
|
|
35
|
-
testCommand
|
|
36
|
-
|
|
60
|
+
testCommand?: string
|
|
61
|
+
/** Shell command. Do not combine with testCommand. */
|
|
62
|
+
test?: string
|
|
63
|
+
release?: ReleaseConfig
|
|
37
64
|
baseDirname?: string
|
|
38
65
|
verbose?: boolean | undefined
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
export type BreakageOutcome =
|
|
42
69
|
| { gitWasClean: false }
|
|
70
|
+
| { gitWasClean: true; setupError: string }
|
|
43
71
|
| {
|
|
44
72
|
gitWasClean: true
|
|
45
73
|
lastReleaseFound: false
|
|
@@ -57,6 +85,7 @@ export type BreakageOutcome =
|
|
|
57
85
|
testsWereFound: true
|
|
58
86
|
testsFound: string[]
|
|
59
87
|
breakingChangesFound: false
|
|
88
|
+
release?: VersionDelta
|
|
60
89
|
}
|
|
61
90
|
| {
|
|
62
91
|
gitWasClean: true
|
|
@@ -66,20 +95,35 @@ export type BreakageOutcome =
|
|
|
66
95
|
testsFound: string[]
|
|
67
96
|
breakingChangesFound: true
|
|
68
97
|
testResult: string
|
|
69
|
-
|
|
70
|
-
certificationStdout: string
|
|
71
|
-
certificationStderr: string
|
|
98
|
+
release?: VersionDelta
|
|
72
99
|
}
|
|
73
100
|
|
|
74
|
-
export async function breakage(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
export async function breakage(
|
|
102
|
+
options: BreakageOptions,
|
|
103
|
+
): Promise<BreakageOutcome & { summary: string }> {
|
|
104
|
+
rejectCertificationOptions(options)
|
|
105
|
+
const {
|
|
106
|
+
tagPattern,
|
|
107
|
+
baselineFile,
|
|
108
|
+
testPattern,
|
|
109
|
+
testCommand,
|
|
110
|
+
test,
|
|
111
|
+
release,
|
|
112
|
+
baseDirname = process.cwd(),
|
|
113
|
+
verbose = false,
|
|
114
|
+
} = options
|
|
115
|
+
if (!testPattern || (!test && !testCommand))
|
|
116
|
+
throw new Error(
|
|
117
|
+
`required arguments were not provided: testPattern and test or testCommand.`,
|
|
118
|
+
)
|
|
119
|
+
if (test !== undefined && testCommand !== undefined)
|
|
120
|
+
throw new Error(`Choose test or testCommand, not both.`)
|
|
121
|
+
if (
|
|
122
|
+
typeof (test ?? testCommand) !== `string` ||
|
|
123
|
+
!(test ?? testCommand)!.trim()
|
|
124
|
+
)
|
|
125
|
+
throw new Error(`test must be a nonempty shell command string.`)
|
|
126
|
+
if (release !== undefined) validateRelease(release)
|
|
83
127
|
let mark: Chronicle[`mark`] | undefined
|
|
84
128
|
let logMarks: Chronicle[`logMarks`] | undefined
|
|
85
129
|
if (verbose) {
|
|
@@ -181,6 +225,34 @@ export async function breakage({
|
|
|
181
225
|
)
|
|
182
226
|
mark?.(`filtered to public test files`)
|
|
183
227
|
|
|
228
|
+
let releaseEvidence: VersionDelta | undefined
|
|
229
|
+
if (release) {
|
|
230
|
+
const current = await readManifest(path.join(baseDirname, `package.json`))
|
|
231
|
+
const prefix = await git.revparse([`--show-prefix`])
|
|
232
|
+
const previous = parseManifest(
|
|
233
|
+
JSON.parse(await git.show([`${releaseCommit}:${prefix}package.json`])),
|
|
234
|
+
`baseline`,
|
|
235
|
+
)
|
|
236
|
+
if (previous.name !== current.name)
|
|
237
|
+
throw new Error(`Release baseline belongs to another package.`)
|
|
238
|
+
const transition = await resolveReleaseDelta(release, {
|
|
239
|
+
packageName: current.name,
|
|
240
|
+
packageDirectory: path.resolve(baseDirname),
|
|
241
|
+
currentVersion: current.version,
|
|
242
|
+
baseline: {
|
|
243
|
+
version: previous.version,
|
|
244
|
+
ref: latestReleaseTag,
|
|
245
|
+
commit: releaseCommit,
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
if (transition !== undefined)
|
|
249
|
+
releaseEvidence = versionDelta(
|
|
250
|
+
transition.previousVersion,
|
|
251
|
+
transition.version,
|
|
252
|
+
release.policy,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
184
256
|
const prepared = await withTestFileState(git, baseDirname, async (state) => {
|
|
185
257
|
if (!(await state.isClean())) {
|
|
186
258
|
return {
|
|
@@ -206,61 +278,41 @@ export async function breakage({
|
|
|
206
278
|
const { restore } = prepared
|
|
207
279
|
|
|
208
280
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
mark?.(`${wereCertified ? `passed` : `failed`} certification`)
|
|
245
|
-
resolve({
|
|
246
|
-
summary: wereCertified
|
|
247
|
-
? `Breaking changes were found and certified.`
|
|
248
|
-
: `Breaking changes were found, but not certified.`,
|
|
249
|
-
gitWasClean: true,
|
|
250
|
-
lastReleaseFound: true,
|
|
251
|
-
lastReleaseTag: latestReleaseTag,
|
|
252
|
-
testsWereFound: true,
|
|
253
|
-
testsFound: productionTestFiles,
|
|
254
|
-
breakingChangesFound: true,
|
|
255
|
-
testResult: thrown as string,
|
|
256
|
-
breakingChangesCertified: wereCertified,
|
|
257
|
-
certificationStdout: stdout,
|
|
258
|
-
certificationStderr: stderr,
|
|
259
|
-
})
|
|
260
|
-
},
|
|
261
|
-
)
|
|
262
|
-
})
|
|
263
|
-
return breakingChangesCertified
|
|
281
|
+
const result = await command(test ?? testCommand!, baseDirname)
|
|
282
|
+
if (result.code > 1)
|
|
283
|
+
return {
|
|
284
|
+
gitWasClean: true,
|
|
285
|
+
setupError: result.output,
|
|
286
|
+
summary: `Public test setup failed (${result.code}).`,
|
|
287
|
+
}
|
|
288
|
+
const common = {
|
|
289
|
+
gitWasClean: true as const,
|
|
290
|
+
lastReleaseFound: true as const,
|
|
291
|
+
lastReleaseTag: latestReleaseTag,
|
|
292
|
+
testsWereFound: true as const,
|
|
293
|
+
testsFound: productionTestFiles,
|
|
294
|
+
...(releaseEvidence ? { release: releaseEvidence } : {}),
|
|
295
|
+
}
|
|
296
|
+
if (result.code === 0)
|
|
297
|
+
return {
|
|
298
|
+
...common,
|
|
299
|
+
breakingChangesFound: false,
|
|
300
|
+
summary: `No breaking changes were detected.`,
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
...common,
|
|
304
|
+
breakingChangesFound: true,
|
|
305
|
+
testResult: result.output,
|
|
306
|
+
summary: releaseEvidence?.breaking
|
|
307
|
+
? `Breaking changes are covered by release ${releaseEvidence.previousVersion} → ${releaseEvidence.version}.`
|
|
308
|
+
: `Breaking changes require a breaking release.`,
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
gitWasClean: true,
|
|
313
|
+
setupError: errorMessage(error),
|
|
314
|
+
summary: `Public test setup failed: ${errorMessage(error)}`,
|
|
315
|
+
}
|
|
264
316
|
} finally {
|
|
265
317
|
await restore()
|
|
266
318
|
mark?.(`restored original public tests`)
|