breakage 0.8.5 → 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.
Files changed (45) hide show
  1. package/README.md +163 -47
  2. package/dist/{breakage-B7dJsgcu.js → breakage-DJiIiLzL.js} +958 -221
  3. package/dist/breakage-DJiIiLzL.js.map +1 -0
  4. package/dist/breakage.d.ts +239 -71
  5. package/dist/breakage.d.ts.map +1 -1
  6. package/dist/breakage.js +3 -1
  7. package/dist/breakage.x.js +216 -79
  8. package/dist/breakage.x.js.map +1 -1
  9. package/dist/changesets.d.ts +8 -0
  10. package/dist/changesets.d.ts.map +1 -0
  11. package/dist/changesets.js +27 -0
  12. package/dist/changesets.js.map +1 -0
  13. package/dist/release-BsdIDT4g.js +69 -0
  14. package/dist/release-BsdIDT4g.js.map +1 -0
  15. package/dist/release-D47BqRuB.d.ts +40 -0
  16. package/dist/release-D47BqRuB.d.ts.map +1 -0
  17. package/npm.md +198 -44
  18. package/package.json +45 -6
  19. package/src/breakage.ts +142 -73
  20. package/src/breakage.x.ts +174 -127
  21. package/src/changesets.ts +51 -0
  22. package/src/config.ts +131 -0
  23. package/src/npm/check.ts +70 -50
  24. package/src/npm/cli.ts +30 -22
  25. package/src/npm/config.ts +38 -6
  26. package/src/npm/environment.ts +10 -10
  27. package/src/npm/matrix-cli.ts +170 -0
  28. package/src/npm/matrix-model.ts +395 -0
  29. package/src/npm/matrix.ts +657 -0
  30. package/src/npm/release.ts +9 -25
  31. package/src/npm/types.ts +5 -45
  32. package/src/release.ts +153 -0
  33. package/src/{npm/command.ts → task.ts} +2 -0
  34. package/dist/breakage-B7dJsgcu.js.map +0 -1
  35. package/dist/breakage.$configPath.schema.json +0 -33
  36. package/dist/breakage.help.schema.json +0 -6
  37. package/dist/breakage.main.schema.json +0 -33
  38. package/dist/breakage.npm.check.schema.json +0 -16
  39. package/dist/breakage.npm.help.schema.json +0 -6
  40. package/dist/breakage.npm.schema.json +0 -6
  41. package/dist/breakage.npm.test.$peer.$range.schema.json +0 -16
  42. package/dist/breakage.npm.test.schema.json +0 -16
  43. package/dist/breakage.prelude.$configPath.schema.json +0 -31
  44. package/dist/breakage.prelude.schema.json +0 -31
  45. package/dist/breakage.schema.schema.json +0 -10
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,14 +5,60 @@ 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"
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"
17
62
  export type {
18
63
  NpmConfig,
19
64
  NpmPeerEvidence,
@@ -22,9 +67,6 @@ export type {
22
67
  NpmSuite,
23
68
  NpmSuiteResult,
24
69
  NpmTestSelection,
25
- ReleaseContext,
26
- ReleaseEvidence,
27
- ReleaseIntent,
28
70
  SupportChange,
29
71
  } from "./npm/types.ts"
30
72
 
@@ -32,14 +74,17 @@ export type BreakageOptions = {
32
74
  tagPattern?: string | undefined
33
75
  baselineFile?: string | undefined
34
76
  testPattern: string
35
- testCommand: string
36
- certifyCommand: string
77
+ testCommand?: string
78
+ /** Shell command. Do not combine with testCommand. */
79
+ test?: string
80
+ release?: ReleaseConfig
37
81
  baseDirname?: string
38
82
  verbose?: boolean | undefined
39
83
  }
40
84
 
41
85
  export type BreakageOutcome =
42
86
  | { gitWasClean: false }
87
+ | { gitWasClean: true; setupError: string }
43
88
  | {
44
89
  gitWasClean: true
45
90
  lastReleaseFound: false
@@ -57,6 +102,7 @@ export type BreakageOutcome =
57
102
  testsWereFound: true
58
103
  testsFound: string[]
59
104
  breakingChangesFound: false
105
+ release?: VersionDelta
60
106
  }
61
107
  | {
62
108
  gitWasClean: true
@@ -66,20 +112,35 @@ export type BreakageOutcome =
66
112
  testsFound: string[]
67
113
  breakingChangesFound: true
68
114
  testResult: string
69
- breakingChangesCertified: boolean
70
- certificationStdout: string
71
- certificationStderr: string
115
+ release?: VersionDelta
72
116
  }
73
117
 
74
- export async function breakage({
75
- tagPattern,
76
- baselineFile,
77
- testPattern,
78
- testCommand,
79
- certifyCommand,
80
- baseDirname = process.cwd(),
81
- verbose = false,
82
- }: BreakageOptions): Promise<BreakageOutcome & { summary: string }> {
118
+ export async function breakage(
119
+ options: BreakageOptions,
120
+ ): Promise<BreakageOutcome & { summary: string }> {
121
+ rejectCertificationOptions(options)
122
+ const {
123
+ tagPattern,
124
+ baselineFile,
125
+ testPattern,
126
+ testCommand,
127
+ test,
128
+ release,
129
+ baseDirname = process.cwd(),
130
+ verbose = false,
131
+ } = options
132
+ if (!testPattern || (!test && !testCommand))
133
+ throw new Error(
134
+ `required arguments were not provided: testPattern and test or testCommand.`,
135
+ )
136
+ if (test !== undefined && testCommand !== undefined)
137
+ throw new Error(`Choose test or testCommand, not both.`)
138
+ if (
139
+ typeof (test ?? testCommand) !== `string` ||
140
+ !(test ?? testCommand)!.trim()
141
+ )
142
+ throw new Error(`test must be a nonempty shell command string.`)
143
+ if (release !== undefined) validateRelease(release)
83
144
  let mark: Chronicle[`mark`] | undefined
84
145
  let logMarks: Chronicle[`logMarks`] | undefined
85
146
  if (verbose) {
@@ -181,6 +242,34 @@ export async function breakage({
181
242
  )
182
243
  mark?.(`filtered to public test files`)
183
244
 
245
+ let releaseEvidence: VersionDelta | undefined
246
+ if (release) {
247
+ const current = await readManifest(path.join(baseDirname, `package.json`))
248
+ const prefix = await git.revparse([`--show-prefix`])
249
+ const previous = parseManifest(
250
+ JSON.parse(await git.show([`${releaseCommit}:${prefix}package.json`])),
251
+ `baseline`,
252
+ )
253
+ if (previous.name !== current.name)
254
+ throw new Error(`Release baseline belongs to another package.`)
255
+ const transition = await resolveReleaseDelta(release, {
256
+ packageName: current.name,
257
+ packageDirectory: path.resolve(baseDirname),
258
+ currentVersion: current.version,
259
+ baseline: {
260
+ version: previous.version,
261
+ ref: latestReleaseTag,
262
+ commit: releaseCommit,
263
+ },
264
+ })
265
+ if (transition !== undefined)
266
+ releaseEvidence = versionDelta(
267
+ transition.previousVersion,
268
+ transition.version,
269
+ release.policy,
270
+ )
271
+ }
272
+
184
273
  const prepared = await withTestFileState(git, baseDirname, async (state) => {
185
274
  if (!(await state.isClean())) {
186
275
  return {
@@ -206,61 +295,41 @@ export async function breakage({
206
295
  const { restore } = prepared
207
296
 
208
297
  try {
209
- const noBreakingChangesDetected = await new Promise<
210
- BreakageOutcome & { summary: string }
211
- >((resolve, reject) => {
212
- const result = exec(
213
- testCommand,
214
- { cwd: baseDirname },
215
- (_, __, stderr) => {
216
- if (result.exitCode === 0) {
217
- resolve({
218
- summary: `No breaking changes were detected.`,
219
- gitWasClean: true,
220
- lastReleaseFound: true,
221
- lastReleaseTag: latestReleaseTag,
222
- testsWereFound: true,
223
- testsFound: productionTestFiles,
224
- breakingChangesFound: false,
225
- })
226
- } else {
227
- reject(stderr)
228
- }
229
- },
230
- )
231
- })
232
- mark?.(`passed tests`)
233
- return noBreakingChangesDetected
234
- } catch (thrown) {
235
- mark?.(`failed tests`)
236
- const breakingChangesCertified = await new Promise<
237
- BreakageOutcome & { summary: string }
238
- >((resolve) => {
239
- const result = exec(
240
- certifyCommand,
241
- { cwd: baseDirname },
242
- (_, stdout, stderr) => {
243
- const wereCertified = result.exitCode === 0
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
298
+ const result = await command(test ?? testCommand!, baseDirname)
299
+ if (result.code > 1)
300
+ return {
301
+ gitWasClean: true,
302
+ setupError: result.output,
303
+ summary: `Public test setup failed (${result.code}).`,
304
+ }
305
+ const common = {
306
+ gitWasClean: true as const,
307
+ lastReleaseFound: true as const,
308
+ lastReleaseTag: latestReleaseTag,
309
+ testsWereFound: true as const,
310
+ testsFound: productionTestFiles,
311
+ ...(releaseEvidence ? { release: releaseEvidence } : {}),
312
+ }
313
+ if (result.code === 0)
314
+ return {
315
+ ...common,
316
+ breakingChangesFound: false,
317
+ summary: `No breaking changes were detected.`,
318
+ }
319
+ return {
320
+ ...common,
321
+ breakingChangesFound: true,
322
+ testResult: result.output,
323
+ summary: releaseEvidence?.breaking
324
+ ? `Breaking changes are covered by release ${releaseEvidence.previousVersion} → ${releaseEvidence.version}.`
325
+ : `Breaking changes require a breaking release.`,
326
+ }
327
+ } catch (error) {
328
+ return {
329
+ gitWasClean: true,
330
+ setupError: errorMessage(error),
331
+ summary: `Public test setup failed: ${errorMessage(error)}`,
332
+ }
264
333
  } finally {
265
334
  await restore()
266
335
  mark?.(`restored original public tests`)
package/src/breakage.x.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import path from "node:path"
3
4
  import { inspect } from "node:util"
4
5
 
5
- import { type } from "arktype"
6
- import type { OptionsGroup } from "comline"
7
6
  import {
8
7
  cli,
9
8
  completionResponse,
@@ -18,28 +17,36 @@ import {
18
17
  } from "comline"
19
18
  import logger from "takua"
20
19
 
21
- import type { BreakageOptions } from "./breakage.ts"
22
- import { breakage, breakagePrelude } from "./breakage.ts"
23
20
  import {
24
21
  defaultConfig,
25
- npmCheckManual,
26
- npmTestManual,
27
- runNpmCli,
28
- } from "./npm/cli.ts"
22
+ configPath,
23
+ resolveConfig,
24
+ configSchema,
25
+ configFields,
26
+ } from "./config.ts"
27
+ import { breakage, breakagePrelude } from "./breakage.ts"
28
+ import { errorMessage } from "./task.ts"
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"
36
+
37
+ // Either command spelling can override the other spelling in a config file.
38
+ const commandOverrides = new Set<`test` | `testCommand`>()
29
39
 
30
40
  const BREAKAGE_MANUAL = options(
31
41
  `Check for breaking changes in a package.`,
32
- type({
33
- "tagPattern?": `string`,
34
- "baselineFile?": `string`,
35
- testPattern: `string`,
36
- testCommand: `string`,
37
- certifyCommand: `string`,
38
- "baseDirname?": `string`,
39
- "verbose?": `boolean`,
40
- }),
42
+ configSchema,
41
43
  {
44
+ release: { configOnly: true },
45
+ npm: { configOnly: true },
46
+ npmMatrix: { configOnly: true },
47
+ out: { configOnly: true },
42
48
  tagPattern: {
49
+ schema: configFields.tagPattern,
43
50
  aliases: [`tag-pattern`],
44
51
  completion: { repeatable: false },
45
52
  flag: `g`,
@@ -48,6 +55,7 @@ const BREAKAGE_MANUAL = options(
48
55
  example: `--tagPattern="my-library"`,
49
56
  },
50
57
  baselineFile: {
58
+ schema: configFields.baselineFile,
51
59
  aliases: [`baseline-file`],
52
60
  completion: { repeatable: false, fileSystem: `files` },
53
61
  required: false,
@@ -55,30 +63,41 @@ const BREAKAGE_MANUAL = options(
55
63
  example: `--baseline-file=.breakage/baseline.json`,
56
64
  },
57
65
  testPattern: {
66
+ schema: configFields.testPattern,
58
67
  aliases: [`test-pattern`, `pattern`],
59
68
  completion: { repeatable: false },
60
69
  flag: `p`,
61
- required: true,
70
+ required: false,
62
71
  description: `The pattern to match test files that test the public API of the library.`,
63
72
  example: `--pattern="*__public.test.ts"`,
64
73
  },
65
74
  testCommand: {
75
+ parse: (value) => {
76
+ commandOverrides.add(`testCommand`)
77
+ return value
78
+ },
79
+ schema: configFields.testCommand,
66
80
  aliases: [`test-command`],
67
81
  completion: { repeatable: false },
68
82
  flag: `t`,
69
- required: true,
83
+ required: false,
70
84
  description: `Complete bash command that runs the tests for the library's public API.`,
71
85
  example: `--testCommand="npm run test"`,
72
86
  },
73
- certifyCommand: {
74
- aliases: [`certify-command`],
87
+ test: {
88
+ parse: (value) => {
89
+ commandOverrides.add(`test`)
90
+ return value
91
+ },
92
+ schema: configFields.test,
75
93
  completion: { repeatable: false },
76
- flag: `c`,
77
- required: true,
78
- description: `Complete bash command that determines whether a major version bump for your package is indicated in the workspace. Exit code 0 indicates that a major version bump is indicated, and exit code 1 indicates that no major version bump is indicated.`,
79
- example: `--certifyCommand=\tsx scripts/certify-major-version-bump.node`,
94
+ required: false,
95
+ description: `Shell command running the restored public contracts.`,
96
+ example: `--test="npm run test:public"`,
80
97
  },
81
98
  baseDirname: {
99
+ parse: (value) => path.resolve(value),
100
+ schema: configFields.baseDirname,
82
101
  aliases: [`base-dir`, `base-dirname`],
83
102
  completion: { repeatable: false, fileSystem: `directories` },
84
103
  flag: `d`,
@@ -87,6 +106,7 @@ const BREAKAGE_MANUAL = options(
87
106
  example: `--baseDirname=./packages/my-package`,
88
107
  },
89
108
  verbose: {
109
+ schema: configFields.verbose,
90
110
  completion: { repeatable: false },
91
111
  flag: `v`,
92
112
  required: false,
@@ -95,20 +115,11 @@ const BREAKAGE_MANUAL = options(
95
115
  parse: parseBooleanOption,
96
116
  },
97
117
  },
98
- ) satisfies OptionsGroup<BreakageOptions>
118
+ )
99
119
 
100
120
  const PRELUDE_MANUAL = options(
101
121
  `Resolve and fetch the release baseline for a cacheable compatibility check.`,
102
- type({
103
- "tagPattern?": `string`,
104
- "baseDirname?": `string`,
105
- "out?": `string`,
106
- "testPattern?": `string`,
107
- "testCommand?": `string`,
108
- "certifyCommand?": `string`,
109
- "baselineFile?": `string`,
110
- "verbose?": `boolean`,
111
- }),
122
+ configSchema,
112
123
  {
113
124
  ...BREAKAGE_MANUAL.optionConfigs,
114
125
  testPattern: {
@@ -119,11 +130,8 @@ const PRELUDE_MANUAL = options(
119
130
  ...BREAKAGE_MANUAL.optionConfigs.testCommand,
120
131
  required: false,
121
132
  },
122
- certifyCommand: {
123
- ...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
124
- required: false,
125
- },
126
133
  out: {
134
+ schema: configFields.out,
127
135
  completion: { repeatable: false, fileSystem: `files` },
128
136
  required: false,
129
137
  description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
@@ -132,30 +140,27 @@ const PRELUDE_MANUAL = options(
132
140
  },
133
141
  )
134
142
 
135
- const SCHEMA_MANUAL = options(
136
- `Create a copy of the schema for configuring breakage.`,
137
- type({ "outdir?": `string` }),
138
- {
139
- outdir: {
140
- aliases: [`out-dir`],
141
- completion: { repeatable: false, fileSystem: `directories` },
142
- flag: `o`,
143
- required: false,
144
- description: `Directory to write the schema to.`,
145
- example: `--outdir=./dist`,
146
- },
147
- },
148
- )
149
-
143
+ let loadedConfigPath: string | undefined
150
144
  const parse = cli(
151
145
  {
152
146
  cliName: `breakage`,
147
+ discoverConfigPath: (segments: readonly string[]) => {
148
+ if (segments[0] === `help` || segments[0] === `npm`) return undefined
149
+ const explicit = segments[0] === `prelude` ? segments[1] : segments[0]
150
+ loadedConfigPath = configPath(
151
+ explicit ?? defaultConfig(process.cwd()),
152
+ explicit !== undefined,
153
+ )
154
+ return loadedConfigPath
155
+ },
153
156
  routes: optional({
154
157
  help: null,
155
- schema: null,
156
158
  npm: optional({
157
159
  help: null,
158
160
  check: null,
161
+ plan: null,
162
+ execute: required({ $id: null }),
163
+ verify: null,
159
164
  test: optional({ $peer: required({ $range: null }) }),
160
165
  }),
161
166
  prelude: optional({ $configPath: null }),
@@ -165,10 +170,12 @@ const parse = cli(
165
170
  "": BREAKAGE_MANUAL,
166
171
  $configPath: BREAKAGE_MANUAL,
167
172
  help: noOptions(`Show usage.`),
168
- schema: SCHEMA_MANUAL,
169
173
  npm: noOptions(`Show npm compatibility commands.`),
170
174
  "npm/help": noOptions(`Show npm compatibility commands.`),
171
175
  "npm/check": npmCheckManual,
176
+ "npm/plan": npmPlanManual,
177
+ "npm/execute/$id": npmExecuteManual,
178
+ "npm/verify": npmVerifyManual,
172
179
  "npm/test": npmTestManual,
173
180
  "npm/test/$peer/$range": npmTestManual,
174
181
  prelude: PRELUDE_MANUAL,
@@ -178,15 +185,6 @@ const parse = cli(
178
185
  $configPath: { fileSystem: `files` },
179
186
  "prelude/$configPath": { fileSystem: `files` },
180
187
  },
181
- discoverConfigPath: (args: readonly string[]) => {
182
- if (args[0] === `help` || args[0] === `schema` || args[0] === `npm`) {
183
- return
184
- }
185
- const configPath =
186
- (args[0] === `prelude` ? args[1] : args[0]) ??
187
- defaultConfig(process.cwd())
188
- return configPath
189
- },
190
188
  },
191
189
  {
192
190
  info: (message, ...data) => logger.info(`breakage`, message, data),
@@ -198,74 +196,123 @@ const completion = await completionResponse(parse.definition, process.argv)
198
196
  if (completion !== undefined) {
199
197
  process.stdout.write(completion)
200
198
  } else {
201
- const { inputs, warnings, writeJsonSchema } = parse(process.argv)
202
- logWarnings(warnings)
199
+ try {
200
+ const { inputs, warnings } = await parse.async(process.argv)
201
+ logWarnings(warnings)
202
+ if (
203
+ process.argv
204
+ .slice(2)
205
+ .some((arg) =>
206
+ /^(?:--certifyCommand|--certify-command|-c)(?:=|$)/.test(arg),
207
+ )
208
+ )
209
+ throw new Error(
210
+ `certifyCommand was removed. Configure release.resolveDelta and release.policy instead.`,
211
+ )
203
212
 
204
- switch (inputs.case) {
205
- case `npm`:
206
- case `npm/help`:
207
- case `help`:
208
- console.log(help(parse.definition))
209
- break
210
- case `npm/check`:
211
- process.exitCode = await runNpmCli(`check`, inputs.opts)
212
- break
213
- case `npm/test`:
214
- process.exitCode = await runNpmCli(`test`, inputs.opts)
215
- break
216
- case `npm/test/$peer/$range`:
217
- process.exitCode = await runNpmCli(`test`, inputs.opts, inputs.params)
218
- break
219
- case `schema`:
220
- {
221
- const { outdir } = inputs.opts
222
- writeJsonSchema(outdir ?? `.`)
223
- process.stdout.write(`📝 Wrote json.schema.`)
224
- }
225
- break
226
- case `prelude`:
227
- case `prelude/$configPath`: {
228
- const baseline = await breakagePrelude({
229
- ...inputs.opts,
230
- out: inputs.opts.out ?? `.breakage/baseline.json`,
231
- })
232
- process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`)
233
- break
234
- }
235
- case ``:
236
- case `$configPath`: {
237
- const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
238
- console: true,
239
- stdout: true,
240
- })
241
- if (`testResult` in returnValue && returnValue.breakingChangesFound) {
242
- process.stdout.write(returnValue.testResult)
213
+ switch (inputs.case) {
214
+ case `npm`:
215
+ case `npm/help`:
216
+ case `help`:
217
+ console.log(help(parse.definition))
218
+ break
219
+ case `npm/check`:
220
+ process.exitCode = await runNpmCli(`check`, inputs.opts)
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
235
+ case `npm/test`:
236
+ process.exitCode = await runNpmCli(`test`, inputs.opts)
237
+ break
238
+ case `npm/test/$peer/$range`:
239
+ process.exitCode = await runNpmCli(`test`, inputs.opts, inputs.params)
240
+ break
241
+ case `prelude`:
242
+ case `prelude/$configPath`: {
243
+ const config = resolveConfig(inputs.opts, loadedConfigPath)
244
+ const baseline = await breakagePrelude({
245
+ ...config,
246
+ out: config.out ?? `.breakage/baseline.json`,
247
+ })
248
+ process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`)
249
+ break
243
250
  }
244
- if (`breakingChangesFound` in returnValue) {
245
- if (returnValue.breakingChangesFound) {
246
- if (returnValue.breakingChangesCertified) {
247
- process.stdout.write(
248
- `👷 Breaking changes were found and certified.`,
249
- )
250
- process.exit(0)
251
+ case ``:
252
+ case `$configPath`: {
253
+ const merged = resolveConfig(inputs.opts, loadedConfigPath)
254
+ if (
255
+ commandOverrides.has(`testCommand`) &&
256
+ !commandOverrides.has(`test`)
257
+ )
258
+ delete merged.test
259
+ if (
260
+ commandOverrides.has(`test`) &&
261
+ !commandOverrides.has(`testCommand`)
262
+ )
263
+ delete merged.testCommand
264
+ if (!merged.testPattern)
265
+ throw new Error(`required arguments were not provided: testPattern.`)
266
+ const { returnValue } = await encapsulate(
267
+ () =>
268
+ breakage({
269
+ ...merged,
270
+ testPattern: merged.testPattern!,
271
+ }),
272
+ {
273
+ console: true,
274
+ stdout: true,
275
+ },
276
+ )
277
+ if (`release` in returnValue && returnValue.release) {
278
+ const { previousVersion, version, delta, breaking } =
279
+ returnValue.release
280
+ process.stdout.write(
281
+ `Release: ${previousVersion} → ${version} (${delta}; ${breaking ? `breaking` : `compatible`}).\n`,
282
+ )
283
+ }
284
+ if (`testResult` in returnValue && returnValue.breakingChangesFound) {
285
+ process.stdout.write(returnValue.testResult)
286
+ }
287
+ if (`breakingChangesFound` in returnValue) {
288
+ if (returnValue.breakingChangesFound) {
289
+ if (returnValue.release?.breaking) {
290
+ process.stdout.write(
291
+ `👷 Breaking changes are covered by the breaking release.`,
292
+ )
293
+ process.exit(0)
294
+ } else {
295
+ process.stdout.write(
296
+ `❌ Breaking changes require a breaking release.`,
297
+ )
298
+ process.exit(1)
299
+ }
251
300
  } else {
252
- process.stdout.write(
253
- `❌ Breaking changes were found, but not certified.`,
254
- )
255
- process.exit(1)
301
+ process.stdout.write(`✅ No breaking changes were found.`)
302
+ process.exit(0)
256
303
  }
257
304
  } else {
258
- process.stdout.write(`✅ No breaking changes were found.`)
259
- process.exit(0)
305
+ process.stdout.write(
306
+ `💥 Breakage failed to determine breaking changes.` +
307
+ `\n` +
308
+ inspect(returnValue),
309
+ )
310
+ process.exit(2)
260
311
  }
261
- } else {
262
- process.stdout.write(
263
- `💥 Breakage failed to determine breaking changes.` +
264
- `\n` +
265
- inspect(returnValue),
266
- )
267
- process.exit(2)
268
312
  }
269
313
  }
314
+ } catch (error) {
315
+ process.stderr.write(`Breakage setup error: ${errorMessage(error)}\n`)
316
+ process.exitCode = 2
270
317
  }
271
318
  }