breakage 0.0.0-stage → 0.0.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.
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "additionalProperties": false,
4
+ "type": "object",
5
+ "properties": {
6
+ "certifyCommand": {
7
+ "type": "string"
8
+ },
9
+ "testCommand": {
10
+ "type": "string"
11
+ },
12
+ "testPattern": {
13
+ "type": "string"
14
+ },
15
+ "baseDirname": {
16
+ "type": "string"
17
+ },
18
+ "tagPattern": {
19
+ "type": "string"
20
+ },
21
+ "verbose": {
22
+ "type": "boolean"
23
+ }
24
+ },
25
+ "required": [
26
+ "certifyCommand",
27
+ "testCommand",
28
+ "testPattern"
29
+ ]
30
+ }
@@ -0,0 +1,43 @@
1
+ //#region src/breakage.d.ts
2
+ export type BreakageOptions = {
3
+ tagPattern?: string | undefined;
4
+ testPattern: string;
5
+ testCommand: string;
6
+ certifyCommand: string;
7
+ baseDirname?: string;
8
+ verbose?: boolean | undefined;
9
+ };
10
+ export type BreakageOutcome = {
11
+ gitWasClean: false;
12
+ } | {
13
+ gitWasClean: true;
14
+ lastReleaseFound: false;
15
+ } | {
16
+ gitWasClean: true;
17
+ lastReleaseFound: true;
18
+ lastReleaseTag: string;
19
+ testsWereFound: false;
20
+ } | {
21
+ gitWasClean: true;
22
+ lastReleaseFound: true;
23
+ lastReleaseTag: string;
24
+ testsWereFound: true;
25
+ testsFound: string[];
26
+ breakingChangesFound: false;
27
+ } | {
28
+ gitWasClean: true;
29
+ lastReleaseFound: true;
30
+ lastReleaseTag: string;
31
+ testsWereFound: true;
32
+ testsFound: string[];
33
+ breakingChangesFound: true;
34
+ testResult: string;
35
+ breakingChangesCertified: boolean;
36
+ certificationStdout: string;
37
+ certificationStderr: string;
38
+ };
39
+ export declare function breakage({ tagPattern, testPattern, testCommand, certifyCommand, baseDirname, verbose }: BreakageOptions): Promise<BreakageOutcome & {
40
+ summary: string;
41
+ }>;
42
+ //#endregion
43
+ //# sourceMappingURL=breakage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breakage.d.ts","names":[],"sources":["../src/breakage.ts"],"mappings":";YAYY;EACX;EACA;EACA;EACA;EACA;EACA;;YAGW;EACP;;EAEF;EACA;;EAGA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;wBAGmB,WACrB,YACA,aACA,aACA,gBACA,aACA,WACE,kBAAkB,QAAQ;EAAoB"}
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "additionalProperties": false,
4
+ "type": "object",
5
+ "properties": {}
6
+ }
@@ -0,0 +1 @@
1
+ export { t as breakage } from "./breakage-D_nc-zEK.js";
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "additionalProperties": false,
4
+ "type": "object",
5
+ "properties": {
6
+ "certifyCommand": {
7
+ "type": "string"
8
+ },
9
+ "testCommand": {
10
+ "type": "string"
11
+ },
12
+ "testPattern": {
13
+ "type": "string"
14
+ },
15
+ "baseDirname": {
16
+ "type": "string"
17
+ },
18
+ "tagPattern": {
19
+ "type": "string"
20
+ },
21
+ "verbose": {
22
+ "type": "boolean"
23
+ }
24
+ },
25
+ "required": [
26
+ "certifyCommand",
27
+ "testCommand",
28
+ "testPattern"
29
+ ]
30
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "additionalProperties": false,
4
+ "type": "object",
5
+ "properties": {
6
+ "outdir": {
7
+ "type": "string"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ import { t as breakage } from "./breakage-D_nc-zEK.js";
3
+ import * as path$1 from "node:path";
4
+ import logger from "takua";
5
+ import { inspect } from "node:util";
6
+ import { type } from "arktype";
7
+ import { cli, completionResponse, encapsulate, help, logWarnings, noOptions, optional, options, parseBooleanOption } from "comline";
8
+ //#region src/breakage.x.ts
9
+ const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type({
10
+ "tagPattern?": `string`,
11
+ testPattern: `string`,
12
+ testCommand: `string`,
13
+ certifyCommand: `string`,
14
+ "baseDirname?": `string`,
15
+ "verbose?": `boolean`
16
+ }), {
17
+ tagPattern: {
18
+ aliases: [`tag-pattern`],
19
+ completion: { repeatable: false },
20
+ flag: `g`,
21
+ required: false,
22
+ description: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,
23
+ example: `--tagPattern="my-library"`
24
+ },
25
+ testPattern: {
26
+ aliases: [`test-pattern`, `pattern`],
27
+ completion: { repeatable: false },
28
+ flag: `p`,
29
+ required: true,
30
+ description: `The pattern to match test files that test the public API of the library.`,
31
+ example: `--pattern="*__public.test.ts"`
32
+ },
33
+ testCommand: {
34
+ aliases: [`test-command`],
35
+ completion: { repeatable: false },
36
+ flag: `t`,
37
+ required: true,
38
+ description: `Complete bash command that runs the tests for the library's public API.`,
39
+ example: `--testCommand="npm run test"`
40
+ },
41
+ certifyCommand: {
42
+ aliases: [`certify-command`],
43
+ completion: { repeatable: false },
44
+ flag: `c`,
45
+ required: true,
46
+ 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.`,
47
+ example: `--certifyCommand=\tsx scripts/certify-major-version-bump.node`
48
+ },
49
+ baseDirname: {
50
+ aliases: [`base-dir`, `base-dirname`],
51
+ completion: {
52
+ repeatable: false,
53
+ fileSystem: `directories`
54
+ },
55
+ flag: `d`,
56
+ required: false,
57
+ description: `The base directory to run the command from.`,
58
+ example: `--baseDirname=./packages/my-package`
59
+ },
60
+ verbose: {
61
+ completion: { repeatable: false },
62
+ flag: `v`,
63
+ required: false,
64
+ description: `Prints out more information about the process.`,
65
+ example: `--verbose`,
66
+ parse: parseBooleanOption
67
+ }
68
+ });
69
+ const SCHEMA_MANUAL = options(`Create a copy of the schema for configuring breakage.`, type({ "outdir?": `string` }), { outdir: {
70
+ aliases: [`out-dir`],
71
+ completion: {
72
+ repeatable: false,
73
+ fileSystem: `directories`
74
+ },
75
+ flag: `o`,
76
+ required: false,
77
+ description: `Directory to write the schema to.`,
78
+ example: `--outdir=./dist`
79
+ } });
80
+ const parse = cli({
81
+ cliName: `breakage`,
82
+ routes: optional({
83
+ help: null,
84
+ schema: null,
85
+ $configPath: null
86
+ }),
87
+ routeOptions: {
88
+ "": BREAKAGE_MANUAL,
89
+ $configPath: BREAKAGE_MANUAL,
90
+ help: noOptions(`Show usage.`),
91
+ schema: SCHEMA_MANUAL
92
+ },
93
+ positionalCompletions: { $configPath: { fileSystem: `files` } },
94
+ discoverConfigPath: (args) => {
95
+ if (args[0] === `help` || args[0] === `schema`) return;
96
+ return args[0] ?? path$1.join(process.cwd(), `breakage.config.json`);
97
+ }
98
+ }, {
99
+ info: (message, ...data) => logger.info(`breakage`, message, data),
100
+ error: (message, ...data) => logger.error(`breakage`, message, data)
101
+ });
102
+ const completion = await completionResponse(parse.definition, process.argv);
103
+ if (completion !== void 0) process.stdout.write(completion);
104
+ else {
105
+ const { inputs, warnings, writeJsonSchema } = parse(process.argv);
106
+ logWarnings(warnings);
107
+ switch (inputs.case) {
108
+ case `help`:
109
+ console.log(help(parse.definition));
110
+ break;
111
+ case `schema`:
112
+ {
113
+ const { outdir } = inputs.opts;
114
+ writeJsonSchema(outdir ?? `.`);
115
+ process.stdout.write(`📝 Wrote json.schema.`);
116
+ }
117
+ break;
118
+ case ``:
119
+ case `$configPath`: {
120
+ const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
121
+ console: true,
122
+ stdout: true
123
+ });
124
+ if (`testResult` in returnValue && returnValue.breakingChangesFound) process.stdout.write(returnValue.testResult);
125
+ if (`breakingChangesFound` in returnValue) {
126
+ if (returnValue.breakingChangesFound) {
127
+ if (returnValue.breakingChangesCertified) {
128
+ process.stdout.write(`👷 Breaking changes were found and certified.`);
129
+ process.exit(0);
130
+ } else {
131
+ process.stdout.write(`❌ Breaking changes were found, but not certified.`);
132
+ process.exit(1);
133
+ }
134
+ } else {
135
+ process.stdout.write(`✅ No breaking changes were found.`);
136
+ process.exit(0);
137
+ }
138
+ } else {
139
+ process.stdout.write("💥 Breakage failed to determine breaking changes.\n" + inspect(returnValue));
140
+ process.exit(2);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ //#endregion
146
+ export {};
147
+
148
+ //# sourceMappingURL=breakage.x.js.map
@@ -0,0 +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":";;;;;;;;AAuBA,MAAM,kBAAkB,QACvB,4CACA,KAAK;CACJ,eAAe;CACf,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,gBAAgB;CAChB,YAAY;AACb,CAAC,GACD;CACC,YAAY;EACX,SAAS,CAAC,aAAa;EACvB,YAAY,EAAE,YAAY,MAAM;EAChC,MAAM;EACN,UAAU;EACV,aAAa;EACb,SAAS;CACV;CACA,aAAa;EACZ,SAAS,CAAC,gBAAgB,SAAS;EACnC,YAAY,EAAE,YAAY,MAAM;EAChC,MAAM;EACN,UAAU;EACV,aAAa;EACb,SAAS;CACV;CACA,aAAa;EACZ,SAAS,CAAC,cAAc;EACxB,YAAY,EAAE,YAAY,MAAM;EAChC,MAAM;EACN,UAAU;EACV,aAAa;EACb,SAAS;CACV;CACA,gBAAgB;EACf,SAAS,CAAC,iBAAiB;EAC3B,YAAY,EAAE,YAAY,MAAM;EAChC,MAAM;EACN,UAAU;EACV,aAAa;EACb,SAAS;CACV;CACA,aAAa;EACZ,SAAS,CAAC,YAAY,cAAc;EACpC,YAAY;GAAE,YAAY;GAAO,YAAY;EAAc;EAC3D,MAAM;EACN,UAAU;EACV,aAAa;EACb,SAAS;CACV;CACA,SAAS;EACR,YAAY,EAAE,YAAY,MAAM;EAChC,MAAM;EACN,UAAU;EACV,aAAa;EACb,SAAS;EACT,OAAO;CACR;AACD,CACD;AAEA,MAAM,gBAAgB,QACrB,yDACA,KAAK,EAAE,WAAW,SAAS,CAAC,GAC5B,EACC,QAAQ;CACP,SAAS,CAAC,SAAS;CACnB,YAAY;EAAE,YAAY;EAAO,YAAY;CAAc;CAC3D,MAAM;CACN,UAAU;CACV,aAAa;CACb,SAAS;AACV,EACD,CACD;AAEA,MAAM,QAAQ,IACb;CACC,SAAS;CACT,QAAQ,SAAS;EAAE,MAAM;EAAM,QAAQ;EAAM,aAAa;CAAK,CAAC;CAChE,cAAc;EACb,IAAI;EACJ,aAAa;EACb,MAAM,UAAU,aAAa;EAC7B,QAAQ;CACT;CACA,uBAAuB,EAAE,aAAa,EAAE,YAAY,QAAQ,EAAE;CAC9D,qBAAqB,SAA4B;EAChD,IAAI,KAAK,OAAO,UAAU,KAAK,OAAO,UACrC;EAID,OADC,KAAK,MAAMA,OAAK,KAAK,QAAQ,IAAI,GAAG,sBAAsB;CAE5D;AACD,GACA;CACC,OAAO,SAAS,GAAG,SAAS,OAAO,KAAK,YAAY,SAAS,IAAI;CACjE,QAAQ,SAAS,GAAG,SAAS,OAAO,MAAM,YAAY,SAAS,IAAI;AACpE,CACD;AAEA,MAAM,aAAa,MAAM,mBAAmB,MAAM,YAAY,QAAQ,IAAI;AAC1E,IAAI,eAAe,KAAA,GAClB,QAAQ,OAAO,MAAM,UAAU;KACzB;CACN,MAAM,EAAE,QAAQ,UAAU,oBAAoB,MAAM,QAAQ,IAAI;CAChE,YAAY,QAAQ;CAEpB,QAAQ,OAAO,MAAf;EACC,KAAK;GACJ,QAAQ,IAAI,KAAK,MAAM,UAAU,CAAC;GAClC;EACD,KAAK;GACJ;IACC,MAAM,EAAE,WAAW,OAAO;IAC1B,gBAAgB,UAAU,GAAG;IAC7B,QAAQ,OAAO,MAAM,uBAAuB;GAC7C;GACA;EACD,KAAK;EACL,KAAK,eAAe;GACnB,MAAM,EAAE,gBAAgB,MAAM,kBAAkB,SAAS,OAAO,IAAI,GAAG;IACtE,SAAS;IACT,QAAQ;GACT,CAAC;GACD,IAAI,gBAAgB,eAAe,YAAY,sBAC9C,QAAQ,OAAO,MAAM,YAAY,UAAU;GAE5C,IAAI,0BAA0B,aAAa;IAC1C,IAAI,YAAY,sBAAsB;KACrC,IAAI,YAAY,0BAA0B;MACzC,QAAQ,OAAO,MACd,+CACD;MACA,QAAQ,KAAK,CAAC;KACf,OAAO;MACN,QAAQ,OAAO,MACd,mDACD;MACA,QAAQ,KAAK,CAAC;KACf;IACD,OAAO;KACN,QAAQ,OAAO,MAAM,mCAAmC;KACxD,QAAQ,KAAK,CAAC;IACf;GACD,OAAO;IACN,QAAQ,OAAO,MACd,wDAEC,QAAQ,WAAW,CACrB;IACA,QAAQ,KAAK,CAAC;GACf;EACD;CACD;AACD"}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
- "name": "breakage",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
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
+ }
@@ -0,0 +1,238 @@
1
+ import { exec } from "node:child_process"
2
+ import path from "node:path"
3
+
4
+ import { minimatch } from "minimatch"
5
+ import { simpleGit } from "simple-git"
6
+ import type { Chronicle } from "takua"
7
+ import logger from "takua"
8
+
9
+ import { withDirectoryLock } from "./directory-lock.ts"
10
+ import { latestReleaseTag as selectLatestReleaseTag } from "./release-tag.ts"
11
+ import { withTestFileState } from "./test-file-state.ts"
12
+
13
+ export type BreakageOptions = {
14
+ tagPattern?: string | undefined
15
+ testPattern: string
16
+ testCommand: string
17
+ certifyCommand: string
18
+ baseDirname?: string
19
+ verbose?: boolean | undefined
20
+ }
21
+
22
+ export type BreakageOutcome =
23
+ | { gitWasClean: false }
24
+ | {
25
+ gitWasClean: true
26
+ lastReleaseFound: false
27
+ }
28
+ | {
29
+ gitWasClean: true
30
+ lastReleaseFound: true
31
+ lastReleaseTag: string
32
+ testsWereFound: false
33
+ }
34
+ | {
35
+ gitWasClean: true
36
+ lastReleaseFound: true
37
+ lastReleaseTag: string
38
+ testsWereFound: true
39
+ testsFound: string[]
40
+ breakingChangesFound: false
41
+ }
42
+ | {
43
+ gitWasClean: true
44
+ lastReleaseFound: true
45
+ lastReleaseTag: string
46
+ testsWereFound: true
47
+ testsFound: string[]
48
+ breakingChangesFound: true
49
+ testResult: string
50
+ breakingChangesCertified: boolean
51
+ certificationStdout: string
52
+ certificationStderr: string
53
+ }
54
+
55
+ export async function breakage({
56
+ tagPattern,
57
+ testPattern,
58
+ testCommand,
59
+ certifyCommand,
60
+ baseDirname = process.cwd(),
61
+ verbose = false,
62
+ }: BreakageOptions): Promise<BreakageOutcome & { summary: string }> {
63
+ let mark: Chronicle[`mark`] | undefined
64
+ let logMarks: Chronicle[`logMarks`] | undefined
65
+ if (verbose) {
66
+ const chronicle = logger.makeChronicle()
67
+ mark = chronicle.mark
68
+ logMarks = chronicle.logMarks
69
+ }
70
+ mark?.(`breakage`)
71
+ const git = simpleGit(baseDirname)
72
+ mark?.(`spawn git`)
73
+ const isGitRepo = await git.checkIsRepo()
74
+ mark?.(`is git repository`)
75
+ if (!isGitRepo) {
76
+ return {
77
+ summary: `The git repository must be clean to run this command.`,
78
+ gitWasClean: false,
79
+ }
80
+ }
81
+ if (
82
+ !(await withTestFileState(git, baseDirname, (state) => state.isClean()))
83
+ ) {
84
+ return {
85
+ summary: `The git repository must be clean to run this command.`,
86
+ gitWasClean: false,
87
+ }
88
+ }
89
+ const tagsRemote = await git.listRemote([`--tags`, `origin`])
90
+
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
+ )
115
+
116
+ mark?.(`fetched latest release tag`)
117
+
118
+ let productionFiles: string[]
119
+ try {
120
+ productionFiles = (
121
+ await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, latestReleaseTag])
122
+ )
123
+ .split(`\0`)
124
+ .filter(Boolean)
125
+ } catch (thrown) {
126
+ mark?.(`failed to list production files`)
127
+ const message =
128
+ thrown instanceof Error
129
+ ? thrown.message
130
+ : typeof thrown === `string`
131
+ ? thrown
132
+ : undefined
133
+ const summary = `Failed to list production files${
134
+ message ? `: ${message}` : `.`
135
+ }`
136
+ return {
137
+ summary,
138
+ gitWasClean: true,
139
+ lastReleaseFound: true,
140
+ lastReleaseTag: latestReleaseTag,
141
+ testsWereFound: false,
142
+ } as const
143
+ }
144
+ mark?.(`listed all files checked into git`)
145
+ const productionTestFiles = productionFiles.filter((file) =>
146
+ minimatch(file, testPattern),
147
+ )
148
+ mark?.(`filtered to public test files`)
149
+
150
+ const prepared = await withTestFileState(git, baseDirname, async (state) => {
151
+ if (!(await state.isClean())) {
152
+ return {
153
+ summary: `The git repository must be clean to run this command.`,
154
+ gitWasClean: false,
155
+ } as const
156
+ }
157
+
158
+ if (productionTestFiles.length === 0) {
159
+ return {
160
+ summary: `No tests were found matching the pattern "${testPattern}".`,
161
+ gitWasClean: true,
162
+ lastReleaseFound: true,
163
+ lastReleaseTag: latestReleaseTag,
164
+ testsWereFound: false,
165
+ } as const
166
+ }
167
+ const restore = await state.replaceTests(
168
+ latestReleaseTag,
169
+ productionTestFiles,
170
+ )
171
+ mark?.(`restored public tests from latest release tag`)
172
+ return { latestReleaseTag, productionTestFiles, restore }
173
+ })
174
+ if (prepared.summary !== undefined) return prepared
175
+ const { restore } = prepared
176
+
177
+ try {
178
+ const noBreakingChangesDetected = await new Promise<
179
+ BreakageOutcome & { summary: string }
180
+ >((resolve, reject) => {
181
+ const result = exec(
182
+ testCommand,
183
+ { cwd: baseDirname },
184
+ (_, __, stderr) => {
185
+ if (result.exitCode === 0) {
186
+ resolve({
187
+ summary: `No breaking changes were detected.`,
188
+ gitWasClean: true,
189
+ lastReleaseFound: true,
190
+ lastReleaseTag: latestReleaseTag,
191
+ testsWereFound: true,
192
+ testsFound: productionTestFiles,
193
+ breakingChangesFound: false,
194
+ })
195
+ } else {
196
+ reject(stderr)
197
+ }
198
+ },
199
+ )
200
+ })
201
+ mark?.(`passed tests`)
202
+ return noBreakingChangesDetected
203
+ } catch (thrown) {
204
+ mark?.(`failed tests`)
205
+ const breakingChangesCertified = await new Promise<
206
+ BreakageOutcome & { summary: string }
207
+ >((resolve) => {
208
+ const result = exec(
209
+ certifyCommand,
210
+ { cwd: baseDirname },
211
+ (_, stdout, stderr) => {
212
+ const wereCertified = result.exitCode === 0
213
+ mark?.(`${wereCertified ? `passed` : `failed`} certification`)
214
+ resolve({
215
+ summary: wereCertified
216
+ ? `Breaking changes were found and certified.`
217
+ : `Breaking changes were found, but not certified.`,
218
+ gitWasClean: true,
219
+ lastReleaseFound: true,
220
+ lastReleaseTag: latestReleaseTag,
221
+ testsWereFound: true,
222
+ testsFound: productionTestFiles,
223
+ breakingChangesFound: true,
224
+ testResult: thrown as string,
225
+ breakingChangesCertified: wereCertified,
226
+ certificationStdout: stdout,
227
+ certificationStderr: stderr,
228
+ })
229
+ },
230
+ )
231
+ })
232
+ return breakingChangesCertified
233
+ } finally {
234
+ await restore()
235
+ mark?.(`restored original public tests`)
236
+ logMarks?.()
237
+ }
238
+ }