breakage 0.8.4 → 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 +164 -46
- package/dist/breakage-DIY49fzP.js +1349 -0
- package/dist/breakage-DIY49fzP.js.map +1 -0
- package/dist/breakage.d.ts +179 -11
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +3 -1
- package/dist/breakage.x.js +169 -67
- 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 +236 -0
- package/package.json +51 -11
- package/src/breakage.ts +136 -70
- package/src/breakage.x.ts +160 -112
- package/src/changesets.ts +51 -0
- package/src/config.ts +128 -0
- package/src/npm/check.ts +449 -0
- package/src/npm/cli.ts +130 -0
- package/src/npm/config.ts +118 -0
- package/src/npm/environment.ts +661 -0
- package/src/npm/manifest.ts +134 -0
- package/src/npm/release.ts +173 -0
- package/src/npm/types.ts +64 -0
- package/src/release.ts +153 -0
- package/src/task.ts +54 -0
- package/dist/breakage-Dc4IldeW.js +0 -435
- package/dist/breakage-Dc4IldeW.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.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
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# npm peer compatibility
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
9
|
+
## Declare support deliberately
|
|
10
|
+
|
|
11
|
+
Current candidates must declare explicit peer support eras, for example `"atom.io": "^0.53.3 || ^0.54.0"`. Moving workspace shorthand (`workspace:`, `workspace:*`, `workspace:^`, and `workspace:~`) is rejected, including in coordinated peer candidates. With `workspace:^`, advancing the local core from `0.53.3` to `0.54.0` silently changes the published support range and drops the previous era. Keep that decision explicit and test the versions you promise to support.
|
|
12
|
+
|
|
13
|
+
A fixed declaration such as `workspace:^0.53.3` is accepted and compared as `^0.53.3`, matching its packed form. Ordinary semver ranges and optional peers retain their meaning. Workspace aliases and relative paths are unsupported. Current source manifests are never rewritten by this normalization.
|
|
14
|
+
|
|
15
|
+
Older releases may have used moving shorthand. For the baseline comparison, register that peer's directory in `localPeers`; Breakage reads its version from the same historical commit as the integration manifest. For example, historical `workspace:^` becomes `^0.53.3` when that commit's peer version is `0.53.3`, even if today's checkout or release plan says `0.54.0`. Missing historical evidence is a setup error. The configured directory must exist at the baseline commit; automatic tracking across package moves is unsupported.
|
|
16
|
+
|
|
17
|
+
After removing a peer requirement, you may retain its historical registration even if its directory has been deleted from the current checkout. Breakage still checks existing registered directories for workspace-source leaks. A broken directory link remains an error.
|
|
18
|
+
|
|
19
|
+
## Start with released peers
|
|
20
|
+
|
|
21
|
+
In the integration package, create complete environments for the peer formulations you want to exercise. For example, `peer-environments/core-1/package.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"name": "core-1-contracts",
|
|
26
|
+
"version": "1.0.0",
|
|
27
|
+
"private": true,
|
|
28
|
+
"type": "module",
|
|
29
|
+
"dependencies": { "atom.io": "1.12.0", "react": "19.0.0" }
|
|
30
|
+
}
|
|
31
|
+
```
|
|
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 resolved intended versions supply the exact identities, and any accompanying dependency pins must match those versions.
|
|
34
|
+
|
|
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
|
+
|
|
37
|
+
Ignore the disposable execution directories in your repository:
|
|
38
|
+
|
|
39
|
+
```gitignore
|
|
40
|
+
peer-environments/*/test/
|
|
41
|
+
node_modules/
|
|
42
|
+
artifacts/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Build and pack the integration before checking. `npm pack --ignore-scripts --pack-destination artifacts` creates an archive with a versioned filename; configure `artifact` to that file or have your build copy it to a stable path. Breakage checks the installed archive's name, version, and peer declarations against the current manifest. Repository preparation owns making the archive current, including generated files and runtime version constants.
|
|
46
|
+
|
|
47
|
+
Write current contracts that import the package and its peers by consumer package names. Keep fixtures and explicit runner configuration under the package directory. The command runs in `peer-environments/<name>/test/`, with those selected paths copied intact. Avoid source aliases, workspace-relative imports, and runner configurations that walk back to workspace source. A Node test runner contract might look like:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { test } from "node:test"
|
|
51
|
+
import { strict as assert } from "node:assert"
|
|
52
|
+
import { createIntegration } from "my-integration"
|
|
53
|
+
import { createStore } from "atom.io"
|
|
54
|
+
|
|
55
|
+
test("the integration uses a consumer's store", () => {
|
|
56
|
+
const store = createStore()
|
|
57
|
+
const integration = createIntegration(store)
|
|
58
|
+
assert.equal(integration.store, store)
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use your actual public API and meaningful assertions. A suite must fail when no tests execute. Exit 1 denotes assertions failing; other nonzero exits denote setup failure. If your runner reports infrastructure errors as 1 too, wrap it to distinguish setup from assertions. Breakage separately classifies failures in preparation, artifact installation, coverage, and resolution as setup failures.
|
|
63
|
+
|
|
64
|
+
Create `breakage.config.ts`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import type { NpmConfig } from "breakage"
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
npm: {
|
|
71
|
+
artifact: "artifacts/integration.tgz",
|
|
72
|
+
representatives: "peer-environments/*",
|
|
73
|
+
lockfile: "package-lock.json",
|
|
74
|
+
prepareCommand: "npm ci --ignore-scripts --workspaces=false",
|
|
75
|
+
suites: {
|
|
76
|
+
"atom.io": {
|
|
77
|
+
files: ["contracts/core/**/*.ts", "fixtures/**/*"],
|
|
78
|
+
command: "node --test contracts/core/*.test.ts",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
localPeers: {
|
|
82
|
+
"atom.io": {
|
|
83
|
+
directory: "../atom.io",
|
|
84
|
+
artifact: "../atom.io/artifacts/core.tgz",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
baseline: { tagPattern: "refs/tags/my-integration@" },
|
|
88
|
+
} satisfies NpmConfig,
|
|
89
|
+
}
|
|
90
|
+
```
|
|
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 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
|
+
|
|
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
|
+
|
|
96
|
+
Run the package check or a focused test:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
breakage npm check
|
|
100
|
+
breakage npm check --coverage-only
|
|
101
|
+
breakage npm test atom.io 1
|
|
102
|
+
breakage npm test --representative peer-environments/core-1
|
|
103
|
+
breakage npm check --config ./breakage.config.ts --json
|
|
104
|
+
```
|
|
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 unapproved support failures, and 2 for setup or coverage errors.
|
|
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 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
|
+
|
|
110
|
+
## Coordinated releases
|
|
111
|
+
|
|
112
|
+
Create a separate representative that explicitly names candidate peers. Leave an unpublished peer out of the frozen dependency list:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"name": "core-next-contracts",
|
|
117
|
+
"version": "1.0.0",
|
|
118
|
+
"private": true,
|
|
119
|
+
"type": "module",
|
|
120
|
+
"dependencies": { "react": "19.0.0" },
|
|
121
|
+
"breakage": { "candidates": ["atom.io"] }
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Set `release.resolveDelta` to a synchronous/asynchronous resolver receiving `packageName`, `packageDirectory`, and `currentVersion`. For Changesets, use the optional helper:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { changesetsReleaseDelta } from "breakage/changesets"
|
|
129
|
+
import { semverPolicy } from "breakage"
|
|
130
|
+
|
|
131
|
+
const release = {
|
|
132
|
+
resolveDelta: changesetsReleaseDelta(),
|
|
133
|
+
policy: semverPolicy({ beforeV1: "minor" }),
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
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.
|
|
138
|
+
|
|
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.
|
|
140
|
+
|
|
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.
|
|
142
|
+
|
|
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.
|
|
144
|
+
|
|
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.
|
|
146
|
+
|
|
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.
|
|
148
|
+
|
|
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.
|
|
150
|
+
|
|
151
|
+
## Shared release configuration
|
|
152
|
+
|
|
153
|
+
Use `defineConfig` to share a release resolver and policy with public checks:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
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
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
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.
|
|
195
|
+
|
|
196
|
+
## Installation, isolation, and recovery
|
|
197
|
+
|
|
198
|
+
`prepareCommand` runs in each representative and must install from its frozen lock file. For another package manager, set `lockfile` and this command accordingly, keeping the environment independent of workspace package substitution. Breakage requires unchanged manifest/lock bytes and checks direct installed identities. An existing installation that changes peer versions during frozen preparation is reported as stale; rerun after reviewing the updated installation. The configured package manager establishes lock consistency; Breakage does not reimplement its lock format.
|
|
199
|
+
|
|
200
|
+
Installed peers may come from `dependencies`, `devDependencies`, or `optionalDependencies`. An optional dependency overrides a required dependency of the same name. When present, it supplies peer evidence and runs its selected relationship suite normally. When genuinely absent, it supplies no evidence: a required peer still fails, while an optional peer may be omitted. Broken installed links and missing manifests remain errors. Optional-peer absence coverage is not required.
|
|
201
|
+
|
|
202
|
+
A representative must not declare conflicting development and runtime requirements for the same dependency. For example, `dependencies.host: "1.0.0"` together with `devDependencies.host: "2.0.0"` is a setup error before installation. Remove the duplicate or give both declarations the same specifier. The same rule applies when the runtime declaration is optional.
|
|
203
|
+
|
|
204
|
+
Runtime dependencies must resolve through the representative or its test installation. Explicit package-store links remain supported, including dependencies alongside the linked package in its nearest `node_modules` namespace. Finding a dependency only in an unrelated ancestor installation is a setup error, even if it is optional and would make the assertions pass. Breakage checks this before and after execution.
|
|
205
|
+
|
|
206
|
+
The ignored `test/` directory is also the ownership lock. An existing directory is rejected without deleting it. Independent environments execute concurrently, while two runs cannot own the same environment. On normal completion or failure, Breakage removes only the stage it created. After a killed process, confirm its commands have stopped, inspect the remaining stage, and remove it before retrying.
|
|
207
|
+
|
|
208
|
+
The default candidate install runs npm with `--prefix . --workspaces=false --ignore-scripts --legacy-peer-deps --package-lock=false --no-audit --no-fund`. It installs the archive's runtime dependencies without automatically installing peers. `installCommand` may replace this with a repository-specific equivalent operating on the generated consumer manifest. It must leave the candidates physically inside the stage. A native dependency that needs a lifecycle build requires an intentional command override.
|
|
209
|
+
|
|
210
|
+
Tests, candidate packages, and their runtime dependencies must resolve the same physical configured peer instances. Breakage rejects shadow copies and workspace-source peer links, including links to configured local packages. pnpm-style store links into dependency installations are permitted. These checks establish installed package identity, not registry integrity attestation. Package-manager layout, runner aliases, and module loaders remain relevant: preserve consumer resolution and test your actual runner setup.
|
|
211
|
+
|
|
212
|
+
## CI and caching
|
|
213
|
+
|
|
214
|
+
Keep the existing public check as `test:breakage` and add the npm check to that release gate, or use `test:breakage:npm` for a separate required job. Preparation must build and pack every participating candidate. A developer-owned matrix can run one configured representative per job, plus an audit job:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
jobs:
|
|
218
|
+
npm-audit:
|
|
219
|
+
steps:
|
|
220
|
+
# Checkout, install tools/dependencies, and prepare release evidence first.
|
|
221
|
+
- run: breakage npm check --coverage-only
|
|
222
|
+
npm-contracts:
|
|
223
|
+
strategy:
|
|
224
|
+
fail-fast: false
|
|
225
|
+
matrix:
|
|
226
|
+
representative: [peer-environments/core-1, peer-environments/core-2]
|
|
227
|
+
steps:
|
|
228
|
+
# Checkout, install tools/dependencies, and build/pack candidates first.
|
|
229
|
+
- run: breakage npm test --representative "${{ matrix.representative }}"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
This is a job excerpt: add your runner, checkout, setup, and working directory. Require the audit, all matrix jobs, and the public check before release. Updating the authored representative list requires updating the required matrix. Breakage neither generates workflows nor aggregates cross-job receipts.
|
|
233
|
+
|
|
234
|
+
Start uncached. A correct cache key includes candidate/peer archives, suite files, representative manifests and lock files, observed installation identities, configuration and imported helpers, baseline, release-plan inputs, and command/environment/toolchain inputs. Unknown dynamic input means the check stays uncached. There is no new npm prelude or mandatory execution plan.
|
|
235
|
+
|
|
236
|
+
Programmatic consumers can import `npmCheck`, `npmTest`, `formatNpmReport`, and their exported types from `breakage`. Both check functions return a structured `NpmReport` with `exitCode`, coverage, support findings, and independent representative/suite results; `mode` distinguishes audit from execution.
|
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,38 +15,78 @@
|
|
|
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": {
|
|
21
32
|
"breakage": "./dist/breakage.x.js"
|
|
22
33
|
},
|
|
23
34
|
"files": [
|
|
35
|
+
"npm.md",
|
|
24
36
|
"dist",
|
|
25
37
|
"src"
|
|
26
38
|
],
|
|
27
39
|
"dependencies": {
|
|
28
|
-
"arktype": "2.2.
|
|
29
|
-
"comline": "0.9.
|
|
40
|
+
"arktype": "2.2.9",
|
|
41
|
+
"comline": "0.9.3",
|
|
30
42
|
"minimatch": "10.2.6",
|
|
43
|
+
"semver": "7.8.5",
|
|
31
44
|
"simple-git": "4.0.2",
|
|
32
45
|
"takua": "0.2.3"
|
|
33
46
|
},
|
|
34
47
|
"devDependencies": {
|
|
35
|
-
"@
|
|
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",
|
|
54
|
+
"@types/semver": "7.8.0",
|
|
36
55
|
"typescript": "7.0.2"
|
|
37
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
|
+
},
|
|
38
81
|
"publishConfig": {
|
|
39
82
|
"access": "public"
|
|
40
83
|
},
|
|
41
84
|
"scripts": {
|
|
42
|
-
"build": "vp pack
|
|
43
|
-
"check": "
|
|
44
|
-
"check:tsc": "tsc --noEmit",
|
|
85
|
+
"build": "vp pack",
|
|
86
|
+
"check": "vp run -w check",
|
|
45
87
|
"postversion": "dprint fmt package.json",
|
|
46
|
-
"
|
|
47
|
-
"pretest": "pnpm run build",
|
|
48
|
-
"test": "BREAKAGE_CLI_ENTRYPOINT=./src/breakage.x.ts bun test -- ./__tests__ ./tests/public",
|
|
88
|
+
"test": "vp run test:run",
|
|
49
89
|
"test:public": "BREAKAGE_CLI_ENTRYPOINT=./src/breakage.x.ts bun test -- ./tests/public",
|
|
50
|
-
"test:watch": "
|
|
90
|
+
"test:watch": "vp run test:run --watch"
|
|
51
91
|
}
|
|
52
92
|
}
|
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,26 +5,69 @@ 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"
|
|
44
|
+
export { formatNpmReport, npmCheck, npmTest } from "./npm/check.ts"
|
|
45
|
+
export type {
|
|
46
|
+
NpmConfig,
|
|
47
|
+
NpmPeerEvidence,
|
|
48
|
+
NpmReport,
|
|
49
|
+
NpmRepresentativeResult,
|
|
50
|
+
NpmSuite,
|
|
51
|
+
NpmSuiteResult,
|
|
52
|
+
NpmTestSelection,
|
|
53
|
+
SupportChange,
|
|
54
|
+
} from "./npm/types.ts"
|
|
16
55
|
|
|
17
56
|
export type BreakageOptions = {
|
|
18
57
|
tagPattern?: string | undefined
|
|
19
58
|
baselineFile?: string | undefined
|
|
20
59
|
testPattern: string
|
|
21
|
-
testCommand
|
|
22
|
-
|
|
60
|
+
testCommand?: string
|
|
61
|
+
/** Shell command. Do not combine with testCommand. */
|
|
62
|
+
test?: string
|
|
63
|
+
release?: ReleaseConfig
|
|
23
64
|
baseDirname?: string
|
|
24
65
|
verbose?: boolean | undefined
|
|
25
66
|
}
|
|
26
67
|
|
|
27
68
|
export type BreakageOutcome =
|
|
28
69
|
| { gitWasClean: false }
|
|
70
|
+
| { gitWasClean: true; setupError: string }
|
|
29
71
|
| {
|
|
30
72
|
gitWasClean: true
|
|
31
73
|
lastReleaseFound: false
|
|
@@ -43,6 +85,7 @@ export type BreakageOutcome =
|
|
|
43
85
|
testsWereFound: true
|
|
44
86
|
testsFound: string[]
|
|
45
87
|
breakingChangesFound: false
|
|
88
|
+
release?: VersionDelta
|
|
46
89
|
}
|
|
47
90
|
| {
|
|
48
91
|
gitWasClean: true
|
|
@@ -52,20 +95,35 @@ export type BreakageOutcome =
|
|
|
52
95
|
testsFound: string[]
|
|
53
96
|
breakingChangesFound: true
|
|
54
97
|
testResult: string
|
|
55
|
-
|
|
56
|
-
certificationStdout: string
|
|
57
|
-
certificationStderr: string
|
|
98
|
+
release?: VersionDelta
|
|
58
99
|
}
|
|
59
100
|
|
|
60
|
-
export async function breakage(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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)
|
|
69
127
|
let mark: Chronicle[`mark`] | undefined
|
|
70
128
|
let logMarks: Chronicle[`logMarks`] | undefined
|
|
71
129
|
if (verbose) {
|
|
@@ -167,6 +225,34 @@ export async function breakage({
|
|
|
167
225
|
)
|
|
168
226
|
mark?.(`filtered to public test files`)
|
|
169
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
|
+
|
|
170
256
|
const prepared = await withTestFileState(git, baseDirname, async (state) => {
|
|
171
257
|
if (!(await state.isClean())) {
|
|
172
258
|
return {
|
|
@@ -192,61 +278,41 @@ export async function breakage({
|
|
|
192
278
|
const { restore } = prepared
|
|
193
279
|
|
|
194
280
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
mark?.(`${wereCertified ? `passed` : `failed`} certification`)
|
|
231
|
-
resolve({
|
|
232
|
-
summary: wereCertified
|
|
233
|
-
? `Breaking changes were found and certified.`
|
|
234
|
-
: `Breaking changes were found, but not certified.`,
|
|
235
|
-
gitWasClean: true,
|
|
236
|
-
lastReleaseFound: true,
|
|
237
|
-
lastReleaseTag: latestReleaseTag,
|
|
238
|
-
testsWereFound: true,
|
|
239
|
-
testsFound: productionTestFiles,
|
|
240
|
-
breakingChangesFound: true,
|
|
241
|
-
testResult: thrown as string,
|
|
242
|
-
breakingChangesCertified: wereCertified,
|
|
243
|
-
certificationStdout: stdout,
|
|
244
|
-
certificationStderr: stderr,
|
|
245
|
-
})
|
|
246
|
-
},
|
|
247
|
-
)
|
|
248
|
-
})
|
|
249
|
-
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
|
+
}
|
|
250
316
|
} finally {
|
|
251
317
|
await restore()
|
|
252
318
|
mark?.(`restored original public tests`)
|