@trofimyukdev/repo-truth 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +273 -2
- package/dist/action.d.ts +52 -0
- package/dist/action.js +182 -0
- package/dist/action.js.map +1 -0
- package/dist/checks/action-pins.d.ts +52 -0
- package/dist/checks/action-pins.js +196 -0
- package/dist/checks/action-pins.js.map +1 -0
- package/dist/checks/autosquash.d.ts +23 -0
- package/dist/checks/autosquash.js +85 -0
- package/dist/checks/autosquash.js.map +1 -0
- package/dist/checks/bidi-controls.d.ts +58 -0
- package/dist/checks/bidi-controls.js +172 -0
- package/dist/checks/bidi-controls.js.map +1 -0
- package/dist/checks/binary-files.d.ts +53 -0
- package/dist/checks/binary-files.js +157 -0
- package/dist/checks/binary-files.js.map +1 -0
- package/dist/checks/config-changes.d.ts +57 -0
- package/dist/checks/config-changes.js +175 -0
- package/dist/checks/config-changes.js.map +1 -0
- package/dist/checks/credentials.d.ts +35 -0
- package/dist/checks/credentials.js +138 -0
- package/dist/checks/credentials.js.map +1 -0
- package/dist/checks/install-scripts.d.ts +39 -0
- package/dist/checks/install-scripts.js +173 -0
- package/dist/checks/install-scripts.js.map +1 -0
- package/dist/checks/lint-rules.d.ts +49 -0
- package/dist/checks/lint-rules.js +184 -0
- package/dist/checks/lint-rules.js.map +1 -0
- package/dist/checks/path-policy.d.ts +40 -0
- package/dist/checks/path-policy.js +195 -0
- package/dist/checks/path-policy.js.map +1 -0
- package/dist/checks/raised-timeouts.d.ts +66 -0
- package/dist/checks/raised-timeouts.js +248 -0
- package/dist/checks/raised-timeouts.js.map +1 -0
- package/dist/checks/script-changes.d.ts +51 -0
- package/dist/checks/script-changes.js +199 -0
- package/dist/checks/script-changes.js.map +1 -0
- package/dist/checks/secret-files.d.ts +58 -0
- package/dist/checks/secret-files.js +165 -0
- package/dist/checks/secret-files.js.map +1 -0
- package/dist/checks/strictness.d.ts +59 -0
- package/dist/checks/strictness.js +260 -0
- package/dist/checks/strictness.js.map +1 -0
- package/dist/checks/submodules.d.ts +51 -0
- package/dist/checks/submodules.js +172 -0
- package/dist/checks/submodules.js.map +1 -0
- package/dist/checks/suppressions.d.ts +53 -0
- package/dist/checks/suppressions.js +203 -0
- package/dist/checks/suppressions.js.map +1 -0
- package/dist/checks/workflow-permissions.d.ts +56 -0
- package/dist/checks/workflow-permissions.js +237 -0
- package/dist/checks/workflow-permissions.js.map +1 -0
- package/dist/checks/workflow-weakening.d.ts +38 -0
- package/dist/checks/workflow-weakening.js +172 -0
- package/dist/checks/workflow-weakening.js.map +1 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +300 -0
- package/dist/cli.js.map +1 -0
- package/dist/commit-range.d.ts +28 -0
- package/dist/commit-range.js +152 -0
- package/dist/commit-range.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +235 -0
- package/dist/config.js.map +1 -0
- package/dist/conflict-markers.d.ts +40 -0
- package/dist/conflict-markers.js +234 -0
- package/dist/conflict-markers.js.map +1 -0
- package/dist/dependency-sources.d.ts +32 -0
- package/dist/dependency-sources.js +260 -0
- package/dist/dependency-sources.js.map +1 -0
- package/dist/figure-claims.d.ts +35 -0
- package/dist/figure-claims.js +139 -0
- package/dist/figure-claims.js.map +1 -0
- package/dist/figure-text.d.ts +22 -0
- package/dist/figure-text.js +119 -0
- package/dist/figure-text.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/large-blobs.d.ts +35 -0
- package/dist/large-blobs.js +163 -0
- package/dist/large-blobs.js.map +1 -0
- package/dist/lockfile-drift.d.ts +36 -0
- package/dist/lockfile-drift.js +206 -0
- package/dist/lockfile-drift.js.map +1 -0
- package/dist/lowered-thresholds.d.ts +70 -0
- package/dist/lowered-thresholds.js +281 -0
- package/dist/lowered-thresholds.js.map +1 -0
- package/dist/measurement-rule.d.ts +39 -0
- package/dist/measurement-rule.js +213 -0
- package/dist/measurement-rule.js.map +1 -0
- package/dist/portable-paths.d.ts +37 -0
- package/dist/portable-paths.js +215 -0
- package/dist/portable-paths.js.map +1 -0
- package/dist/registry.d.ts +43 -0
- package/dist/registry.js +144 -0
- package/dist/registry.js.map +1 -0
- package/dist/replay-plan.d.ts +36 -0
- package/dist/replay-plan.js +196 -0
- package/dist/replay-plan.js.map +1 -0
- package/dist/replayed-figures.d.ts +36 -0
- package/dist/replayed-figures.js +159 -0
- package/dist/replayed-figures.js.map +1 -0
- package/dist/snapshot-refreshes.d.ts +36 -0
- package/dist/snapshot-refreshes.js +202 -0
- package/dist/snapshot-refreshes.js.map +1 -0
- package/dist/stray-files.d.ts +52 -0
- package/dist/stray-files.js +234 -0
- package/dist/stray-files.js.map +1 -0
- package/dist/symlinks.d.ts +28 -0
- package/dist/symlinks.js +193 -0
- package/dist/symlinks.js.map +1 -0
- package/dist/trailers.d.ts +36 -0
- package/dist/trailers.js +110 -0
- package/dist/trailers.js.map +1 -0
- package/dist/weakened-tests.d.ts +94 -0
- package/dist/weakened-tests.js +458 -0
- package/dist/weakened-tests.js.map +1 -0
- package/package.json +31 -4
package/dist/symlinks.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlinks.
|
|
3
|
+
*
|
|
4
|
+
* A symbolic link is committed as a blob holding its target, so a diff shows a
|
|
5
|
+
* link to `/etc/passwd` as one harmless line of text. This check reads every
|
|
6
|
+
* link `base..candidate` adds, retargets or turns a file into, takes the
|
|
7
|
+
* target from the candidate's own blob (never from the working tree), resolves
|
|
8
|
+
* it from the link's own directory and reports one that is [absolute], that
|
|
9
|
+
* climbs [outside] the repository, or that lands in the repository's [.git]
|
|
10
|
+
* directory. A target inside the repository is not a finding whether or not
|
|
11
|
+
* anything exists there.
|
|
12
|
+
*/
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
const NAME = "RT-11";
|
|
15
|
+
const SYMLINK_MODE = "120000";
|
|
16
|
+
function runGit(cwd, args) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" }, (error, stdout, stderr) => {
|
|
19
|
+
if (error) {
|
|
20
|
+
const detail = stderr.toString("utf8").trim();
|
|
21
|
+
reject(new Error(detail.length > 0 ? detail : error.message));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
resolve(stdout);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Escape everything outside printable ASCII so a name cannot replay a control sequence. */
|
|
29
|
+
function escapeText(text) {
|
|
30
|
+
let escaped = "";
|
|
31
|
+
for (const char of text) {
|
|
32
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
33
|
+
if (codePoint >= 0x20 && codePoint <= 0x7e) {
|
|
34
|
+
escaped += char;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
escaped += `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return escaped;
|
|
41
|
+
}
|
|
42
|
+
function quote(text) {
|
|
43
|
+
return `"${escapeText(text)}"`;
|
|
44
|
+
}
|
|
45
|
+
function decodeText(bytes) {
|
|
46
|
+
try {
|
|
47
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return bytes.toString("latin1");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function splitNul(out) {
|
|
54
|
+
const fields = [];
|
|
55
|
+
let start = 0;
|
|
56
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
57
|
+
if (out[i] === 0) {
|
|
58
|
+
fields.push(decodeText(out.subarray(start, i)));
|
|
59
|
+
start = i + 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return fields;
|
|
63
|
+
}
|
|
64
|
+
async function resolveCommit(cwd, revision) {
|
|
65
|
+
if (revision.length === 0 || revision.startsWith("-")) {
|
|
66
|
+
throw new Error(`revision ${quote(revision)} names no commit`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const out = await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`]);
|
|
70
|
+
const hash = out.toString("utf8").trim();
|
|
71
|
+
if (hash.length === 0) {
|
|
72
|
+
throw new Error("empty answer");
|
|
73
|
+
}
|
|
74
|
+
return hash;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new Error(`revision ${quote(revision)} names no commit: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The links the range adds, retargets or turns a file into, read from the diff of the two commits. */
|
|
81
|
+
async function changedLinks(root, base, candidate) {
|
|
82
|
+
const label = `${base.slice(0, 12)} and ${candidate.slice(0, 12)}`;
|
|
83
|
+
let ancestor;
|
|
84
|
+
try {
|
|
85
|
+
ancestor = (await runGit(root, ["merge-base", base, candidate])).toString("utf8").trim();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new Error(`no merge base of ${label}: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
if (ancestor.length === 0) {
|
|
91
|
+
throw new Error(`no merge base of ${label}`);
|
|
92
|
+
}
|
|
93
|
+
let out;
|
|
94
|
+
try {
|
|
95
|
+
out = await runGit(root, [
|
|
96
|
+
"diff-tree",
|
|
97
|
+
"-r",
|
|
98
|
+
"-z",
|
|
99
|
+
"--raw",
|
|
100
|
+
"--no-renames",
|
|
101
|
+
"--no-ext-diff",
|
|
102
|
+
"--no-textconv",
|
|
103
|
+
"--no-color",
|
|
104
|
+
"--diff-filter=ACMRT",
|
|
105
|
+
"--abbrev=40",
|
|
106
|
+
ancestor,
|
|
107
|
+
candidate,
|
|
108
|
+
"--",
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new Error(`could not read the entries changed by the range: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
const fields = splitNul(out);
|
|
115
|
+
const links = [];
|
|
116
|
+
// Each entry is ":oldmode newmode oldsha newsha status" then the path.
|
|
117
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
118
|
+
const meta = (fields[i] ?? "").replace(/^:/, "").split(" ");
|
|
119
|
+
const path = fields[i + 1] ?? "";
|
|
120
|
+
if (meta[1] === SYMLINK_MODE && meta[3] !== undefined) {
|
|
121
|
+
links.push({ path, blob: meta[3] });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return links.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
125
|
+
}
|
|
126
|
+
async function readTarget(root, link) {
|
|
127
|
+
try {
|
|
128
|
+
return decodeText(await runGit(root, ["cat-file", "blob", link.blob]));
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw new Error(`could not read the blob of link ${quote(link.path)}: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** The rule a target breaks when the link at `path` points at it, or undefined if it stays inside. */
|
|
135
|
+
function brokenRule(path, target) {
|
|
136
|
+
if (target.startsWith("/") || target.startsWith("\\\\") || /^[A-Za-z]:/.test(target)) {
|
|
137
|
+
return "absolute";
|
|
138
|
+
}
|
|
139
|
+
const stack = path.split("/").slice(0, -1);
|
|
140
|
+
for (const segment of target.split(/[\\/]/)) {
|
|
141
|
+
if (segment === "" || segment === ".") {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (segment === "..") {
|
|
145
|
+
if (stack.length === 0) {
|
|
146
|
+
return "outside";
|
|
147
|
+
}
|
|
148
|
+
stack.pop();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
stack.push(segment);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if ((stack[0] ?? "").toLowerCase() === ".git") {
|
|
155
|
+
return ".git";
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check every link `base..candidate` adds or changes. Fails closed: a revision
|
|
161
|
+
* that names no commit, or any git invocation that fails, is a `fail` record
|
|
162
|
+
* naming the cause - never a `pass`, and never a thrown error.
|
|
163
|
+
*/
|
|
164
|
+
export async function checkSymlinks(options) {
|
|
165
|
+
try {
|
|
166
|
+
const { cwd, base, candidate } = options;
|
|
167
|
+
const baseCommit = await resolveCommit(cwd, base);
|
|
168
|
+
const candidateCommit = await resolveCommit(cwd, candidate);
|
|
169
|
+
const root = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).toString("utf8").replace(/\r?\n$/, "");
|
|
170
|
+
const links = await changedLinks(root, baseCommit, candidateCommit);
|
|
171
|
+
const evidence = [];
|
|
172
|
+
for (const link of links) {
|
|
173
|
+
const target = await readTarget(root, link);
|
|
174
|
+
const rule = brokenRule(link.path, target);
|
|
175
|
+
if (rule !== undefined) {
|
|
176
|
+
evidence.push(`link ${quote(link.path)} -> ${quote(target)} breaks rule [${rule}]`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (evidence.length > 0) {
|
|
180
|
+
return { name: NAME, status: "fail", evidence };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
name: NAME,
|
|
184
|
+
status: "pass",
|
|
185
|
+
evidence: [`${links.length} link(s) added or changed in ${quote(base)}..${quote(candidate)} stay inside the repository`],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
return { name: NAME, status: "fail", evidence: [escapeText(message)] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=symlinks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"symlinks.js","sourceRoot":"","sources":["../src/symlinks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-Id trailer check.
|
|
3
|
+
*
|
|
4
|
+
* A landing says which queued task it closes in a trailer, because the
|
|
5
|
+
* trailer is the one signal a rebase or squash does not rewrite and a later
|
|
6
|
+
* reader can match exactly. The trailer block is read by git itself
|
|
7
|
+
* (`%(trailers)`), never by splitting message text on a colon, so folded
|
|
8
|
+
* values, empty values and a missing blank line before the block are all
|
|
9
|
+
* decided by git's rules.
|
|
10
|
+
*
|
|
11
|
+
* Which commit must carry one: the LANDING, which is the candidate commit.
|
|
12
|
+
* The rest of `base..candidate` is the task's intermediate work and needs
|
|
13
|
+
* none; a trailer on one of them does not count for the landing.
|
|
14
|
+
*/
|
|
15
|
+
import type { CheckRecord } from "./index.js";
|
|
16
|
+
/** The trailer key that names the task a landing closes. */
|
|
17
|
+
export declare const TASK_TRAILER_KEY = "Task-Id";
|
|
18
|
+
/** The trailer key the factory writes on the merges it makes. */
|
|
19
|
+
export declare const FACTORY_TRAILER_KEY = "Millwright-Task-ID";
|
|
20
|
+
/** The shape of a task id: a letter, letters or digits, a hyphen, digits. */
|
|
21
|
+
export declare const TASK_ID_PATTERN: RegExp;
|
|
22
|
+
/** Where to look, and what range of commits to look at. */
|
|
23
|
+
export interface TrailerCheckOptions {
|
|
24
|
+
/** The git repository to run in. */
|
|
25
|
+
readonly cwd: string;
|
|
26
|
+
/** The revision the range starts after (exclusive). */
|
|
27
|
+
readonly base: string;
|
|
28
|
+
/** The revision the range ends at (inclusive); the landing. */
|
|
29
|
+
readonly candidate: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check that the landing of `base..candidate` - the candidate commit - carries
|
|
33
|
+
* a well-formed `Task-Id` trailer. Fails closed: an unknown revision or any
|
|
34
|
+
* failing git invocation is a `fail`, never a `pass` and never a throw.
|
|
35
|
+
*/
|
|
36
|
+
export declare function checkTrailers(options: TrailerCheckOptions): Promise<CheckRecord>;
|
package/dist/trailers.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-Id trailer check.
|
|
3
|
+
*
|
|
4
|
+
* A landing says which queued task it closes in a trailer, because the
|
|
5
|
+
* trailer is the one signal a rebase or squash does not rewrite and a later
|
|
6
|
+
* reader can match exactly. The trailer block is read by git itself
|
|
7
|
+
* (`%(trailers)`), never by splitting message text on a colon, so folded
|
|
8
|
+
* values, empty values and a missing blank line before the block are all
|
|
9
|
+
* decided by git's rules.
|
|
10
|
+
*
|
|
11
|
+
* Which commit must carry one: the LANDING, which is the candidate commit.
|
|
12
|
+
* The rest of `base..candidate` is the task's intermediate work and needs
|
|
13
|
+
* none; a trailer on one of them does not count for the landing.
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
/** The trailer key that names the task a landing closes. */
|
|
17
|
+
export const TASK_TRAILER_KEY = "Task-Id";
|
|
18
|
+
/** The trailer key the factory writes on the merges it makes. */
|
|
19
|
+
export const FACTORY_TRAILER_KEY = "Millwright-Task-ID";
|
|
20
|
+
/** The shape of a task id: a letter, letters or digits, a hyphen, digits. */
|
|
21
|
+
export const TASK_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*-[0-9]+$/;
|
|
22
|
+
const NAME = "RT-03";
|
|
23
|
+
function runGit(cwd, args) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
execFile("git", args, { cwd, maxBuffer: 64 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
26
|
+
if (error) {
|
|
27
|
+
reject(new Error(stderr.trim() || error.message));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
resolve(stdout);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function fail(evidence) {
|
|
35
|
+
return { name: NAME, status: "fail", evidence };
|
|
36
|
+
}
|
|
37
|
+
async function resolveCommit(cwd, revision) {
|
|
38
|
+
try {
|
|
39
|
+
const out = await runGit(cwd, [
|
|
40
|
+
"rev-parse",
|
|
41
|
+
"--verify",
|
|
42
|
+
"--quiet",
|
|
43
|
+
"--end-of-options",
|
|
44
|
+
revision + "^{commit}",
|
|
45
|
+
]);
|
|
46
|
+
const sha = out.trim();
|
|
47
|
+
return sha.length > 0 ? sha : undefined;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The values of every `Task-Id` trailer in a commit's trailer block, as git
|
|
55
|
+
* parses and unfolds them. One entry per trailer; an empty value is "".
|
|
56
|
+
*/
|
|
57
|
+
async function readTaskIds(cwd, sha) {
|
|
58
|
+
const out = await runGit(cwd, [
|
|
59
|
+
"log",
|
|
60
|
+
"-1",
|
|
61
|
+
"--format=%(trailers:key=" + TASK_TRAILER_KEY + ",key=" + FACTORY_TRAILER_KEY + ",unfold,separator=%x00)",
|
|
62
|
+
sha,
|
|
63
|
+
"--",
|
|
64
|
+
]);
|
|
65
|
+
// Each entry is `Task-Id: value` as git printed it; the key is known and
|
|
66
|
+
// colon-free, so cutting after the first colon only drops the key git matched.
|
|
67
|
+
const text = out.replace(/\n$/, "");
|
|
68
|
+
if (text.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return text.split("\0").map((entry) => entry.slice(entry.indexOf(":") + 1).trim());
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check that the landing of `base..candidate` - the candidate commit - carries
|
|
75
|
+
* a well-formed `Task-Id` trailer. Fails closed: an unknown revision or any
|
|
76
|
+
* failing git invocation is a `fail`, never a `pass` and never a throw.
|
|
77
|
+
*/
|
|
78
|
+
export async function checkTrailers(options) {
|
|
79
|
+
const { cwd, base, candidate } = options;
|
|
80
|
+
const range = base + ".." + candidate;
|
|
81
|
+
try {
|
|
82
|
+
const baseSha = await resolveCommit(cwd, base);
|
|
83
|
+
if (baseSha === undefined) {
|
|
84
|
+
return fail([`revision ${base} does not name a commit`]);
|
|
85
|
+
}
|
|
86
|
+
const landing = await resolveCommit(cwd, candidate);
|
|
87
|
+
if (landing === undefined) {
|
|
88
|
+
return fail([`revision ${candidate} does not name a commit`]);
|
|
89
|
+
}
|
|
90
|
+
const listed = (await runGit(cwd, ["rev-list", baseSha + ".." + landing, "--"])).trim();
|
|
91
|
+
if (listed.length === 0) {
|
|
92
|
+
return { name: NAME, status: "pass", evidence: [`range ${range} is empty`] };
|
|
93
|
+
}
|
|
94
|
+
const subject = (await runGit(cwd, ["log", "-1", "--format=%s", landing, "--"])).replace(/\n$/, "");
|
|
95
|
+
const label = `${landing.slice(0, 12)} ${subject}`;
|
|
96
|
+
const values = await readTaskIds(cwd, landing);
|
|
97
|
+
if (values.length === 0) {
|
|
98
|
+
return fail([`${label}: no ${TASK_TRAILER_KEY} trailer`]);
|
|
99
|
+
}
|
|
100
|
+
const bad = values.filter((value) => !TASK_ID_PATTERN.test(value));
|
|
101
|
+
if (bad.length > 0) {
|
|
102
|
+
return fail(bad.map((value) => `${label}: ${TASK_TRAILER_KEY} value "${value}" does not parse as a task id`));
|
|
103
|
+
}
|
|
104
|
+
return { name: NAME, status: "pass", evidence: [`${label}: closes ${values.join(", ")}`] };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return fail([`could not check trailers of ${range}: ${error.message}`]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=trailers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trailers.js","sourceRoot":"","sources":["../src/trailers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weakened tests.
|
|
3
|
+
*
|
|
4
|
+
* The cheapest way to make a red suite green is to make it ask less: skip the
|
|
5
|
+
* failing test, focus the run on the passing ones, delete the test or the
|
|
6
|
+
* assertion that fails. Each of those leaves the build green, and each is
|
|
7
|
+
* legitimate sometimes, so this check does not judge intent. It reports every
|
|
8
|
+
* such move in `base..candidate` for a human to sanction, and fails while one
|
|
9
|
+
* is unsanctioned.
|
|
10
|
+
*
|
|
11
|
+
* Only the DIFF is read. A marker that predates the range is never reported,
|
|
12
|
+
* and a marker line the range removes and adds back unchanged is a move, not
|
|
13
|
+
* a new marker. File contents are read by blob id through git, never from the
|
|
14
|
+
* working tree, and the diff is taken from the merge base of the two
|
|
15
|
+
* revisions, as in the portable-paths check.
|
|
16
|
+
*
|
|
17
|
+
* What counts as a test file, a marker, a test or an assertion lives in one
|
|
18
|
+
* table, `WEAKENING_SPELLINGS`; a caller on another runner passes its own.
|
|
19
|
+
*/
|
|
20
|
+
import type { CheckRecord } from "./index.js";
|
|
21
|
+
/** The kinds of move this version reports. */
|
|
22
|
+
export type WeakeningCategory = "skip" | "only" | "test-file-deleted" | "test-removed" | "assertion-removed";
|
|
23
|
+
/** The kinds of file a marker can apply to; `any` is every file. */
|
|
24
|
+
export type MarkerScope = "test" | "config" | "any";
|
|
25
|
+
/** One spelling of a marker that disables or narrows tests. */
|
|
26
|
+
export interface MarkerSpelling {
|
|
27
|
+
/** `skip` for a test skipped or left todo, `only` for a focus marker. */
|
|
28
|
+
readonly category: "skip" | "only";
|
|
29
|
+
/** The kinds of file the marker counts in. */
|
|
30
|
+
readonly scope: readonly MarkerScope[];
|
|
31
|
+
/** Tested against each added or removed line, without the diff's sign. */
|
|
32
|
+
readonly pattern: RegExp;
|
|
33
|
+
/** A line `pattern` matches. */
|
|
34
|
+
readonly example: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The recognised spellings. A caller passes its own as the `spellings` option;
|
|
38
|
+
* each key it gives replaces that key of this table, so extending one list is
|
|
39
|
+
* `{ ...WEAKENING_SPELLINGS, markers: [...WEAKENING_SPELLINGS.markers, mine] }`.
|
|
40
|
+
*/
|
|
41
|
+
export interface WeakeningSpellings {
|
|
42
|
+
/** Paths, relative to the repository root, that are test files. */
|
|
43
|
+
readonly testFiles: readonly RegExp[];
|
|
44
|
+
/** Paths that configure a test runner. */
|
|
45
|
+
readonly configFiles: readonly RegExp[];
|
|
46
|
+
readonly markers: readonly MarkerSpelling[];
|
|
47
|
+
/**
|
|
48
|
+
* A line of a test file that declares one test. The test's name is the
|
|
49
|
+
* group named `name`, else the first group, else the whole line.
|
|
50
|
+
*/
|
|
51
|
+
readonly tests: readonly RegExp[];
|
|
52
|
+
/** A line of a test file that makes an assertion. */
|
|
53
|
+
readonly assertions: readonly RegExp[];
|
|
54
|
+
}
|
|
55
|
+
export declare const WEAKENING_SPELLINGS: WeakeningSpellings;
|
|
56
|
+
/** One move that lowers what the suite asserts. */
|
|
57
|
+
export interface WeakeningFinding {
|
|
58
|
+
/** `<category>:<file>:<line>`. */
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly category: WeakeningCategory;
|
|
61
|
+
/** The path on `side`, relative to the repository root. */
|
|
62
|
+
readonly file: string;
|
|
63
|
+
/** The line on `side`; 0 for a deleted file that had no lines. */
|
|
64
|
+
readonly line: number;
|
|
65
|
+
/** `candidate` for a line the range added, `base` for one it removed. */
|
|
66
|
+
readonly side: "base" | "candidate";
|
|
67
|
+
readonly detail: string;
|
|
68
|
+
/** The sanction's reason, or `null` while the finding is unsanctioned. */
|
|
69
|
+
readonly sanction: string | null;
|
|
70
|
+
}
|
|
71
|
+
export interface WeakenedTestsRecord extends CheckRecord {
|
|
72
|
+
readonly findings: readonly WeakeningFinding[];
|
|
73
|
+
}
|
|
74
|
+
export interface WeakenedTestsOptions {
|
|
75
|
+
/** The git repository to run in. */
|
|
76
|
+
readonly cwd: string;
|
|
77
|
+
/** The revision the range starts after. */
|
|
78
|
+
readonly base: string;
|
|
79
|
+
/** The revision the range ends at. */
|
|
80
|
+
readonly candidate: string;
|
|
81
|
+
/** Replaces the keys of `WEAKENING_SPELLINGS` it gives. */
|
|
82
|
+
readonly spellings?: Partial<WeakeningSpellings> | undefined;
|
|
83
|
+
/** Keys are finding ids or `<category>:<file>`; values are the reasons. */
|
|
84
|
+
readonly sanctions?: Readonly<Record<string, string>> | undefined;
|
|
85
|
+
}
|
|
86
|
+
/** The key that sanctions a finding in a commit message of the range. */
|
|
87
|
+
export declare const SANCTION_KEY = "Sanctioned-Weakening";
|
|
88
|
+
/**
|
|
89
|
+
* Report the moves of `base..candidate` that lower what the suite asserts.
|
|
90
|
+
* Fails while any finding is unsanctioned, and fails closed: a revision that
|
|
91
|
+
* names no commit, or any git invocation that fails, is a `fail` record naming
|
|
92
|
+
* the cause - never a `pass`, and never a thrown error.
|
|
93
|
+
*/
|
|
94
|
+
export declare function checkWeakenedTests(options: WeakenedTestsOptions): Promise<WeakenedTestsRecord>;
|