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.
- package/LICENSE +5 -0
- package/README.md +234 -2
- package/dist/breakage-D_nc-zEK.js +345 -0
- package/dist/breakage-D_nc-zEK.js.map +1 -0
- package/dist/breakage.$configPath.schema.json +30 -0
- package/dist/breakage.d.ts +43 -0
- package/dist/breakage.d.ts.map +1 -0
- package/dist/breakage.help.schema.json +6 -0
- package/dist/breakage.js +1 -0
- package/dist/breakage.main.schema.json +30 -0
- package/dist/breakage.schema.schema.json +10 -0
- package/dist/breakage.x.js +148 -0
- package/dist/breakage.x.js.map +1 -0
- package/package.json +51 -5
- package/src/breakage.ts +238 -0
- package/src/breakage.x.ts +181 -0
- package/src/directory-lock.ts +29 -0
- package/src/release-tag.ts +62 -0
- package/src/test-file-state.ts +252 -0
|
@@ -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"}
|
package/dist/breakage.js
ADDED
|
@@ -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,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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|
package/src/breakage.ts
ADDED
|
@@ -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
|
+
}
|