breakage 0.0.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as breakage } from "./breakage-D_nc-zEK.js";
2
+ import { n as breakagePrelude, t as breakage } from "./breakage-Dc4IldeW.js";
3
3
  import * as path$1 from "node:path";
4
4
  import logger from "takua";
5
5
  import { inspect } from "node:util";
@@ -8,6 +8,7 @@ import { cli, completionResponse, encapsulate, help, logWarnings, noOptions, opt
8
8
  //#region src/breakage.x.ts
9
9
  const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type({
10
10
  "tagPattern?": `string`,
11
+ "baselineFile?": `string`,
11
12
  testPattern: `string`,
12
13
  testCommand: `string`,
13
14
  certifyCommand: `string`,
@@ -22,6 +23,16 @@ const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type
22
23
  description: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,
23
24
  example: `--tagPattern="my-library"`
24
25
  },
26
+ baselineFile: {
27
+ aliases: [`baseline-file`],
28
+ completion: {
29
+ repeatable: false,
30
+ fileSystem: `files`
31
+ },
32
+ required: false,
33
+ description: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,
34
+ example: `--baseline-file=.breakage/baseline.json`
35
+ },
25
36
  testPattern: {
26
37
  aliases: [`test-pattern`, `pattern`],
27
38
  completion: { repeatable: false },
@@ -66,6 +77,39 @@ const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type
66
77
  parse: parseBooleanOption
67
78
  }
68
79
  });
