breakage 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{breakage-DIY49fzP.js → breakage-DJiIiLzL.js} +632 -9
- package/dist/breakage-DJiIiLzL.js.map +1 -0
- package/dist/breakage.d.ts +198 -86
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +2 -2
- package/dist/breakage.x.js +142 -3
- package/dist/breakage.x.js.map +1 -1
- package/npm.md +144 -12
- package/package.json +1 -1
- package/src/breakage.ts +17 -0
- package/src/breakage.x.ts +26 -0
- package/src/config.ts +3 -0
- package/src/npm/check.ts +19 -0
- package/src/npm/cli.ts +4 -0
- package/src/npm/matrix-cli.ts +170 -0
- package/src/npm/matrix-model.ts +395 -0
- package/src/npm/matrix.ts +657 -0
- package/dist/breakage-DIY49fzP.js.map +0 -1
package/npm.md
CHANGED
|
@@ -185,7 +185,7 @@ export default defineConfig({
|
|
|
185
185
|
|
|
186
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
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.
|
|
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. Release-delta resolvers and policies are functions; the optional matrix's archive preparation accepts a command or function.
|
|
189
189
|
|
|
190
190
|
## Removing support
|
|
191
191
|
|
|
@@ -211,26 +211,158 @@ Tests, candidate packages, and their runtime dependencies must resolve the same
|
|
|
211
211
|
|
|
212
212
|
## CI and caching
|
|
213
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.
|
|
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. For a local package, `npm check` still performs the complete check without a matrix plan. For a repository with several packages or distributed runners, use the optional plan → execute → verify protocol below. CI owns scheduling and artifact transport; Breakage owns discovery, audits, candidate identity, and receipt verification.
|
|
215
|
+
|
|
216
|
+
### Configure and plan a matrix
|
|
217
|
+
|
|
218
|
+
Keep each package's existing `npm` configuration, representative manifests/lock files, and contracts. Add this to the root `breakage.config.ts`:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { defineConfig } from "breakage"
|
|
222
|
+
|
|
223
|
+
export default defineConfig({
|
|
224
|
+
npmMatrix: {
|
|
225
|
+
configs: ["packages/*/breakage.config.ts"],
|
|
226
|
+
prepare: "pnpm exec turbo run pack:breakage",
|
|
227
|
+
label: ({ packageName, description, representative }) =>
|
|
228
|
+
`${packageName}: ${description ?? representative}`,
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`configs` is a root-relative glob or list of globs. Only matched configs with an `npm` section participate; a package is discovered once even when patterns overlap. Package names must be unique. Root matrix configuration does not implicitly supply release hooks to package configs: share `release` through a TypeScript module and `defineConfig` as usual. Each required peer needs a relationship suite, each declared era needs coverage, and every selected representative must have executable audited suites. Keep exploratory environments outside the matrix's configured representative globs.
|
|
234
|
+
|
|
235
|
+
`prepare` runs once after the coverage/support audits. Use your existing build/pack task to create the exact `artifact` paths in the package configs, including registered local peers. It must not alter tracked sources, manifests, lock files, contracts, or release evidence. Tool choice and dependency build order remain yours. Alternatively, provide an async function receiving `{ rootDirectory, archives }`; each deduplicated archive request contains `{ packageName, directory, artifact }`, with both paths relative to the root. Its return value is ignored, and throwing fails planning. `label` is optional and receives package name, representative path, optional manifest description, and audited peers; the default is `packageName: representative`.
|
|
236
|
+
|
|
237
|
+
Use a clean Git checkout and ignore generated archives, `.breakage/npm-plan/`, `.breakage/receipts/`, and each representative's `test/` directory. Commit the intended source/config changes before planning. All package inputs and configured archive paths must stay within the checkout. The root must be the Git checkout root; the default is the current directory. Planning requires baseline tags or pinned baseline files and the same Changesets/tool dependencies as the package checks. With `changesetsReleaseDelta()`, both pending changesets and versioned manifests supply the existing release evidence; no matrix-specific release script is needed.
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
breakage npm plan --config breakage.config.ts --out .breakage/npm-plan
|
|
241
|
+
# stdout is one JSON object: {"include":[{"id":"...","label":"...",...}]}
|
|
242
|
+
# Copy an id from that output; execute every id before verifying.
|
|
243
|
+
breakage npm execute <id> --plan .breakage/npm-plan/plan.json --receipt .breakage/receipts/<id>.json
|
|
244
|
+
breakage npm verify --plan .breakage/npm-plan/plan.json --receipts '.breakage/receipts/*.json'
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`plan` writes `plan.json` and content-addressed archives into the bundle directory, defaulting to `.breakage/npm-plan`. `--root` changes the checkout root for all three commands; config, output, plan, and receipt paths resolve from it. Quoting the receipt glob lets Breakage collect all matching files, including duplicates from different runners. Empty discovery, uncovered eras, unapproved support changes, and preparation failures are errors. A failed re-plan removes the previous `plan.json` in that output directory. After cancellation, discard the incomplete bundle and inspect/remove its `.lock` directory before retrying.
|
|
248
|
+
|
|
249
|
+
The plan has `formatVersion: 1`, a fresh run identity, source commit, configuration and input fingerprints, release evidence, coverage/support audits, expected environments/suites/peer identities, and archive SHA-256 digests. Its `id` binds the complete payload. Execution consumes the bundled archives directly; workers do not repack or restore archives over workspace outputs. Each receipt has `formatVersion: 1`, `planId`, environment `id`, `exitCode`, and a report or setup error. Environment IDs remain stable for a config/representative path, while plan IDs change on every planning run.
|
|
250
|
+
|
|
251
|
+
Execution rechecks the source, configuration, representative discovery, contract/manifests/lockfile inputs, release evidence, and archive digests before and after running the shared npm test engine with exactly the planned suites. A representative can contain a peer outside its declared support range while covering another peer; matrix execution omits that unsupported relationship's suite, matching the audit. Full peer identity and configuration validation still apply. Explicit `npm test --representative` exploration continues to run every configured suite whose peer is present. A valid plan produces a failure receipt on ordinary setup or compatibility failure. An invalid/unreadable plan or killed process may produce no receipt; that cannot satisfy verification. Assertions retain exit 1, setup failures exit 2, and successful execution exits 0. Planning and aggregate verification return 0 on success and 2 on any failure.
|
|
252
|
+
|
|
253
|
+
### Use the same protocol from TypeScript
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { readFile } from "node:fs/promises"
|
|
257
|
+
import { npmPlan, npmMatrix, npmExecute, npmVerify } from "breakage"
|
|
258
|
+
import config from "./breakage.config.ts"
|
|
259
|
+
|
|
260
|
+
const location = { rootDirectory: process.cwd() }
|
|
261
|
+
if (!config.npmMatrix) throw new Error("Configure npmMatrix first.")
|
|
262
|
+
const plan = await npmPlan(config.npmMatrix, location)
|
|
263
|
+
const receipts = []
|
|
264
|
+
for (const { id } of npmMatrix(plan).include) {
|
|
265
|
+
receipts.push(await npmExecute(plan, id, location))
|
|
266
|
+
}
|
|
267
|
+
const result = await npmVerify(plan, receipts, location)
|
|
268
|
+
process.exitCode = result.exitCode
|
|
269
|
+
|
|
270
|
+
// Another runner can load the transported bundle without depending on its old path.
|
|
271
|
+
const transportedPlan: unknown = JSON.parse(
|
|
272
|
+
await readFile(".breakage/npm-plan/plan.json", "utf8"),
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The exported types are `NpmMatrixConfig`, `NpmPrepareContext`, `NpmArchiveRequest`, `NpmMatrixLabelContext`, `NpmMatrixLocation`, `NpmPlan`, `NpmReceipt`, and `NpmVerification`. `npmVerify` validates the checkout and bundle as well as receipts. `verifyNpmReceipts(plan, receipts)` provides pure structural/audit/receipt verification for custom orchestrators that independently validate source/config/archive identity. It does not inspect the filesystem. Receipt order and runner-local installed paths do not affect either verifier.
|
|
277
|
+
|
|
278
|
+
### Distribute the matrix in CI
|
|
279
|
+
|
|
280
|
+
This GitHub Actions example assumes your existing `./.github/actions/setup` installs pinned Node/package-manager versions and frozen workspace dependencies. Keep the plan bundle and receipts specific to the current workflow run; fetch release history in every checkout. The verify job must require successful planning and execution jobs as well as complete receipts, so cancellation or skipped jobs cannot reuse old success. Keep the ordinary public check required separately.
|
|
215
281
|
|
|
216
282
|
```yaml
|
|
283
|
+
name: Peer compatibility
|
|
284
|
+
on: [pull_request]
|
|
285
|
+
permissions:
|
|
286
|
+
contents: read
|
|
217
287
|
jobs:
|
|
218
|
-
|
|
288
|
+
plan:
|
|
289
|
+
runs-on: ubuntu-latest
|
|
290
|
+
outputs:
|
|
291
|
+
matrix: ${{ steps.plan.outputs.matrix }}
|
|
219
292
|
steps:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
293
|
+
- uses: actions/checkout@v4
|
|
294
|
+
with:
|
|
295
|
+
fetch-depth: 0
|
|
296
|
+
- uses: ./.github/actions/setup
|
|
297
|
+
- id: plan
|
|
298
|
+
run: |
|
|
299
|
+
matrix=$(pnpm exec breakage npm plan)
|
|
300
|
+
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
|
301
|
+
- uses: actions/upload-artifact@v4
|
|
302
|
+
with:
|
|
303
|
+
name: peer-plan
|
|
304
|
+
path: .breakage/npm-plan/
|
|
305
|
+
include-hidden-files: true
|
|
306
|
+
if-no-files-found: error
|
|
307
|
+
execute:
|
|
308
|
+
needs: plan
|
|
309
|
+
runs-on: ubuntu-latest
|
|
310
|
+
name: ${{ matrix.label }}
|
|
223
311
|
strategy:
|
|
224
312
|
fail-fast: false
|
|
225
|
-
matrix:
|
|
226
|
-
|
|
313
|
+
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
|
314
|
+
steps:
|
|
315
|
+
- uses: actions/checkout@v4
|
|
316
|
+
with:
|
|
317
|
+
fetch-depth: 0
|
|
318
|
+
- uses: ./.github/actions/setup
|
|
319
|
+
- uses: actions/download-artifact@v4
|
|
320
|
+
with:
|
|
321
|
+
name: peer-plan
|
|
322
|
+
path: .breakage/npm-plan/
|
|
323
|
+
- env:
|
|
324
|
+
ENVIRONMENT_ID: ${{ matrix.id }}
|
|
325
|
+
run: pnpm exec breakage npm execute "$ENVIRONMENT_ID" --plan .breakage/npm-plan/plan.json --receipt ".breakage/receipts/$ENVIRONMENT_ID.json"
|
|
326
|
+
- uses: actions/upload-artifact@v4
|
|
327
|
+
if: always()
|
|
328
|
+
with:
|
|
329
|
+
name: peer-receipt-${{ matrix.id }}
|
|
330
|
+
path: .breakage/receipts/
|
|
331
|
+
include-hidden-files: true
|
|
332
|
+
if-no-files-found: error
|
|
333
|
+
verify:
|
|
334
|
+
name: Peer compatibility
|
|
335
|
+
if: always()
|
|
336
|
+
needs: [plan, execute]
|
|
337
|
+
runs-on: ubuntu-latest
|
|
227
338
|
steps:
|
|
228
|
-
|
|
229
|
-
|
|
339
|
+
- env:
|
|
340
|
+
PLAN_RESULT: ${{ needs.plan.result }}
|
|
341
|
+
EXECUTION_RESULT: ${{ needs.execute.result }}
|
|
342
|
+
run: test "$PLAN_RESULT" = success && test "$EXECUTION_RESULT" = success
|
|
343
|
+
- uses: actions/checkout@v4
|
|
344
|
+
with:
|
|
345
|
+
fetch-depth: 0
|
|
346
|
+
- uses: ./.github/actions/setup
|
|
347
|
+
- uses: actions/download-artifact@v4
|
|
348
|
+
with:
|
|
349
|
+
name: peer-plan
|
|
350
|
+
path: .breakage/npm-plan/
|
|
351
|
+
- uses: actions/download-artifact@v4
|
|
352
|
+
with:
|
|
353
|
+
pattern: peer-receipt-*
|
|
354
|
+
path: .breakage/receipts/
|
|
355
|
+
- run: pnpm exec breakage npm verify --plan .breakage/npm-plan/plan.json --receipts '.breakage/receipts/**/*.json'
|
|
230
356
|
```
|
|
231
357
|
|
|
232
|
-
|
|
358
|
+
Keep downloaded receipt artifacts in separate subdirectories: merging them could overwrite duplicate filenames before the verifier sees them. Require the stable aggregate `Peer compatibility` job. Pin action references according to your repository policy. Other CI systems can transport the same JSON and archive bundle, schedule `npmMatrix(plan).include`, and run the same commands.
|
|
359
|
+
|
|
360
|
+
### Remove repository-specific orchestration
|
|
361
|
+
|
|
362
|
+
For atom.io-style scripts, replace discovery/coverage commands with `npm plan`, archive collection and copying with its bundle, per-environment wrappers with `npm execute`, and custom receipt validation with `npm verify`. Delete the custom plan/receipt types, hashing, suite enumeration, and aggregate verifier tests once the repository uses the protocol. Keep package configs/shared release helpers, authored peer environments/contracts, the build/pack task, tool setup, and CI artifact transport. No workspace manager, task runner, Changesets versioning, or CI provider is invoked implicitly.
|
|
363
|
+
|
|
364
|
+
Verification rejects missing, duplicate, unexpected, malformed, stale, failed, or skipped receipts; wrong package/peer identities; audit reports substituted for execution; and missing, duplicate, unexecuted, or failing suites. It validates coverage/support audits as well as executions. The protocol checks consistency within trusted CI, not cryptographic attestation of an untrusted worker: a worker able to fabricate both plans and receipts can fabricate success. Commands, hooks, environment variables, and external services remain repository-owned inputs; use identical toolchains and deterministic hooks across jobs.
|
|
233
365
|
|
|
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
|
|
366
|
+
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. Do not reuse execution receipts across planning runs. There is no new npm prelude, and the matrix protocol is optional.
|
|
235
367
|
|
|
236
368
|
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
package/src/breakage.ts
CHANGED
|
@@ -42,6 +42,23 @@ export type { CommandResult } from "./task.ts"
|
|
|
42
42
|
export type { BreakageBaseline, BreakagePreludeOptions } from "./baseline.ts"
|
|
43
43
|
export { breakagePrelude } from "./baseline.ts"
|
|
44
44
|
export { formatNpmReport, npmCheck, npmTest } from "./npm/check.ts"
|
|
45
|
+
export {
|
|
46
|
+
npmPlan,
|
|
47
|
+
npmExecute,
|
|
48
|
+
npmVerify,
|
|
49
|
+
npmMatrix,
|
|
50
|
+
verifyNpmReceipts,
|
|
51
|
+
} from "./npm/matrix.ts"
|
|
52
|
+
export type {
|
|
53
|
+
NpmMatrixConfig,
|
|
54
|
+
NpmMatrixLabelContext,
|
|
55
|
+
NpmPrepareContext,
|
|
56
|
+
NpmArchiveRequest,
|
|
57
|
+
NpmPlan,
|
|
58
|
+
NpmReceipt,
|
|
59
|
+
NpmVerification,
|
|
60
|
+
NpmMatrixLocation,
|
|
61
|
+
} from "./npm/matrix.ts"
|
|
45
62
|
export type {
|
|
46
63
|
NpmConfig,
|
|
47
64
|
NpmPeerEvidence,
|
package/src/breakage.x.ts
CHANGED
|
@@ -27,6 +27,12 @@ import {
|
|
|
27
27
|
import { breakage, breakagePrelude } from "./breakage.ts"
|
|
28
28
|
import { errorMessage } from "./task.ts"
|
|
29
29
|
import { npmCheckManual, npmTestManual, runNpmCli } from "./npm/cli.ts"
|
|
30
|
+
import {
|
|
31
|
+
npmPlanManual,
|
|
32
|
+
npmExecuteManual,
|
|
33
|
+
npmVerifyManual,
|
|
34
|
+
runNpmMatrixCli,
|
|
35
|
+
} from "./npm/matrix-cli.ts"
|
|
30
36
|
|
|
31
37
|
// Either command spelling can override the other spelling in a config file.
|
|
32
38
|
const commandOverrides = new Set<`test` | `testCommand`>()
|
|
@@ -37,6 +43,7 @@ const BREAKAGE_MANUAL = options(
|
|
|
37
43
|
{
|
|
38
44
|
release: { configOnly: true },
|
|
39
45
|
npm: { configOnly: true },
|
|
46
|
+
npmMatrix: { configOnly: true },
|
|
40
47
|
out: { configOnly: true },
|
|
41
48
|
tagPattern: {
|
|
42
49
|
schema: configFields.tagPattern,
|
|
@@ -151,6 +158,9 @@ const parse = cli(
|
|
|
151
158
|
npm: optional({
|
|
152
159
|
help: null,
|
|
153
160
|
check: null,
|
|
161
|
+
plan: null,
|
|
162
|
+
execute: required({ $id: null }),
|
|
163
|
+
verify: null,
|
|
154
164
|
test: optional({ $peer: required({ $range: null }) }),
|
|
155
165
|
}),
|
|
156
166
|
prelude: optional({ $configPath: null }),
|
|
@@ -163,6 +173,9 @@ const parse = cli(
|
|
|
163
173
|
npm: noOptions(`Show npm compatibility commands.`),
|
|
164
174
|
"npm/help": noOptions(`Show npm compatibility commands.`),
|
|
165
175
|
"npm/check": npmCheckManual,
|
|
176
|
+
"npm/plan": npmPlanManual,
|
|
177
|
+
"npm/execute/$id": npmExecuteManual,
|
|
178
|
+
"npm/verify": npmVerifyManual,
|
|
166
179
|
"npm/test": npmTestManual,
|
|
167
180
|
"npm/test/$peer/$range": npmTestManual,
|
|
168
181
|
prelude: PRELUDE_MANUAL,
|
|
@@ -206,6 +219,19 @@ if (completion !== undefined) {
|
|
|
206
219
|
case `npm/check`:
|
|
207
220
|
process.exitCode = await runNpmCli(`check`, inputs.opts)
|
|
208
221
|
break
|
|
222
|
+
case `npm/plan`:
|
|
223
|
+
process.exitCode = await runNpmMatrixCli(`plan`, inputs.opts)
|
|
224
|
+
break
|
|
225
|
+
case `npm/execute/$id`:
|
|
226
|
+
process.exitCode = await runNpmMatrixCli(
|
|
227
|
+
`execute`,
|
|
228
|
+
inputs.opts,
|
|
229
|
+
inputs.params.id,
|
|
230
|
+
)
|
|
231
|
+
break
|
|
232
|
+
case `npm/verify`:
|
|
233
|
+
process.exitCode = await runNpmMatrixCli(`verify`, inputs.opts)
|
|
234
|
+
break
|
|
209
235
|
case `npm/test`:
|
|
210
236
|
process.exitCode = await runNpmCli(`test`, inputs.opts)
|
|
211
237
|
break
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type } from "arktype"
|
|
|
5
5
|
import { cli, options, type ConfigInput } from "comline"
|
|
6
6
|
|
|
7
7
|
import { npmConfigSchema } from "./npm/config.ts"
|
|
8
|
+
import { npmMatrixConfigSchema } from "./npm/matrix-model.ts"
|
|
8
9
|
import {
|
|
9
10
|
releaseSchema,
|
|
10
11
|
validateRelease,
|
|
@@ -34,6 +35,7 @@ export const configSchema = type({
|
|
|
34
35
|
"out?": configFields.out,
|
|
35
36
|
"release?": releaseSchema,
|
|
36
37
|
"npm?": npmConfigSchema,
|
|
38
|
+
"npmMatrix?": npmMatrixConfigSchema,
|
|
37
39
|
})
|
|
38
40
|
|
|
39
41
|
/** The complete config, with no fields exposed to argv by this loader. */
|
|
@@ -48,6 +50,7 @@ export const configOptions = options(`Breakage configuration`, configSchema, {
|
|
|
48
50
|
out: { configOnly: true },
|
|
49
51
|
release: { configOnly: true },
|
|
50
52
|
npm: { configOnly: true },
|
|
53
|
+
npmMatrix: { configOnly: true },
|
|
51
54
|
})
|
|
52
55
|
export type BreakageConfig = ConfigInput<typeof configOptions>
|
|
53
56
|
|
package/src/npm/check.ts
CHANGED
|
@@ -133,6 +133,7 @@ async function runRepresentative(
|
|
|
133
133
|
directory: string,
|
|
134
134
|
coverageOnly: boolean,
|
|
135
135
|
selection?: NpmTestSelection,
|
|
136
|
+
plannedSuites?: readonly string[],
|
|
136
137
|
): Promise<NpmRepresentativeResult> {
|
|
137
138
|
const label = path.relative(base, directory).split(path.sep).join(`/`)
|
|
138
139
|
const result: NpmRepresentativeResult = {
|
|
@@ -196,6 +197,7 @@ async function runRepresentative(
|
|
|
196
197
|
const peers = Object.keys(config.suites).filter((peer) => {
|
|
197
198
|
const evidence = result.peers[peer]
|
|
198
199
|
if (!evidence) return false
|
|
200
|
+
if (plannedSuites !== undefined) return plannedSuites.includes(peer)
|
|
199
201
|
if (selection && `peer` in selection) return peer === selection.peer
|
|
200
202
|
if (selection && `representative` in selection) return true
|
|
201
203
|
return matches(evidence.version, manifest.peerDependencies![peer]!)
|
|
@@ -257,6 +259,7 @@ async function run(
|
|
|
257
259
|
baseDirectory: string,
|
|
258
260
|
coverageOnly: boolean,
|
|
259
261
|
selection?: NpmTestSelection,
|
|
262
|
+
plannedSuites?: readonly string[],
|
|
260
263
|
): Promise<NpmReport> {
|
|
261
264
|
const report: NpmReport = {
|
|
262
265
|
package: { name: `(unresolved)`, version: `(unresolved)` },
|
|
@@ -330,6 +333,7 @@ async function run(
|
|
|
330
333
|
directory,
|
|
331
334
|
coverageOnly,
|
|
332
335
|
selection,
|
|
336
|
+
plannedSuites,
|
|
333
337
|
),
|
|
334
338
|
),
|
|
335
339
|
),
|
|
@@ -412,6 +416,21 @@ export async function npmTest(
|
|
|
412
416
|
)
|
|
413
417
|
}
|
|
414
418
|
|
|
419
|
+
/** Internal matrix entrypoint: keep the full config for validation and peer identity checks. */
|
|
420
|
+
export async function npmTestPlanned(
|
|
421
|
+
config: NpmConfig,
|
|
422
|
+
selection: { representative: string; suites: readonly string[] },
|
|
423
|
+
options: { baseDirectory?: string } = {},
|
|
424
|
+
): Promise<NpmReport> {
|
|
425
|
+
return await run(
|
|
426
|
+
config,
|
|
427
|
+
options.baseDirectory ?? process.cwd(),
|
|
428
|
+
false,
|
|
429
|
+
{ representative: selection.representative },
|
|
430
|
+
selection.suites,
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
415
434
|
export function formatNpmReport(report: NpmReport): string {
|
|
416
435
|
const lines = [
|
|
417
436
|
`${report.package.name}@${report.package.version} — npm ${report.mode}`,
|
package/src/npm/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ type CommonOptions = {
|
|
|
16
16
|
json?: boolean | undefined
|
|
17
17
|
release?: import("../release.ts").ReleaseConfig
|
|
18
18
|
npm?: import("./types.ts").NpmConfig
|
|
19
|
+
npmMatrix?: import("./matrix-model.ts").NpmMatrixConfig
|
|
19
20
|
}
|
|
20
21
|
type CheckOptions = CommonOptions & { coverageOnly?: boolean | undefined }
|
|
21
22
|
type TestOptions = CommonOptions & { representative?: string | undefined }
|
|
@@ -23,6 +24,7 @@ type TestOptions = CommonOptions & { representative?: string | undefined }
|
|
|
23
24
|
const common = {
|
|
24
25
|
release: configOptions.optionConfigs.release,
|
|
25
26
|
npm: configOptions.optionConfigs.npm,
|
|
27
|
+
npmMatrix: configOptions.optionConfigs.npmMatrix,
|
|
26
28
|
config: {
|
|
27
29
|
schema: type(`string`),
|
|
28
30
|
required: false,
|
|
@@ -47,6 +49,7 @@ export const npmCheckManual: OptionsGroup<CheckOptions> = options(
|
|
|
47
49
|
"coverageOnly?": `boolean`,
|
|
48
50
|
"release?": configOptions.optionsSchema.get(`release`),
|
|
49
51
|
"npm?": configOptions.optionsSchema.get(`npm`),
|
|
52
|
+
"npmMatrix?": configOptions.optionsSchema.get(`npmMatrix`),
|
|
50
53
|
}),
|
|
51
54
|
{
|
|
52
55
|
...common,
|
|
@@ -69,6 +72,7 @@ export const npmTestManual: OptionsGroup<TestOptions> = options(
|
|
|
69
72
|
"representative?": `string`,
|
|
70
73
|
"release?": configOptions.optionsSchema.get(`release`),
|
|
71
74
|
"npm?": configOptions.optionsSchema.get(`npm`),
|
|
75
|
+
"npmMatrix?": configOptions.optionsSchema.get(`npmMatrix`),
|
|
72
76
|
}),
|
|
73
77
|
{
|
|
74
78
|
...common,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { glob, readFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { type } from "arktype"
|
|
4
|
+
import { options } from "comline"
|
|
5
|
+
import { configOptions, defaultConfig, loadConfig } from "../config.ts"
|
|
6
|
+
import { errorMessage } from "../task.ts"
|
|
7
|
+
import { formatNpmReport } from "./check.ts"
|
|
8
|
+
import {
|
|
9
|
+
npmPlan,
|
|
10
|
+
npmExecute,
|
|
11
|
+
npmVerify,
|
|
12
|
+
npmMatrix,
|
|
13
|
+
writeMatrixJson,
|
|
14
|
+
} from "./matrix.ts"
|
|
15
|
+
|
|
16
|
+
const hiddenSchema = {
|
|
17
|
+
"release?": configOptions.optionsSchema.get(`release`),
|
|
18
|
+
"npm?": configOptions.optionsSchema.get(`npm`),
|
|
19
|
+
"npmMatrix?": configOptions.optionsSchema.get(`npmMatrix`),
|
|
20
|
+
}
|
|
21
|
+
const hidden = {
|
|
22
|
+
release: { configOnly: true },
|
|
23
|
+
npm: { configOnly: true },
|
|
24
|
+
npmMatrix: { configOnly: true },
|
|
25
|
+
} as const
|
|
26
|
+
const root = {
|
|
27
|
+
schema: type(`string`),
|
|
28
|
+
required: false,
|
|
29
|
+
description: `Source Git checkout root (defaults to the current directory).`,
|
|
30
|
+
example: `--root .`,
|
|
31
|
+
completion: { fileSystem: `directories` as const },
|
|
32
|
+
}
|
|
33
|
+
const plan = {
|
|
34
|
+
schema: type(`string`),
|
|
35
|
+
required: true,
|
|
36
|
+
description: `Prepared plan.json; archives are read from its bundle directory.`,
|
|
37
|
+
example: `--plan .breakage/npm-plan/plan.json`,
|
|
38
|
+
completion: { fileSystem: `files` as const },
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const npmPlanManual = options(
|
|
42
|
+
`Audit the peer matrix and prepare a portable archive bundle; print matrix JSON.`,
|
|
43
|
+
type({
|
|
44
|
+
...hiddenSchema,
|
|
45
|
+
"config?": `string`,
|
|
46
|
+
"root?": `string`,
|
|
47
|
+
"out?": `string`,
|
|
48
|
+
}),
|
|
49
|
+
{
|
|
50
|
+
...hidden,
|
|
51
|
+
root,
|
|
52
|
+
config: {
|
|
53
|
+
schema: type(`string`),
|
|
54
|
+
required: false,
|
|
55
|
+
description: `TypeScript configuration containing npmMatrix.`,
|
|
56
|
+
example: `--config breakage.config.ts`,
|
|
57
|
+
completion: { fileSystem: `files` },
|
|
58
|
+
},
|
|
59
|
+
out: {
|
|
60
|
+
schema: type(`string`),
|
|
61
|
+
required: false,
|
|
62
|
+
description: `Bundle directory (defaults to .breakage/npm-plan).`,
|
|
63
|
+
example: `--out .breakage/peer-bundle`,
|
|
64
|
+
completion: { fileSystem: `directories` },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
export const npmExecuteManual = options(
|
|
69
|
+
`Execute one planned representative and write its receipt.`,
|
|
70
|
+
type({
|
|
71
|
+
...hiddenSchema,
|
|
72
|
+
plan: `string`,
|
|
73
|
+
receipt: `string`,
|
|
74
|
+
"root?": `string`,
|
|
75
|
+
}),
|
|
76
|
+
{
|
|
77
|
+
...hidden,
|
|
78
|
+
root,
|
|
79
|
+
plan,
|
|
80
|
+
receipt: {
|
|
81
|
+
schema: type(`string`),
|
|
82
|
+
required: true,
|
|
83
|
+
description: `Output receipt filename.`,
|
|
84
|
+
example: `--receipt .breakage/receipts/environment.json`,
|
|
85
|
+
completion: { fileSystem: `files` },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
export const npmVerifyManual = options(
|
|
90
|
+
`Verify the complete planned matrix against collected execution receipts.`,
|
|
91
|
+
type({
|
|
92
|
+
...hiddenSchema,
|
|
93
|
+
plan: `string`,
|
|
94
|
+
receipts: `string`,
|
|
95
|
+
"root?": `string`,
|
|
96
|
+
}),
|
|
97
|
+
{
|
|
98
|
+
...hidden,
|
|
99
|
+
root,
|
|
100
|
+
plan,
|
|
101
|
+
receipts: {
|
|
102
|
+
schema: type(`string`),
|
|
103
|
+
required: true,
|
|
104
|
+
description: `Glob selecting all collected JSON receipts, relative to root.`,
|
|
105
|
+
example: `--receipts '.breakage/receipts/**/*.json'`,
|
|
106
|
+
completion: { fileSystem: `files` },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
export async function runNpmMatrixCli(
|
|
112
|
+
action: `plan` | `execute` | `verify`,
|
|
113
|
+
opts: {
|
|
114
|
+
root?: string
|
|
115
|
+
config?: string
|
|
116
|
+
out?: string
|
|
117
|
+
plan?: string
|
|
118
|
+
receipt?: string
|
|
119
|
+
receipts?: string
|
|
120
|
+
},
|
|
121
|
+
id?: string,
|
|
122
|
+
): Promise<number> {
|
|
123
|
+
try {
|
|
124
|
+
const rootDirectory = path.resolve(opts.root ?? process.cwd())
|
|
125
|
+
if (action === `plan`) {
|
|
126
|
+
const config = await loadConfig(
|
|
127
|
+
path.resolve(
|
|
128
|
+
rootDirectory,
|
|
129
|
+
opts.config ?? defaultConfig(rootDirectory),
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
if (!config.npmMatrix)
|
|
133
|
+
throw new Error(`Configure npmMatrix.configs and npmMatrix.prepare.`)
|
|
134
|
+
const prepared = await npmPlan(config.npmMatrix, {
|
|
135
|
+
rootDirectory,
|
|
136
|
+
...(opts.out ? { bundleDirectory: opts.out } : {}),
|
|
137
|
+
})
|
|
138
|
+
process.stdout.write(`${JSON.stringify(npmMatrix(prepared))}\n`)
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
const filename = path.resolve(rootDirectory, opts.plan!)
|
|
142
|
+
const value: unknown = JSON.parse(await readFile(filename, `utf8`))
|
|
143
|
+
const location = { rootDirectory, bundleDirectory: path.dirname(filename) }
|
|
144
|
+
if (action === `execute`) {
|
|
145
|
+
const receipt = await npmExecute(value, id!, location)
|
|
146
|
+
await writeMatrixJson(path.resolve(rootDirectory, opts.receipt!), receipt)
|
|
147
|
+
if (receipt.report) process.stdout.write(formatNpmReport(receipt.report))
|
|
148
|
+
if (receipt.error) process.stderr.write(`${receipt.error}\n`)
|
|
149
|
+
return receipt.exitCode
|
|
150
|
+
}
|
|
151
|
+
const receipts: unknown[] = []
|
|
152
|
+
for await (const filename of glob(opts.receipts!, { cwd: rootDirectory }))
|
|
153
|
+
receipts.push(
|
|
154
|
+
JSON.parse(
|
|
155
|
+
await readFile(path.resolve(rootDirectory, filename), `utf8`),
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
const result = await npmVerify(value, receipts, location)
|
|
159
|
+
if (result.errors.length)
|
|
160
|
+
process.stderr.write(`${result.errors.join(`\n`)}\n`)
|
|
161
|
+
else
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
`Verified passing receipts for all ${result.environments} planned environments.\n`,
|
|
164
|
+
)
|
|
165
|
+
return result.exitCode
|
|
166
|
+
} catch (error) {
|
|
167
|
+
process.stderr.write(`npm matrix setup error: ${errorMessage(error)}\n`)
|
|
168
|
+
return 2
|
|
169
|
+
}
|
|
170
|
+
}
|