@avi2dg/checks 0.38.0 → 0.39.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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/dist/templates/readme.md +4 -1
- package/docs/gates/checks-docs.md +1 -1
- package/docs/gates/checks-mutation-baseline.md +49 -2
- package/package.json +1 -1
- package/src/docs/doc-templates.ts +10 -2
- package/src/testing/mutation-baseline.ts +97 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.39.0
|
|
6
|
+
|
|
7
|
+
Released 2026-10-11.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **testing:** prune superseded mutation baselines after main uploads [#144](https://github.com/avi2d/checks/pull/144)
|
|
12
|
+
|
|
13
|
+
## 0.38.1
|
|
14
|
+
|
|
15
|
+
Released 2026-10-11.
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
|
|
19
|
+
- **docs:** derive README what-you-see line from source, make Where things are optional [#142](https://github.com/avi2d/checks/pull/142)
|
|
20
|
+
|
|
5
21
|
## 0.38.0
|
|
6
22
|
|
|
7
23
|
Released 2026-10-10.
|
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ These bins run on their own:
|
|
|
157
157
|
- [`checks-flake`](docs/gates/checks-flake.md) runs the suite on a schedule and records the seeds a flaky test fails with.
|
|
158
158
|
- [`checks-mutation`](docs/gates/checks-mutation.md) runs Stryker for scoped checks and refuses a full run outside CI.
|
|
159
159
|
- [`checks-mutation-compare`](docs/gates/checks-mutation-compare.md) compares the mutants of two reports and lists each one that regresses.
|
|
160
|
-
- [`checks-mutation-baseline`](docs/gates/checks-mutation-baseline.md) restores the newest mutation baseline from `main`, through a cache on a self-hosted runner.
|
|
160
|
+
- [`checks-mutation-baseline`](docs/gates/checks-mutation-baseline.md) restores the newest mutation baseline from `main`, through a cache on a self-hosted runner, and deletes superseded baselines after a `main` upload.
|
|
161
161
|
- [`checks-subsumed-tests`](docs/gates/checks-subsumed-tests.md) lists each test another test subsumes in a mutation run.
|
|
162
162
|
- [`checks-changelog`](docs/gates/checks-changelog.md) writes the pending release into `CHANGELOG.md` from the conventional commits since the last release.
|
|
163
163
|
- [`checks-release-notes`](docs/gates/checks-release-notes.md) writes one `CHANGELOG.md` section to a file for a GitHub release.
|
package/dist/templates/readme.md
CHANGED
|
@@ -13,7 +13,8 @@ To install <name>:
|
|
|
13
13
|
1. <step>
|
|
14
14
|
1. <step>
|
|
15
15
|
|
|
16
|
-
<What you see when it worked.>
|
|
16
|
+
<What you see when it worked, written only from what the source shows.>
|
|
17
|
+
<Leave this line out when the source shows nothing.>
|
|
17
18
|
|
|
18
19
|
## <Everyday task, verb first, or what the reader looks up>
|
|
19
20
|
|
|
@@ -26,6 +27,8 @@ To <do the task>:
|
|
|
26
27
|
|
|
27
28
|
## Where things are
|
|
28
29
|
|
|
30
|
+
<Leave this section out when there is no path worth mapping yet.>
|
|
31
|
+
|
|
29
32
|
| Path | What it holds |
|
|
30
33
|
| --- | --- |
|
|
31
34
|
|
|
@@ -201,7 +201,7 @@ With one it is that commit against its parent, or against the empty tree for a r
|
|
|
201
201
|
|
|
202
202
|
```
|
|
203
203
|
docs: 6 violation(s):
|
|
204
|
-
README.md:1: lacks `##
|
|
204
|
+
README.md:1: lacks `## Before you begin`
|
|
205
205
|
README.md:12: carries `;`, a semicolon. Use two sentences
|
|
206
206
|
README.md:14: carries `former`, a report about the past. Say what is true now, and leave what changed to the changelog, a commit message or a decision record
|
|
207
207
|
README.md:20: names `scripts/bild.ts`, which is not in the repository
|
|
@@ -4,7 +4,8 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# checks-mutation-baseline
|
|
6
6
|
|
|
7
|
-
`checks-mutation-baseline` restores the newest mutation baseline that `main` published, and
|
|
7
|
+
`checks-mutation-baseline` restores the newest mutation baseline that `main` published, and deletes superseded baselines after a `main` upload.
|
|
8
|
+
On a self-hosted runner it keeps each baseline it downloads, so the next job copies it instead.
|
|
8
9
|
|
|
9
10
|
## What it checks
|
|
10
11
|
|
|
@@ -22,12 +23,23 @@ It takes the report from `mutation.json` at the artifact's root, or else from `m
|
|
|
22
23
|
|
|
23
24
|
It creates the directories each destination needs.
|
|
24
25
|
|
|
26
|
+
With `--prune` it deletes the repository's superseded baselines instead of restoring one.
|
|
27
|
+
It keeps the newest unexpired `mutation-baseline` among the runs the restore reads, and the newest unexpired `mutation-baseline-full` the restore takes first.
|
|
28
|
+
It also keeps the unexpired baselines of the run it runs in, which `GITHUB_RUN_ID` names.
|
|
29
|
+
The restore reads only successful runs, so it takes them once that run succeeds.
|
|
30
|
+
A failed artifact lookup while it picks what to keep stops it before it deletes anything.
|
|
31
|
+
Treating that artifact as absent would delete the one the restore takes.
|
|
32
|
+
It deletes only artifacts with exactly those two names in the repository it runs in.
|
|
33
|
+
It deletes only unexpired artifacts, since the restore never reads an expired one.
|
|
34
|
+
With `--dry-run` it prints what it would delete and what it would keep, and deletes nothing.
|
|
35
|
+
It deletes through `gh`, which needs a token that deletes the repository's Actions artifacts, such as `GH_TOKEN: ${{ github.token }}` with `actions: write`.
|
|
36
|
+
|
|
25
37
|
## What it reads
|
|
26
38
|
|
|
27
39
|
It reads the runs and their artifacts through `gh`, which needs a token that reads the repository's Actions, such as `GH_TOKEN: ${{ github.token }}` with `actions: read`.
|
|
28
40
|
The `mutation-baseline` artifact holds `stryker-incremental.json` and `mutation/mutation.json`, which an upload of `reports/stryker-incremental.json` and `reports/mutation/mutation.json` writes.
|
|
29
41
|
The `mutation-baseline-full` artifact holds `mutation.json` at its root, which an upload of `reports/mutation/mutation.json` alone writes.
|
|
30
|
-
It reads `RUNNER_ENVIRONMENT` and `HOME` to place the runner cache
|
|
42
|
+
It reads `RUNNER_ENVIRONMENT` and `HOME` to place the runner cache, and `--prune` reads `GITHUB_RUN_ID`.
|
|
31
43
|
|
|
32
44
|
## The runner cache
|
|
33
45
|
|
|
@@ -51,9 +63,12 @@ When `HOME` is unset it works the same way, and prints to stderr that the runner
|
|
|
51
63
|
```sh
|
|
52
64
|
checks-mutation-baseline <incremental-dest> [mutation-json-dest]
|
|
53
65
|
checks-mutation-baseline --full <mutation-json-dest>
|
|
66
|
+
checks-mutation-baseline --prune [--dry-run]
|
|
54
67
|
```
|
|
55
68
|
|
|
56
69
|
`--full` restores only the report of a run that started with no state.
|
|
70
|
+
`--prune` deletes superseded baselines after an upload.
|
|
71
|
+
`--dry-run` lists what `--prune` would delete, and deletes nothing.
|
|
57
72
|
|
|
58
73
|
## Exit codes
|
|
59
74
|
|
|
@@ -63,6 +78,11 @@ checks-mutation-baseline --full <mutation-json-dest>
|
|
|
63
78
|
| 2 | the arguments do not parse, `gh` cannot list the runs, the cache cannot be written, or another job removed the entry before it was copied |
|
|
64
79
|
|
|
65
80
|
A failed artifact lookup or download prints the `gh` error and moves on, as a missing artifact does.
|
|
81
|
+
With `--prune`, `0` means it deleted the superseded artifacts, found none, or could not delete some.
|
|
82
|
+
`2` means the arguments do not parse, or `gh` cannot list the runs, a run's artifacts or the repository's artifacts.
|
|
83
|
+
A failed delete prints a `::warning::` annotation naming the artifact and the `gh` error, moves on, and the summary names the artifacts it could not delete.
|
|
84
|
+
A failed delete still exits `0`, since a failed step fails the `main` run and the restore then skips the baseline that run uploaded.
|
|
85
|
+
The next `main` run's prune deletes what this one missed.
|
|
66
86
|
|
|
67
87
|
## Sample output
|
|
68
88
|
|
|
@@ -72,6 +92,12 @@ mutation-baseline: restored it from the runner's cache, mutation-baseline artifa
|
|
|
72
92
|
|
|
73
93
|
A miss prints `downloaded it into the runner's cache` instead, and a hosted runner prints `downloaded it`.
|
|
74
94
|
|
|
95
|
+
```
|
|
96
|
+
mutation-baseline: deleted artifacts 11542133901, 11553708473, 11632727585; kept mutation-baseline artifact 11670749656 from run 38055231501
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With `--dry-run` the summary says what it would delete and what it would keep.
|
|
100
|
+
|
|
75
101
|
## When it runs
|
|
76
102
|
|
|
77
103
|
Only a CI step the repository writes runs it.
|
|
@@ -84,6 +110,27 @@ A baseline job restores the state the previous `main` run left before an increme
|
|
|
84
110
|
run: bunx checks-mutation-baseline reports/stryker-incremental.json
|
|
85
111
|
```
|
|
86
112
|
|
|
113
|
+
A baseline job uploads the state the incremental run wrote, then on `main` prunes the superseded baselines:
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
- uses: actions/upload-artifact@v4
|
|
117
|
+
with:
|
|
118
|
+
name: mutation-baseline
|
|
119
|
+
path: |
|
|
120
|
+
reports/mutation/mutation.json
|
|
121
|
+
reports/stryker-incremental.json
|
|
122
|
+
retention-days: 7
|
|
123
|
+
- name: Prune superseded baselines
|
|
124
|
+
if: github.ref == 'refs/heads/main'
|
|
125
|
+
env:
|
|
126
|
+
GH_TOKEN: ${{ github.token }}
|
|
127
|
+
run: bunx checks-mutation-baseline --prune
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The incremental upload keeps 7 days of retention, since the restore reads only the newest `main` run.
|
|
131
|
+
The full upload keeps 30 days, since full runs start weekly.
|
|
132
|
+
A pull request's scope step loses its coverage when no full baseline outlives the gap.
|
|
133
|
+
|
|
87
134
|
A pull request's scope step restores the full report with `--full`:
|
|
88
135
|
|
|
89
136
|
```yaml
|
package/package.json
CHANGED
|
@@ -67,13 +67,21 @@ export const TEMPLATES: Readonly<Record<Kind, Template>> = {
|
|
|
67
67
|
lead: ["<The concept in two to four sentences: what this is, who it is for, why you would use it.>"],
|
|
68
68
|
sections: [
|
|
69
69
|
BEFORE_YOU_BEGIN,
|
|
70
|
-
fixed("Install", REQUIRED, [
|
|
70
|
+
fixed("Install", REQUIRED, [
|
|
71
|
+
"To install <name>:",
|
|
72
|
+
"",
|
|
73
|
+
"1. <step>",
|
|
74
|
+
"1. <step>",
|
|
75
|
+
"",
|
|
76
|
+
"<What you see when it worked, written only from what the source shows.>",
|
|
77
|
+
"<Leave this line out when the source shows nothing.>",
|
|
78
|
+
]),
|
|
71
79
|
open("<Everyday task, verb first, or what the reader looks up>", "any", REQUIRED, [
|
|
72
80
|
...STEPS,
|
|
73
81
|
"",
|
|
74
82
|
"<Or, for what the reader looks up, a table or a list with no steps.>",
|
|
75
83
|
]),
|
|
76
|
-
fixed("Where things are",
|
|
84
|
+
fixed("Where things are", optional("there is no path worth mapping yet"), ["| Path | What it holds |", "| --- | --- |"]),
|
|
77
85
|
TROUBLESHOOTING,
|
|
78
86
|
RELATED_TOPICS,
|
|
79
87
|
],
|
|
@@ -10,7 +10,7 @@ class BaselineFailure extends Schema.TaggedError<BaselineFailure>()("BaselineFai
|
|
|
10
10
|
}) {}
|
|
11
11
|
|
|
12
12
|
const NAME = "checks-mutation-baseline";
|
|
13
|
-
const USAGE = "usage: checks-mutation-baseline <incremental-dest> [mutation-json-dest] | --full <mutation-json-dest>";
|
|
13
|
+
const USAGE = "usage: checks-mutation-baseline <incremental-dest> [mutation-json-dest] | --full <mutation-json-dest> | --prune [--dry-run]";
|
|
14
14
|
const WORKFLOW = "mutation.yml";
|
|
15
15
|
const BRANCH = "main";
|
|
16
16
|
const INCREMENTAL = "stryker-incremental.json";
|
|
@@ -21,6 +21,7 @@ const LATEST_ARTIFACT = "mutation-baseline";
|
|
|
21
21
|
const FULL_ARTIFACT = "mutation-baseline-full";
|
|
22
22
|
const LATEST_LIMIT = 20;
|
|
23
23
|
const FULL_LIMIT = 50;
|
|
24
|
+
const PAGE_SIZE = 100;
|
|
24
25
|
const FULL_EVENTS = ["schedule", "workflow_dispatch"];
|
|
25
26
|
|
|
26
27
|
// The first of `from` the artifact holds is copied to `to`.
|
|
@@ -33,7 +34,7 @@ const Artifact = Schema.Struct({
|
|
|
33
34
|
id: Schema.Int,
|
|
34
35
|
name: Schema.String,
|
|
35
36
|
expired: Schema.Boolean,
|
|
36
|
-
workflow_run: Schema.Struct({ repository_id: Schema.Int }),
|
|
37
|
+
workflow_run: Schema.Struct({ id: Schema.Int, repository_id: Schema.Int }),
|
|
37
38
|
});
|
|
38
39
|
type Artifact = typeof Artifact.Type;
|
|
39
40
|
|
|
@@ -67,12 +68,66 @@ const latestCandidates = Effect.gen(function* () {
|
|
|
67
68
|
return newest === undefined ? [] : [newest.databaseId];
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
// All non-pull-request runs newest first: a newer full run holds no baseline, and the newest baseline must survive it.
|
|
72
|
+
const latestRuns = listed(undefined, LATEST_LIMIT).pipe(Effect.map((runs) => newestFirst(runs.filter((run) => run.event !== "pull_request"))));
|
|
73
|
+
|
|
70
74
|
// Each full event is listed on its own, because one shared window lets push runs crowd out every full run.
|
|
71
75
|
const fullCandidates = Effect.gen(function* () {
|
|
72
76
|
const perEvent = yield* Effect.forEach(FULL_EVENTS, (event) => listed(event, FULL_LIMIT));
|
|
73
77
|
return newestFirst(perEvent.flat());
|
|
74
78
|
});
|
|
75
79
|
|
|
80
|
+
// A successful run uploaded the files, so the newest unexpired artifact is the one the restore takes.
|
|
81
|
+
// A failed lookup aborts rather than counting as absent, since that would delete the artifact the restore takes.
|
|
82
|
+
const keptArtifact = Effect.fn("keptArtifact")(function* (runs: readonly number[], name: string) {
|
|
83
|
+
for (const run of runs) {
|
|
84
|
+
const artifact = yield* artifactOf(run, name);
|
|
85
|
+
if (Option.isSome(artifact)) return Option.some<Kept>({ run, id: artifact.value.id, name });
|
|
86
|
+
}
|
|
87
|
+
return Option.none<Kept>();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const everyArtifact = Effect.fn("everyArtifact")(function* () {
|
|
91
|
+
const artifacts: Artifact[] = [];
|
|
92
|
+
let page = 1;
|
|
93
|
+
while (true) {
|
|
94
|
+
const batch = yield* gh(["api", `repos/{owner}/{repo}/actions/artifacts?per_page=${PAGE_SIZE}&page=${page}`]).pipe(
|
|
95
|
+
Effect.flatMap(decodeArtifacts),
|
|
96
|
+
Effect.mapError((cause) => new BaselineFailure({ message: cause.message })),
|
|
97
|
+
);
|
|
98
|
+
artifacts.push(...batch.artifacts);
|
|
99
|
+
if (batch.artifacts.length < PAGE_SIZE) return artifacts;
|
|
100
|
+
page += 1;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const missedDeletion = Effect.fn("missedDeletion")(function* (artifact: Artifact) {
|
|
105
|
+
return yield* gh(["api", "-X", "DELETE", `repos/{owner}/{repo}/actions/artifacts/${artifact.id}`]).pipe(
|
|
106
|
+
Effect.as(Option.none<Missed>()),
|
|
107
|
+
Effect.catch((failure) => Effect.succeedSome<Missed>({ id: artifact.id, name: artifact.name, error: failure.message })),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const pruneBaselines = Effect.fn("pruneBaselines")(function* (dryRun: boolean) {
|
|
112
|
+
const running = yield* Config.Int("GITHUB_RUN_ID").pipe(Config.option);
|
|
113
|
+
const latest = yield* keptArtifact(yield* latestRuns, LATEST_ARTIFACT);
|
|
114
|
+
const full = yield* keptArtifact(yield* fullCandidates, FULL_ARTIFACT);
|
|
115
|
+
const artifacts = yield* everyArtifact();
|
|
116
|
+
const keeping = [...Option.toArray(latest), ...Option.toArray(full), ...uploadedBy(artifacts, running)];
|
|
117
|
+
const victims = superseded(artifacts, new Set(keeping.map((keep) => keep.id)));
|
|
118
|
+
const removed: number[] = [];
|
|
119
|
+
const missed: Missed[] = [];
|
|
120
|
+
if (!dryRun) {
|
|
121
|
+
for (const victim of victims) {
|
|
122
|
+
const failed = yield* missedDeletion(victim);
|
|
123
|
+
if (Option.isSome(failed)) missed.push(failed.value);
|
|
124
|
+
else removed.push(victim.id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
yield* Console.log(pruneReport(dryRun, dryRun ? victims.map((victim) => victim.id) : removed, missed, keeping));
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
|
|
76
131
|
const artifactOf = Effect.fn("artifactOf")(function* (run: number, name: string) {
|
|
77
132
|
const found = yield* gh(["api", `repos/{owner}/{repo}/actions/runs/${run}/artifacts?name=${name}`]).pipe(
|
|
78
133
|
Effect.flatMap(decodeArtifacts),
|
|
@@ -98,6 +153,37 @@ const downloaded = Effect.fn("downloaded")(function* (run: number, artifact: Art
|
|
|
98
153
|
return fetched && (yield* holds(dir, needed));
|
|
99
154
|
});
|
|
100
155
|
|
|
156
|
+
export type Kept = { readonly run: number; readonly id: number; readonly name: string };
|
|
157
|
+
|
|
158
|
+
export type Missed = { readonly id: number; readonly name: string; readonly error: string };
|
|
159
|
+
|
|
160
|
+
const isBaseline = (artifact: Artifact): boolean => (artifact.name === LATEST_ARTIFACT || artifact.name === FULL_ARTIFACT) && !artifact.expired;
|
|
161
|
+
|
|
162
|
+
// The restore reads only successful runs, so the running run's own uploads join the keepers rather than replace them.
|
|
163
|
+
export function uploadedBy(artifacts: readonly Artifact[], running: Option.Option<number>): readonly Kept[] {
|
|
164
|
+
if (Option.isNone(running)) return [];
|
|
165
|
+
return artifacts
|
|
166
|
+
.filter((artifact) => isBaseline(artifact) && artifact.workflow_run.id === running.value)
|
|
167
|
+
.sort((one, other) => one.id - other.id)
|
|
168
|
+
.map((artifact) => ({ run: running.value, id: artifact.id, name: artifact.name }));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function superseded(artifacts: readonly Artifact[], kept: ReadonlySet<number>): readonly Artifact[] {
|
|
172
|
+
return artifacts.filter((artifact) => isBaseline(artifact) && !kept.has(artifact.id)).sort((one, other) => one.id - other.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// A workflow command ends at a newline, so the error's own newlines and percent signs are escaped.
|
|
176
|
+
const annotated = (text: string): string => text.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
177
|
+
|
|
178
|
+
export function pruneReport(dryRun: boolean, removed: readonly number[], missed: readonly Missed[], keeping: readonly Kept[]): string {
|
|
179
|
+
const kept = keeping.length === 0 ? "no baseline artifacts" : keeping.map((keep) => `${keep.name} artifact ${keep.id} from run ${keep.run}`).join(", ");
|
|
180
|
+
const deleted = removed.length === 0 ? "nothing to delete" : `deleted artifacts ${removed.join(", ")}`;
|
|
181
|
+
if (dryRun) return `mutation-baseline: dry run, ${removed.length === 0 ? "nothing to delete" : `would delete artifacts ${removed.join(", ")}`}; would keep ${kept}`;
|
|
182
|
+
if (missed.length === 0) return `mutation-baseline: ${deleted}; kept ${kept}`;
|
|
183
|
+
const warnings = missed.map((miss) => `::warning::mutation-baseline: could not delete ${miss.name} artifact ${miss.id}: ${annotated(miss.error)}`);
|
|
184
|
+
return [...warnings, `mutation-baseline: ${deleted}; kept ${kept}; could not delete artifacts ${missed.map((miss) => miss.id).join(", ")}`].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
101
187
|
export function pruned(entries: readonly string[], kept: number, restoring: string): readonly string[] {
|
|
102
188
|
return entries
|
|
103
189
|
.filter((entry) => /^\d+$/.test(entry))
|
|
@@ -182,9 +268,16 @@ const runnerCache = Effect.gen(function* () {
|
|
|
182
268
|
// An artifact that lacks `needed` counts as no baseline, and `optional` copies only what the artifact holds.
|
|
183
269
|
type Request = { readonly full: boolean; readonly needed: Copy; readonly optional: readonly Copy[] };
|
|
184
270
|
|
|
271
|
+
type PruneRequest = { readonly prune: true; readonly dryRun: boolean };
|
|
272
|
+
|
|
185
273
|
const isDestination = (arg: string | undefined): arg is string => arg !== undefined && !arg.startsWith("-");
|
|
186
274
|
|
|
187
|
-
function parsed(args: readonly string[]): Request | undefined {
|
|
275
|
+
function parsed(args: readonly string[]): Request | PruneRequest | undefined {
|
|
276
|
+
if (args[0] === "--prune") {
|
|
277
|
+
const [flag, ...extra] = args.slice(1);
|
|
278
|
+
if ((flag === undefined || flag === "--dry-run") && extra.length === 0) return { prune: true, dryRun: flag !== undefined };
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
188
281
|
if (args[0] === "--full") {
|
|
189
282
|
const [report, ...extra] = args.slice(1);
|
|
190
283
|
if (!isDestination(report) || extra.length > 0) return undefined;
|
|
@@ -198,6 +291,7 @@ function parsed(args: readonly string[]): Request | undefined {
|
|
|
198
291
|
const mutationBaseline = Effect.gen(function* () {
|
|
199
292
|
const request = parsed(process.argv.slice(2));
|
|
200
293
|
if (request === undefined) return yield* new Usage({ message: USAGE });
|
|
294
|
+
if ("prune" in request) return yield* pruneBaselines(request.dryRun);
|
|
201
295
|
const name = request.full ? FULL_ARTIFACT : LATEST_ARTIFACT;
|
|
202
296
|
const root = yield* runnerCache;
|
|
203
297
|
for (const run of yield* request.full ? fullCandidates : latestCandidates) {
|