80
+ const PRELUDE_MANUAL = options(`Resolve and fetch the release baseline for a cacheable compatibility check.`, type({
81
+ "tagPattern?": `string`,
82
+ "baseDirname?": `string`,
83
+ "out?": `string`,
84
+ "testPattern?": `string`,
85
+ "testCommand?": `string`,
86
+ "certifyCommand?": `string`,
87
+ "baselineFile?": `string`,
88
+ "verbose?": `boolean`
89
+ }), {
90
+ ...BREAKAGE_MANUAL.optionConfigs,
91
+ testPattern: {
92
+ ...BREAKAGE_MANUAL.optionConfigs.testPattern,
93
+ required: false
94
+ },
95
+ testCommand: {
96
+ ...BREAKAGE_MANUAL.optionConfigs.testCommand,
97
+ required: false
98
+ },
99
+ certifyCommand: {
100
+ ...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
101
+ required: false
102
+ },
103
+ out: {
104
+ completion: {
105
+ repeatable: false,
106
+ fileSystem: `files`
107
+ },
108
+ required: false,
109
+ description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
110
+ example: `--out=.breakage/baseline.json`
111
+ }
112
+ });
69
113
  const SCHEMA_MANUAL = options(`Create a copy of the schema for configuring breakage.`, type({ "outdir?": `string` }), { outdir: {
70
114
  aliases: [`out-dir`],
71
115
  completion: {
@@ -82,18 +126,24 @@ const parse = cli({
82
126
  routes: optional({
83
127
  help: null,
84
128
  schema: null,
129
+ prelude: optional({ $configPath: null }),
85
130
  $configPath: null
86
131
  }),
87
132
  routeOptions: {
88
133
  "": BREAKAGE_MANUAL,
89
134
  $configPath: BREAKAGE_MANUAL,
90
135
  help: noOptions(`Show usage.`),
91
- schema: SCHEMA_MANUAL
136
+ schema: SCHEMA_MANUAL,
137
+ prelude: PRELUDE_MANUAL,
138
+ "prelude/$configPath": PRELUDE_MANUAL
139
+ },
140
+ positionalCompletions: {
141
+ $configPath: { fileSystem: `files` },
142
+ "prelude/$configPath": { fileSystem: `files` }
92
143
  },
93
- positionalCompletions: { $configPath: { fileSystem: `files` } },
94
144
  discoverConfigPath: (args) => {
95
145
  if (args[0] === `help` || args[0] === `schema`) return;
96
- return args[0] ?? path$1.join(process.cwd(), `breakage.config.json`);
146
+ return (args[0] === `prelude` ? args[1] : args[0]) ?? path$1.join(process.cwd(), `breakage.config.json`);
97
147
  }
98
148
  }, {
99
149
  info: (message, ...data) => logger.info(`breakage`, message, data),
@@ -115,6 +165,15 @@ else {
115
165
  process.stdout.write(`📝 Wrote json.schema.`);
116
166
  }
117
167
  break;
168
+ case `prelude`:
169
+ case `prelude/$configPath`: {
170
+ const baseline = await breakagePrelude({
171
+ ...inputs.opts,
172
+ out: inputs.opts.out ?? `.breakage/baseline.json`
173
+ });
174
+ process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`);
175
+ break;
176
+ }
118
177
  case ``:
119
178
  case `$configPath`: {
120
179
  const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
@@ -1 +1 @@
1
- {"version":3,"file":"breakage.x.js","names":["path"],"sources":["../src/breakage.x.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as path from \"node:path\"\nimport { inspect } from \"node:util\"\n\nimport { type } from \"arktype\"\nimport type { OptionsGroup } from \"comline\"\nimport {\n\tcli,\n\tcompletionResponse,\n\tencapsulate,\n\thelp,\n\tlogWarnings,\n\tnoOptions,\n\toptional,\n\toptions,\n\tparseBooleanOption,\n} from \"comline\"\nimport logger from \"takua\"\n\nimport type { BreakageOptions } from \"./breakage.ts\"\nimport { breakage } from \"./breakage.ts\"\n\nconst BREAKAGE_MANUAL = options(\n\t`Check for breaking changes in a package.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\ttestPattern: `string`,\n\t\ttestCommand: `string`,\n\t\tcertifyCommand: `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\ttagPattern: {\n\t\t\taliases: [`tag-pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `g`,\n\t\t\trequired: false,\n\t\t\tdescription: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,\n\t\t\texample: `--tagPattern=\"my-library\"`,\n\t\t},\n\t\ttestPattern: {\n\t\t\taliases: [`test-pattern`, `pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `p`,\n\t\t\trequired: true,\n\t\t\tdescription: `The pattern to match test files that test the public API of the library.`,\n\t\t\texample: `--pattern=\"*__public.test.ts\"`,\n\t\t},\n\t\ttestCommand: {\n\t\t\taliases: [`test-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `t`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that runs the tests for the library's public API.`,\n\t\t\texample: `--testCommand=\"npm run test\"`,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\taliases: [`certify-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `c`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that determines whether a major version bump for your package is indicated in the workspace. Exit code 0 indicates that a major version bump is indicated, and exit code 1 indicates that no major version bump is indicated.`,\n\t\t\texample: `--certifyCommand=\\tsx scripts/certify-major-version-bump.node`,\n\t\t},\n\t\tbaseDirname: {\n\t\t\taliases: [`base-dir`, `base-dirname`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `d`,\n\t\t\trequired: false,\n\t\t\tdescription: `The base directory to run the command from.`,\n\t\t\texample: `--baseDirname=./packages/my-package`,\n\t\t},\n\t\tverbose: {\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `v`,\n\t\t\trequired: false,\n\t\t\tdescription: `Prints out more information about the process.`,\n\t\t\texample: `--verbose`,\n\t\t\tparse: parseBooleanOption,\n\t\t},\n\t},\n) satisfies OptionsGroup<BreakageOptions>\n\nconst SCHEMA_MANUAL = options(\n\t`Create a copy of the schema for configuring breakage.`,\n\ttype({ \"outdir?\": `string` }),\n\t{\n\t\toutdir: {\n\t\t\taliases: [`out-dir`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `o`,\n\t\t\trequired: false,\n\t\t\tdescription: `Directory to write the schema to.`,\n\t\t\texample: `--outdir=./dist`,\n\t\t},\n\t},\n)\n\nconst parse = cli(\n\t{\n\t\tcliName: `breakage`,\n\t\troutes: optional({ help: null, schema: null, $configPath: null }),\n\t\trouteOptions: {\n\t\t\t\"\": BREAKAGE_MANUAL,\n\t\t\t$configPath: BREAKAGE_MANUAL,\n\t\t\thelp: noOptions(`Show usage.`),\n\t\t\tschema: SCHEMA_MANUAL,\n\t\t},\n\t\tpositionalCompletions: { $configPath: { fileSystem: `files` } },\n\t\tdiscoverConfigPath: (args: readonly string[]) => {\n\t\t\tif (args[0] === `help` || args[0] === `schema`) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst configPath =\n\t\t\t\targs[0] ?? path.join(process.cwd(), `breakage.config.json`)\n\t\t\treturn configPath\n\t\t},\n\t},\n\t{\n\t\tinfo: (message, ...data) => logger.info(`breakage`, message, data),\n\t\terror: (message, ...data) => logger.error(`breakage`, message, data),\n\t},\n)\n\nconst completion = await completionResponse(parse.definition, process.argv)\nif (completion !== undefined) {\n\tprocess.stdout.write(completion)\n} else {\n\tconst { inputs, warnings, writeJsonSchema } = parse(process.argv)\n\tlogWarnings(warnings)\n\n\tswitch (inputs.case) {\n\t\tcase `help`:\n\t\t\tconsole.log(help(parse.definition))\n\t\t\tbreak\n\t\tcase `schema`:\n\t\t\t{\n\t\t\t\tconst { outdir } = inputs.opts\n\t\t\t\twriteJsonSchema(outdir ?? `.`)\n\t\t\t\tprocess.stdout.write(`📝 Wrote json.schema.`)\n\t\t\t}\n\t\t\tbreak\n\t\tcase ``:\n\t\tcase `$configPath`: {\n\t\t\tconst { returnValue } = await encapsulate(() => breakage(inputs.opts), {\n\t\t\t\tconsole: true,\n\t\t\t\tstdout: true,\n\t\t\t})\n\t\t\tif (`testResult` in returnValue && returnValue.breakingChangesFound) {\n\t\t\t\tprocess.stdout.write(returnValue.testResult)\n\t\t\t}\n\t\t\tif (`breakingChangesFound` in returnValue) {\n\t\t\t\tif (returnValue.breakingChangesFound) {\n\t\t\t\t\tif (returnValue.breakingChangesCertified) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`👷 Breaking changes were found and certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(0)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`❌ Breaking changes were found, but not certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(1)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.stdout.write(`✅ No breaking changes were found.`)\n\t\t\t\t\tprocess.exit(0)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t`💥 Breakage failed to determine breaking changes.` +\n\t\t\t\t\t\t`\\n` +\n\t\t\t\t\t\tinspect(returnValue),\n\t\t\t\t)\n\t\t\t\tprocess.exit(2)\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"breakage.x.js","names":["path"],"sources":["../src/breakage.x.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as path from \"node:path\"\nimport { inspect } from \"node:util\"\n\nimport { type } from \"arktype\"\nimport type { OptionsGroup } from \"comline\"\nimport {\n\tcli,\n\tcompletionResponse,\n\tencapsulate,\n\thelp,\n\tlogWarnings,\n\tnoOptions,\n\toptional,\n\toptions,\n\tparseBooleanOption,\n} from \"comline\"\nimport logger from \"takua\"\n\nimport type { BreakageOptions } from \"./breakage.ts\"\nimport { breakage, breakagePrelude } from \"./breakage.ts\"\n\nconst BREAKAGE_MANUAL = options(\n\t`Check for breaking changes in a package.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\t\"baselineFile?\": `string`,\n\t\ttestPattern: `string`,\n\t\ttestCommand: `string`,\n\t\tcertifyCommand: `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\ttagPattern: {\n\t\t\taliases: [`tag-pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `g`,\n\t\t\trequired: false,\n\t\t\tdescription: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,\n\t\t\texample: `--tagPattern=\"my-library\"`,\n\t\t},\n\t\tbaselineFile: {\n\t\t\taliases: [`baseline-file`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `files` },\n\t\t\trequired: false,\n\t\t\tdescription: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,\n\t\t\texample: `--baseline-file=.breakage/baseline.json`,\n\t\t},\n\t\ttestPattern: {\n\t\t\taliases: [`test-pattern`, `pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `p`,\n\t\t\trequired: true,\n\t\t\tdescription: `The pattern to match test files that test the public API of the library.`,\n\t\t\texample: `--pattern=\"*__public.test.ts\"`,\n\t\t},\n\t\ttestCommand: {\n\t\t\taliases: [`test-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `t`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that runs the tests for the library's public API.`,\n\t\t\texample: `--testCommand=\"npm run test\"`,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\taliases: [`certify-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `c`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that determines whether a major version bump for your package is indicated in the workspace. Exit code 0 indicates that a major version bump is indicated, and exit code 1 indicates that no major version bump is indicated.`,\n\t\t\texample: `--certifyCommand=\\tsx scripts/certify-major-version-bump.node`,\n\t\t},\n\t\tbaseDirname: {\n\t\t\taliases: [`base-dir`, `base-dirname`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `d`,\n\t\t\trequired: false,\n\t\t\tdescription: `The base directory to run the command from.`,\n\t\t\texample: `--baseDirname=./packages/my-package`,\n\t\t},\n\t\tverbose: {\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `v`,\n\t\t\trequired: false,\n\t\t\tdescription: `Prints out more information about the process.`,\n\t\t\texample: `--verbose`,\n\t\t\tparse: parseBooleanOption,\n\t\t},\n\t},\n) satisfies OptionsGroup<BreakageOptions>\n\nconst PRELUDE_MANUAL = options(\n\t`Resolve and fetch the release baseline for a cacheable compatibility check.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"out?\": `string`,\n\t\t\"testPattern?\": `string`,\n\t\t\"testCommand?\": `string`,\n\t\t\"certifyCommand?\": `string`,\n\t\t\"baselineFile?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\t...BREAKAGE_MANUAL.optionConfigs,\n\t\ttestPattern: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.testPattern,\n\t\t\trequired: false,\n\t\t},\n\t\ttestCommand: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.testCommand,\n\t\t\trequired: false,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.certifyCommand,\n\t\t\trequired: false,\n\t\t},\n\t\tout: {\n\t\t\tcompletion: { repeatable: false, fileSystem: `files` },\n\t\t\trequired: false,\n\t\t\tdescription: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,\n\t\t\texample: `--out=.breakage/baseline.json`,\n\t\t},\n\t},\n)\n\nconst SCHEMA_MANUAL = options(\n\t`Create a copy of the schema for configuring breakage.`,\n\ttype({ \"outdir?\": `string` }),\n\t{\n\t\toutdir: {\n\t\t\taliases: [`out-dir`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `o`,\n\t\t\trequired: false,\n\t\t\tdescription: `Directory to write the schema to.`,\n\t\t\texample: `--outdir=./dist`,\n\t\t},\n\t},\n)\n\nconst parse = cli(\n\t{\n\t\tcliName: `breakage`,\n\t\troutes: optional({\n\t\t\thelp: null,\n\t\t\tschema: null,\n\t\t\tprelude: optional({ $configPath: null }),\n\t\t\t$configPath: null,\n\t\t}),\n\t\trouteOptions: {\n\t\t\t\"\": BREAKAGE_MANUAL,\n\t\t\t$configPath: BREAKAGE_MANUAL,\n\t\t\thelp: noOptions(`Show usage.`),\n\t\t\tschema: SCHEMA_MANUAL,\n\t\t\tprelude: PRELUDE_MANUAL,\n\t\t\t\"prelude/$configPath\": PRELUDE_MANUAL,\n\t\t},\n\t\tpositionalCompletions: {\n\t\t\t$configPath: { fileSystem: `files` },\n\t\t\t\"prelude/$configPath\": { fileSystem: `files` },\n\t\t},\n\t\tdiscoverConfigPath: (args: readonly string[]) => {\n\t\t\tif (args[0] === `help` || args[0] === `schema`) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst configPath =\n\t\t\t\t(args[0] === `prelude` ? args[1] : args[0]) ??\n\t\t\t\tpath.join(process.cwd(), `breakage.config.json`)\n\t\t\treturn configPath\n\t\t},\n\t},\n\t{\n\t\tinfo: (message, ...data) => logger.info(`breakage`, message, data),\n\t\terror: (message, ...data) => logger.error(`breakage`, message, data),\n\t},\n)\n\nconst completion = await completionResponse(parse.definition, process.argv)\nif (completion !== undefined) {\n\tprocess.stdout.write(completion)\n} else {\n\tconst { inputs, warnings, writeJsonSchema } = parse(process.argv)\n\tlogWarnings(warnings)\n\n\tswitch (inputs.case) {\n\t\tcase `help`:\n\t\t\tconsole.log(help(parse.definition))\n\t\t\tbreak\n\t\tcase `schema`:\n\t\t\t{\n\t\t\t\tconst { outdir } = inputs.opts\n\t\t\t\twriteJsonSchema(outdir ?? `.`)\n\t\t\t\tprocess.stdout.write(`📝 Wrote json.schema.`)\n\t\t\t}\n\t\t\tbreak\n\t\tcase `prelude`:\n\t\tcase `prelude/$configPath`: {\n\t\t\tconst baseline = await breakagePrelude({\n\t\t\t\t...inputs.opts,\n\t\t\t\tout: inputs.opts.out ?? `.breakage/baseline.json`,\n\t\t\t})\n\t\t\tprocess.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\\n`)\n\t\t\tbreak\n\t\t}\n\t\tcase ``:\n\t\tcase `$configPath`: {\n\t\t\tconst { returnValue } = await encapsulate(() => breakage(inputs.opts), {\n\t\t\t\tconsole: true,\n\t\t\t\tstdout: true,\n\t\t\t})\n\t\t\tif (`testResult` in returnValue && returnValue.breakingChangesFound) {\n\t\t\t\tprocess.stdout.write(returnValue.testResult)\n\t\t\t}\n\t\t\tif (`breakingChangesFound` in returnValue) {\n\t\t\t\tif (returnValue.breakingChangesFound) {\n\t\t\t\t\tif (returnValue.breakingChangesCertified) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`👷 Breaking changes were found and certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(0)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`❌ Breaking changes were found, but not certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(1)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.stdout.write(`✅ No breaking changes were found.`)\n\t\t\t\t\tprocess.exit(0)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t`💥 Breakage failed to determine breaking changes.` +\n\t\t\t\t\t\t`\\n` +\n\t\t\t\t\t\tinspect(returnValue),\n\t\t\t\t)\n\t\t\t\tprocess.exit(2)\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,52 +1,51 @@
1
1
  {
2
- "name": "breakage",
3
- "version": "0.0.0",
4
- "description": "Run yesterday’s tests against tomorrow’s release.",
5
- "homepage": "https://github.com/jeremybanka/breakage#readme",
6
- "bugs": "https://github.com/jeremybanka/breakage/issues",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/jeremybanka/breakage.git",
10
- "directory": "packages/breakage"
11
- },
12
- "license": "MPL-2.0",
13
- "author": {
14
- "name": "Jeremy Banka",
15
- "email": "hello@jeremybanka.com"
16
- },
17
- "type": "module",
18
- "main": "dist/breakage.js",
19
- "types": "dist/breakage.d.ts",
20
- "bin": {
21
- "breakage": "./dist/breakage.x.js"
22
- },
23
- "files": [
24
- "dist",
25
- "src"
26
- ],
27
- "scripts": {
28
- "build": "tsdown && pnpm run schema",
29
- "check": "pnpm run check:tsc",
30
- "check:tsc": "tsc --noEmit",
31
- "postversion": "dprint fmt package.json",
32
- "schema": "bun src/breakage.x.ts --outdir=dist -- schema",
33
- "pretest": "pnpm run build",
34
- "test": "bun test -- ./__tests__",
35
- "test:watch": "bun test --watch -- ./__tests__"
36
- },
37
- "dependencies": {
38
- "arktype": "2.2.7",
39
- "comline": "0.9.0",
40
- "minimatch": "10.2.6",
41
- "simple-git": "4.0.2",
42
- "takua": "0.2.3"
43
- },
44
- "devDependencies": {
45
- "@types/bun": "npm:bun-types@1.4.2",
46
- "tsdown": "0.23.0",
47
- "typescript": "7.0.2"
48
- },
49
- "publishConfig": {
50
- "access": "public"
51
- }
52
- }
2
+ "name": "breakage",
3
+ "version": "0.8.2",
4
+ "description": "Run yesterday’s tests against tomorrow’s release.",
5
+ "homepage": "https://github.com/jeremybanka/breakage#readme",
6
+ "bugs": "https://github.com/jeremybanka/breakage/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/jeremybanka/breakage.git",
10
+ "directory": "packages/breakage"
11
+ },
12
+ "license": "MPL-2.0",
13
+ "author": {
14
+ "name": "Jeremy Banka",
15
+ "email": "hello@jeremybanka.com"
16
+ },
17
+ "type": "module",
18
+ "main": "dist/breakage.js",
19
+ "types": "dist/breakage.d.ts",
20
+ "bin": {
21
+ "breakage": "./dist/breakage.x.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "dependencies": {
28
+ "arktype": "2.2.8",
29
+ "comline": "0.9.0",
30
+ "minimatch": "10.2.6",
31
+ "simple-git": "4.0.2",
32
+ "takua": "0.2.3"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "npm:bun-types@1.4.3",
36
+ "typescript": "7.0.2"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "vp pack && pnpm run schema",
43
+ "check": "pnpm run check:tsc",
44
+ "check:tsc": "tsc --noEmit",
45
+ "postversion": "dprint fmt package.json",
46
+ "schema": "bun src/breakage.x.ts --outdir=dist -- schema",
47
+ "pretest": "pnpm run build",
48
+ "test": "bun test -- ./__tests__",
49
+ "test:watch": "bun test --watch -- ./__tests__"
50
+ }
51
+ }
@@ -0,0 +1,143 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
3
+ import path from "node:path"
4
+
5
+ import type { SimpleGit } from "simple-git"
6
+ import { simpleGit } from "simple-git"
7
+
8
+ import { withDirectoryLock } from "./directory-lock.ts"
9
+ import { latestReleaseTag } from "./release-tag.ts"
10
+
11
+ export type BaselineOptions = {
12
+ tagPattern?: string | undefined
13
+ baseDirname?: string
14
+ }
15
+
16
+ export type BreakagePreludeOptions = BaselineOptions & { out: string }
17
+
18
+ export type BreakageBaseline = {
19
+ version: 1
20
+ tagPattern: string | null
21
+ packageDirectory: string
22
+ ref: string
23
+ commit: string
24
+ }
25
+
26
+ const objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/
27
+
28
+ function isBaseline(value: unknown): value is BreakageBaseline {
29
+ if (typeof value !== `object` || value === null) return false
30
+ const record = value as Partial<BreakageBaseline>
31
+ return (
32
+ record.version === 1 &&
33
+ (record.tagPattern === null || typeof record.tagPattern === `string`) &&
34
+ typeof record.packageDirectory === `string` &&
35
+ typeof record.ref === `string` &&
36
+ record.ref.startsWith(`refs/tags/`) &&
37
+ typeof record.commit === `string` &&
38
+ objectId.test(record.commit)
39
+ )
40
+ }
41
+
42
+ /** Resolve and fetch a release without running tests or changing the worktree. */
43
+ export async function breakagePrelude({
44
+ out,
45
+ tagPattern,
46
+ baseDirname = process.cwd(),
47
+ }: BreakagePreludeOptions): Promise<BreakageBaseline> {
48
+ const filename = path.resolve(baseDirname, out)
49
+ const temporaryPath = `${filename}.${randomUUID()}.tmp`
50
+ let canInvalidate = false
51
+ try {
52
+ const git = simpleGit(baseDirname)
53
+ // An ignored, untracked snapshot cannot make the compatibility check dirty.
54
+ if (!(await git.raw([`check-ignore`, `--`, filename])).trim()) {
55
+ throw new Error(`Baseline output must be gitignored: ${filename}.`)
56
+ }
57
+ canInvalidate = true
58
+ const remoteTags = await git.listRemote([`--tags`, `origin`])
59
+ const ref = latestReleaseTag(remoteTags, tagPattern)
60
+ if (!ref) {
61
+ throw new Error(`No tags found matching the pattern "${tagPattern}".`)
62
+ }
63
+ const refs = new Map(
64
+ remoteTags
65
+ .trim()
66
+ .split(`\n`)
67
+ .map((line) => {
68
+ const [sha, name] = line.split(`\t`)
69
+ return [name, sha]
70
+ }),
71
+ )
72
+ // ls-remote supplies the peeled commit for annotated tags.
73
+ const commit = refs.get(`${ref}^{}`) ?? refs.get(ref)
74
+ if (!commit || !objectId.test(commit)) {
75
+ throw new Error(`Could not resolve the release commit for ${ref}.`)
76
+ }
77
+ const commonGitDirectory = await git.revparse([
78
+ `--path-format=absolute`,
79
+ `--git-common-dir`,
80
+ ])
81
+ await withDirectoryLock(
82
+ path.join(commonGitDirectory, `breakage-fetch.lock`),
83
+ () => git.fetch([`origin`, commit, `--no-tags`, `--no-write-fetch-head`]),
84
+ )
85
+ if ((await git.raw([`cat-file`, `-t`, commit])).trim() !== `commit`) {
86
+ throw new Error(`Release ${ref} does not resolve to a commit.`)
87
+ }
88
+ const baseline: BreakageBaseline = {
89
+ version: 1,
90
+ tagPattern: tagPattern ?? null,
91
+ packageDirectory: await git.revparse([`--show-prefix`]),
92
+ ref,
93
+ commit,
94
+ }
95
+ await mkdir(path.dirname(filename), { recursive: true })
96
+ await writeFile(temporaryPath, `${JSON.stringify(baseline, null, 2)}\n`, {
97
+ flag: `wx`,
98
+ })
99
+ await rename(temporaryPath, filename)
100
+ return baseline
101
+ } catch (cause) {
102
+ // A failed refresh must never leave an older snapshot available as fallback.
103
+ if (canInvalidate) await rm(filename, { force: true })
104
+ throw new Error(`Failed to prepare baseline ${filename}.`, { cause })
105
+ } finally {
106
+ await rm(temporaryPath, { force: true })
107
+ }
108
+ }
109
+
110
+ export async function readBaseline(
111
+ git: SimpleGit,
112
+ filename: string,
113
+ { tagPattern }: BaselineOptions,
114
+ ): Promise<BreakageBaseline> {
115
+ try {
116
+ const baseline: unknown = JSON.parse(await readFile(filename, `utf8`))
117
+ if (!isBaseline(baseline)) throw new Error(`Invalid baseline fields.`)
118
+ if (
119
+ baseline.tagPattern !== (tagPattern ?? null) ||
120
+ baseline.packageDirectory !== (await git.revparse([`--show-prefix`]))
121
+ ) {
122
+ throw new Error(`Baseline belongs to a different package or tag pattern.`)
123
+ }
124
+ if (
125
+ (
126
+ await git.raw([`check-ref-format`, `--normalize`, baseline.ref])
127
+ ).trim() !== baseline.ref
128
+ ) {
129
+ throw new Error(`Invalid release ref.`)
130
+ }
131
+ if (
132
+ (await git.raw([`cat-file`, `-t`, baseline.commit])).trim() !== `commit`
133
+ ) {
134
+ throw new Error(`Pinned object is not a locally available commit.`)
135
+ }
136
+ return baseline
137
+ } catch (cause) {
138
+ throw new Error(
139
+ `Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`,
140
+ { cause },
141
+ )
142
+ }
143
+ }
package/src/breakage.ts CHANGED
@@ -6,12 +6,17 @@ import { simpleGit } from "simple-git"
6
6
  import type { Chronicle } from "takua"
7
7
  import logger from "takua"
8
8
 
9
+ import { readBaseline } from "./baseline.ts"
9
10
  import { withDirectoryLock } from "./directory-lock.ts"
10
11
  import { latestReleaseTag as selectLatestReleaseTag } from "./release-tag.ts"
11
12
  import { withTestFileState } from "./test-file-state.ts"
12
13
 
14
+ export type { BreakageBaseline, BreakagePreludeOptions } from "./baseline.ts"
15
+ export { breakagePrelude } from "./baseline.ts"
16
+
13
17
  export type BreakageOptions = {
14
18
  tagPattern?: string | undefined
19
+ baselineFile?: string | undefined
15
20
  testPattern: string
16
21
  testCommand: string
17
22
  certifyCommand: string
@@ -54,6 +59,7 @@ export type BreakageOutcome =
54
59
 
55
60
  export async function breakage({
56
61
  tagPattern,
62
+ baselineFile,
57
63
  testPattern,
58
64
  testCommand,
59
65
  certifyCommand,
@@ -86,39 +92,53 @@ export async function breakage({
86
92
  gitWasClean: false,
87
93
  }
88
94
  }
89
- const tagsRemote = await git.listRemote([`--tags`, `origin`])
95
+ let latestReleaseTag: string | undefined
96
+ let releaseCommit: string
97
+ if (baselineFile !== undefined) {
98
+ const baseline = await readBaseline(
99
+ git,
100
+ path.resolve(baseDirname, baselineFile),
101
+ { tagPattern },
102
+ )
103
+ latestReleaseTag = baseline.ref
104
+ releaseCommit = baseline.commit
105
+ mark?.(`read pinned release baseline`)
106
+ } else {
107
+ const tagsRemote = await git.listRemote([`--tags`, `origin`])
90
108
 
91
- mark?.(`list remote tags`)
92
- const latestReleaseTag = selectLatestReleaseTag(tagsRemote, tagPattern)
93
- mark?.(`found latest release tag`)
94
- if (!latestReleaseTag) {
95
- return {
96
- summary: `No tags found matching the pattern "${tagPattern}".`,
97
- gitWasClean: true,
98
- lastReleaseFound: false,
99
- } as const
100
- }
101
- const commonGitDirectory = await git.revparse([
102
- `--path-format=absolute`,
103
- `--git-common-dir`,
104
- ])
105
- await withDirectoryLock(
106
- path.join(commonGitDirectory, `breakage-fetch.lock`),
107
- () =>
108
- git.fetch([
109
- `origin`,
110
- `${latestReleaseTag}:${latestReleaseTag}`,
111
- `--no-tags`,
112
- `--no-write-fetch-head`,
113
- ]),
114
- )
109
+ mark?.(`list remote tags`)
110
+ latestReleaseTag = selectLatestReleaseTag(tagsRemote, tagPattern)
111
+ mark?.(`found latest release tag`)
112
+ if (!latestReleaseTag) {
113
+ return {
114
+ summary: `No tags found matching the pattern "${tagPattern}".`,
115
+ gitWasClean: true,
116
+ lastReleaseFound: false,
117
+ } as const
118
+ }
119
+ const commonGitDirectory = await git.revparse([
120
+ `--path-format=absolute`,
121
+ `--git-common-dir`,
122
+ ])
123
+ await withDirectoryLock(
124
+ path.join(commonGitDirectory, `breakage-fetch.lock`),
125
+ () =>
126
+ git.fetch([
127
+ `origin`,
128
+ `${latestReleaseTag}:${latestReleaseTag}`,
129
+ `--no-tags`,
130
+ `--no-write-fetch-head`,
131
+ ]),
132
+ )
115
133
 
116
- mark?.(`fetched latest release tag`)
134
+ releaseCommit = await git.revparse([`${latestReleaseTag}^{commit}`])
135
+ mark?.(`fetched latest release tag`)
136
+ }
117
137
 
118
138
  let productionFiles: string[]
119
139
  try {
120
140
  productionFiles = (
121
- await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, latestReleaseTag])
141
+ await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, releaseCommit])
122
142
  )
123
143
  .split(`\0`)
124
144
  .filter(Boolean)
@@ -164,10 +184,7 @@ export async function breakage({
164
184
  testsWereFound: false,
165
185
  } as const
166
186
  }
167
- const restore = await state.replaceTests(
168
- latestReleaseTag,
169
- productionTestFiles,
170
- )
187
+ const restore = await state.replaceTests(releaseCommit, productionTestFiles)
171
188
  mark?.(`restored public tests from latest release tag`)
172
189
  return { latestReleaseTag, productionTestFiles, restore }
173
190
  })
package/src/breakage.x.ts CHANGED
@@ -19,12 +19,13 @@ import {
19
19
  import logger from "takua"
20
20
 
21
21
  import type { BreakageOptions } from "./breakage.ts"
22
- import { breakage } from "./breakage.ts"
22
+ import { breakage, breakagePrelude } from "./breakage.ts"
23
23
 
24
24
  const BREAKAGE_MANUAL = options(
25
25
  `Check for breaking changes in a package.`,
26
26
  type({
27
27
  "tagPattern?": `string`,
28
+ "baselineFile?": `string`,
28
29
  testPattern: `string`,
29
30
  testCommand: `string`,
30
31
  certifyCommand: `string`,
@@ -40,6 +41,13 @@ const BREAKAGE_MANUAL = options(
40
41
  description: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,
41
42
  example: `--tagPattern="my-library"`,
42
43
  },
44
+ baselineFile: {
45
+ aliases: [`baseline-file`],
46
+ completion: { repeatable: false, fileSystem: `files` },
47
+ required: false,
48
+ description: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,
49
+ example: `--baseline-file=.breakage/baseline.json`,
50
+ },
43
51
  testPattern: {
44
52
  aliases: [`test-pattern`, `pattern`],
45
53
  completion: { repeatable: false },
@@ -83,6 +91,41 @@ const BREAKAGE_MANUAL = options(
83
91
  },
84
92
  ) satisfies OptionsGroup<BreakageOptions>
85
93
 
94
+ const PRELUDE_MANUAL = options(
95
+ `Resolve and fetch the release baseline for a cacheable compatibility check.`,
96
+ type({
97
+ "tagPattern?": `string`,
98
+ "baseDirname?": `string`,
99
+ "out?": `string`,
100
+ "testPattern?": `string`,
101
+ "testCommand?": `string`,
102
+ "certifyCommand?": `string`,
103
+ "baselineFile?": `string`,
104
+ "verbose?": `boolean`,
105
+ }),
106
+ {
107
+ ...BREAKAGE_MANUAL.optionConfigs,
108
+ testPattern: {
109
+ ...BREAKAGE_MANUAL.optionConfigs.testPattern,
110
+ required: false,
111
+ },
112
+ testCommand: {
113
+ ...BREAKAGE_MANUAL.optionConfigs.testCommand,
114
+ required: false,
115
+ },
116
+ certifyCommand: {
117
+ ...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
118
+ required: false,
119
+ },
120
+ out: {
121
+ completion: { repeatable: false, fileSystem: `files` },
122
+ required: false,
123
+ description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
124
+ example: `--out=.breakage/baseline.json`,
125
+ },
126
+ },
127
+ )
128
+
86
129
  const SCHEMA_MANUAL = options(
87
130
  `Create a copy of the schema for configuring breakage.`,
88
131
  type({ "outdir?": `string` }),
@@ -101,20 +144,31 @@ const SCHEMA_MANUAL = options(
101
144
  const parse = cli(
102
145
  {
103
146
  cliName: `breakage`,
104
- routes: optional({ help: null, schema: null, $configPath: null }),
147
+ routes: optional({
148
+ help: null,
149
+ schema: null,
150
+ prelude: optional({ $configPath: null }),
151
+ $configPath: null,
152
+ }),
105
153
  routeOptions: {
106
154
  "": BREAKAGE_MANUAL,
107
155
  $configPath: BREAKAGE_MANUAL,
108
156
  help: noOptions(`Show usage.`),
109
157
  schema: SCHEMA_MANUAL,
158
+ prelude: PRELUDE_MANUAL,
159
+ "prelude/$configPath": PRELUDE_MANUAL,
160
+ },
161
+ positionalCompletions: {
162
+ $configPath: { fileSystem: `files` },
163
+ "prelude/$configPath": { fileSystem: `files` },
110
164
  },
111
- positionalCompletions: { $configPath: { fileSystem: `files` } },
112
165
  discoverConfigPath: (args: readonly string[]) => {
113
166
  if (args[0] === `help` || args[0] === `schema`) {
114
167
  return
115
168
  }
116
169
  const configPath =
117
- args[0] ?? path.join(process.cwd(), `breakage.config.json`)
170
+ (args[0] === `prelude` ? args[1] : args[0]) ??
171
+ path.join(process.cwd(), `breakage.config.json`)
118
172
  return configPath
119
173
  },
120
174
  },
@@ -142,6 +196,15 @@ if (completion !== undefined) {
142
196
  process.stdout.write(`📝 Wrote json.schema.`)
143
197
  }
144
198
  break
199
+ case `prelude`:
200
+ case `prelude/$configPath`: {
201
+ const baseline = await breakagePrelude({
202
+ ...inputs.opts,
203
+ out: inputs.opts.out ?? `.breakage/baseline.json`,
204
+ })
205
+ process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`)
206
+ break
207
+ }
145
208
  case ``:
146
209
  case `$configPath`: {
147
210
  const { returnValue } = await encapsulate(() => breakage(inputs.opts), {