@avi2dg/checks 0.63.1 → 0.65.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/docs/bins.md +3 -4
- package/docs/design.md +3 -2
- package/docs/gates/checks-advisories.md +8 -6
- package/docs/gates/checks-ci-wiring.md +45 -7
- package/docs/gates/checks-flake.md +6 -7
- package/docs/gates/checks-lint.md +1 -1
- package/docs/gates/checks-mutation-scope.md +1 -1
- package/docs/gates/checks-mutation.md +1 -0
- package/docs/gates/checks-night-run.md +32 -26
- package/docs/gates/checks-release-notes.md +5 -0
- package/docs/gates/checks-release-pr.md +2 -1
- package/docs/gates/checks-release-push.md +4 -3
- package/docs/gates/checks-setup.md +2 -0
- package/docs/gates/checks-test.md +28 -1
- package/docs/how-to/set-up-a-repository.md +4 -0
- package/package.json +5 -1
- package/src/delivery/ci-wiring.ts +20 -7
- package/src/delivery/job-timeout.ts +30 -0
- package/src/delivery/release-push.ts +1 -1
- package/src/delivery/release-schedule.ts +53 -18
- package/src/delivery/schedule-policy.ts +64 -0
- package/src/delivery/setup-workflow.ts +43 -0
- package/src/delivery/setup.ts +2 -39
- package/src/testing/affected-tests.ts +261 -0
- package/src/testing/mutation-scope-graph.ts +13 -0
- package/src/testing/mutation-scope.ts +2 -9
- package/src/testing/night-run-findings.ts +7 -4
- package/src/testing/night-run.ts +43 -16
- package/src/testing/test-skips.ts +3 -3
- package/src/testing/test.ts +57 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "avi2d",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"src/delivery/release-pr.ts",
|
|
45
45
|
"src/delivery/release-preflight.ts",
|
|
46
46
|
"src/delivery/release-schedule.ts",
|
|
47
|
+
"src/delivery/schedule-policy.ts",
|
|
47
48
|
"src/delivery/release-publish.ts",
|
|
48
49
|
"src/delivery/release-push.ts",
|
|
49
50
|
"src/delivery/release-tag.ts",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"src/testing/block-screens.ts",
|
|
54
55
|
"src/testing/test.ts",
|
|
55
56
|
"src/testing/test-skips.ts",
|
|
57
|
+
"src/testing/affected-tests.ts",
|
|
56
58
|
"src/core/swc.ts",
|
|
57
59
|
"src/testing/test-report.ts",
|
|
58
60
|
"src/testing/flake.ts",
|
|
@@ -76,11 +78,13 @@
|
|
|
76
78
|
"src/delivery/workflow-references.ts",
|
|
77
79
|
"src/delivery/bun-pin.ts",
|
|
78
80
|
"src/delivery/mutation-job.ts",
|
|
81
|
+
"src/delivery/job-timeout.ts",
|
|
79
82
|
"src/delivery/runner-policy.ts",
|
|
80
83
|
"src/delivery/secrets.ts",
|
|
81
84
|
"src/delivery/secrets-pack.ts",
|
|
82
85
|
"src/delivery/pr-text.ts",
|
|
83
86
|
"src/delivery/setup.ts",
|
|
87
|
+
"src/delivery/setup-workflow.ts",
|
|
84
88
|
"src/delivery/gitleaks.ts",
|
|
85
89
|
"src/delivery/gitleaks.toml",
|
|
86
90
|
"src/quality/comment-matchers.ts",
|
|
@@ -4,9 +4,11 @@ import { defaultBranch, git } from "../core/git.ts";
|
|
|
4
4
|
import { runMain } from "../core/main.ts";
|
|
5
5
|
import { ENTRY_POINT, KIT_GATES, type KitGate } from "../core/gates.ts";
|
|
6
6
|
import { findBunPinFaults, formatBunPinReport, readBunPinPolicy } from "./bun-pin.ts";
|
|
7
|
+
import { findLongJobs, formatLongJobReport } from "./job-timeout.ts";
|
|
7
8
|
import { readScheduleFiles, readShellFiles } from "./mutation-job.ts";
|
|
8
9
|
import { findScheduledReleases, formatScheduleReport } from "./release-schedule.ts";
|
|
9
10
|
import { findRunnerFaults, formatRunnerReport, jobRuns, readRunnerConfig } from "./runner-policy.ts";
|
|
11
|
+
import { findScheduledRuns, formatScheduledRunReport } from "./schedule-policy.ts";
|
|
10
12
|
import { invokes, mentions, plainCommand, type Command } from "./shell-command.ts";
|
|
11
13
|
import { findMissingWorkflows, formatMissingReport, isRecord, names, workflowJobs, type Workflow } from "./workflow-references.ts";
|
|
12
14
|
|
|
@@ -65,6 +67,7 @@ const TITLE_LINT = "bunx checks-commit-title";
|
|
|
65
67
|
const TITLE_CALLS = ["bun src/delivery/commit-title.ts", "./node_modules/.bin/commitlint"];
|
|
66
68
|
const ScriptsManifest = Schema.Struct({
|
|
67
69
|
scripts: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
70
|
+
private: Schema.optionalKey(Schema.Boolean),
|
|
68
71
|
});
|
|
69
72
|
// Without these a pull_request workflow never sees the commits a pull request pushes.
|
|
70
73
|
const GATING_TYPES = ["opened", "synchronize"];
|
|
@@ -271,16 +274,19 @@ export const parseWorkflow = (path: string, text: string): Effect.Effect<Workflo
|
|
|
271
274
|
catch: (error) => new WiringError({ message: `cannot parse ${path}: ${String(error)}` }),
|
|
272
275
|
});
|
|
273
276
|
|
|
274
|
-
const
|
|
277
|
+
const readManifest = Effect.fn("readManifest")(function* (root: string) {
|
|
275
278
|
const fs = yield* FileSystem.FileSystem;
|
|
276
279
|
const file = (yield* Path.Path).join(root, "package.json");
|
|
277
|
-
|
|
280
|
+
return (yield* fs.exists(file))
|
|
278
281
|
? yield* fs.readFileString(file).pipe(
|
|
279
282
|
Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(ScriptsManifest))),
|
|
280
283
|
Effect.mapError((cause) => new WiringError({ message: `cannot read ${file}: ${cause.message}` })),
|
|
281
284
|
)
|
|
282
285
|
: ScriptsManifest.make({});
|
|
283
|
-
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const readScripts = Effect.fn("readScripts")(function* (root: string) {
|
|
289
|
+
return new Map(Object.entries((yield* readManifest(root)).scripts ?? {}));
|
|
284
290
|
});
|
|
285
291
|
|
|
286
292
|
export const readDeclaration = Effect.fn("readDeclaration")(function* (root: string) {
|
|
@@ -322,17 +328,24 @@ const wiring = Effect.gen(function* () {
|
|
|
322
328
|
yield* gaps.length > 0 ? Console.error(gapReport) : Console.log(gapReport);
|
|
323
329
|
const missing = findMissingWorkflows(workflows);
|
|
324
330
|
if (missing.length > 0) yield* Console.error(formatMissingReport(missing));
|
|
325
|
-
const
|
|
331
|
+
const manifest = yield* readManifest(root);
|
|
332
|
+
const scripts = new Map(Object.entries(manifest.scripts ?? {}));
|
|
333
|
+
const visibility = yield* readVisibility;
|
|
326
334
|
const runs = workflows.flatMap((workflow) => workflowJobs(workflow).flatMap(([, job]) => jobRuns(workflow, job)));
|
|
327
335
|
const runners = yield* readRunnerConfig(root);
|
|
328
|
-
const faults = findRunnerFaults({ visibility
|
|
336
|
+
const faults = findRunnerFaults({ visibility, scripts, files: yield* readShellFiles(root, scripts, runs), runners }, workflows);
|
|
329
337
|
if (faults.length > 0) yield* Console.error(formatRunnerReport(faults));
|
|
330
338
|
const bunFaults = findBunPinFaults(yield* readBunPinPolicy(root, workflows), workflows);
|
|
331
339
|
if (bunFaults.length > 0) yield* Console.error(formatBunPinReport(bunFaults));
|
|
332
340
|
const scheduleFiles = yield* readScheduleFiles(root, scripts, runs);
|
|
333
|
-
const
|
|
341
|
+
const owner = { visibility, publishes: manifest.private !== true };
|
|
342
|
+
const scheduled = findScheduledReleases(workflows, { scripts, files: scheduleFiles }, owner);
|
|
334
343
|
if (scheduled.length > 0) yield* Console.error(formatScheduleReport(scheduled));
|
|
335
|
-
|
|
344
|
+
const scheduledRuns = findScheduledRuns(workflows, { scripts, files: scheduleFiles }, owner);
|
|
345
|
+
if (scheduledRuns.length > 0) yield* Console.error(formatScheduledRunReport(scheduledRuns));
|
|
346
|
+
const longJobs = findLongJobs(workflows);
|
|
347
|
+
if (longJobs.length > 0) yield* Console.error(formatLongJobReport(longJobs));
|
|
348
|
+
return [gaps, faults, missing, bunFaults, scheduled, scheduledRuns, longJobs].every((found) => found.length === 0);
|
|
336
349
|
});
|
|
337
350
|
|
|
338
351
|
if (import.meta.main) runMain("ci-wiring", wiring);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { workflowJobs, type Workflow } from "./workflow-references.ts";
|
|
2
|
+
|
|
3
|
+
export type LongJob = {
|
|
4
|
+
readonly location: string;
|
|
5
|
+
readonly fault: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const LIMIT = 60;
|
|
9
|
+
const SET = `set timeout-minutes to a number of at most ${LIMIT}`;
|
|
10
|
+
|
|
11
|
+
function timeoutFault(timeout: unknown): string | undefined {
|
|
12
|
+
if (timeout === undefined) return "the job sets no timeout-minutes, so a hung run holds its runner for GitHub's 360 minute default; " + SET;
|
|
13
|
+
if (typeof timeout !== "number") return "its timeout-minutes cannot be shown to stay within an hour before the run; " + SET;
|
|
14
|
+
return timeout > LIMIT ? `its timeout-minutes of ${timeout} lets the job run for hours; ${SET}` : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A job that calls a reusable workflow cannot set timeout-minutes, and the called workflow's own jobs are judged where it lives.
|
|
18
|
+
export function findLongJobs(workflows: readonly Workflow[]): readonly LongJob[] {
|
|
19
|
+
return workflows.flatMap((workflow) =>
|
|
20
|
+
workflowJobs(workflow).flatMap(([id, job]) => {
|
|
21
|
+
if (job["uses"] !== undefined) return [];
|
|
22
|
+
const fault = timeoutFault(job["timeout-minutes"]);
|
|
23
|
+
return fault === undefined ? [] : [{ location: `${workflow.path} job ${id}`, fault }];
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatLongJobReport(jobs: readonly LongJob[]): string {
|
|
29
|
+
return [`ci-wiring: ${jobs.length} job(s) can run for hours:`, ...jobs.map(({ location, fault }) => ` ${location}: ${fault}`)].join("\n");
|
|
30
|
+
}
|
|
@@ -12,7 +12,7 @@ const LABEL = "release-push";
|
|
|
12
12
|
const USAGE = "usage: release-push.ts <release-workflow> <workflow>...";
|
|
13
13
|
const UNTAGGED = "tag the release commit with checks-release-tag, whose refusal says what to fix";
|
|
14
14
|
const POLL = Duration.seconds(15);
|
|
15
|
-
const POLLS =
|
|
15
|
+
const POLLS = 88;
|
|
16
16
|
|
|
17
17
|
const Run = Schema.Struct({ path: Schema.String, status: Schema.String, conclusion: Schema.NullOr(Schema.String), html_url: Schema.String });
|
|
18
18
|
type Run = typeof Run.Type;
|
|
@@ -5,6 +5,7 @@ import { isRecord, locate, names, workflowJobs, type Workflow } from "./workflow
|
|
|
5
5
|
|
|
6
6
|
export type ScheduledRelease = {
|
|
7
7
|
readonly location: string;
|
|
8
|
+
readonly fault: string;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export type ScheduleContext = {
|
|
@@ -12,18 +13,26 @@ export type ScheduleContext = {
|
|
|
12
13
|
readonly files: ReadonlyMap<string, string>;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
export type ScheduleOwner = {
|
|
17
|
+
readonly visibility: "private" | "public" | "unknown";
|
|
18
|
+
readonly publishes: boolean;
|
|
19
|
+
};
|
|
16
20
|
|
|
17
21
|
const RELEASE_BINS = ["pr", "push", "tag", "publish"].flatMap((bin) => [`checks-release-${bin}`, `release-${bin}.ts`]);
|
|
22
|
+
const PUBLISH_BINS = ["checks-release-publish", "release-publish.ts"];
|
|
23
|
+
const NUMBER = /^\d+$/;
|
|
24
|
+
const BY_HAND = "drop the schedule, so a release runs only when someone starts it";
|
|
25
|
+
|
|
26
|
+
function isBinIn(bins: readonly string[], word: string): boolean {
|
|
27
|
+
return bins.some((name) => word === name || word.endsWith(`/${name}`));
|
|
28
|
+
}
|
|
18
29
|
|
|
19
|
-
function
|
|
20
|
-
return
|
|
30
|
+
function isPublishCommand([head = "", ...rest]: Command): boolean {
|
|
31
|
+
return (head === "npm" && rest[0] === "publish") || isBinIn(PUBLISH_BINS, head);
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
function isReleaseCommand(program: Command): boolean {
|
|
24
|
-
|
|
25
|
-
if (head === "npm" && rest[0] === "publish") return true;
|
|
26
|
-
return isReleaseBin(head);
|
|
35
|
+
return isPublishCommand(program) || isBinIn(RELEASE_BINS, program[0] ?? "");
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
function localUses(job: Readonly<Record<string, unknown>>): string | undefined {
|
|
@@ -31,9 +40,10 @@ function localUses(job: Readonly<Record<string, unknown>>): string | undefined {
|
|
|
31
40
|
return typeof uses === "string" && uses.startsWith("./") ? uses.slice(2) : undefined;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
function
|
|
43
|
+
function workflowRuns(
|
|
35
44
|
workflow: Workflow,
|
|
36
45
|
context: ScheduleContext,
|
|
46
|
+
isBin: (program: Command) => boolean,
|
|
37
47
|
byPath: ReadonlyMap<string, Workflow>,
|
|
38
48
|
walkedWorkflows: readonly string[],
|
|
39
49
|
): boolean {
|
|
@@ -41,32 +51,57 @@ function workflowReleases(
|
|
|
41
51
|
if (
|
|
42
52
|
workflowJobs(workflow)
|
|
43
53
|
.flatMap(([, job]) => jobRuns(workflow, job))
|
|
44
|
-
.some((run) => runsBin(run, context.scripts, files,
|
|
54
|
+
.some((run) => runsBin(run, context.scripts, files, isBin, true))
|
|
45
55
|
)
|
|
46
56
|
return true;
|
|
47
57
|
return workflowJobs(workflow).some(([, job]) => {
|
|
48
58
|
const called = localUses(job);
|
|
49
59
|
if (called === undefined || walkedWorkflows.includes(called)) return false;
|
|
50
60
|
const next = byPath.get(called);
|
|
51
|
-
return next !== undefined &&
|
|
61
|
+
return next !== undefined && workflowRuns(next, context, isBin, byPath, [...walkedWorkflows, called]);
|
|
52
62
|
});
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
function
|
|
65
|
+
function crons(document: unknown): readonly unknown[] {
|
|
66
|
+
const on = isRecord(document) ? document["on"] : undefined;
|
|
67
|
+
const schedule = isRecord(on) ? on["schedule"] : undefined;
|
|
68
|
+
return Array.isArray(schedule) ? schedule.map((entry) => (isRecord(entry) ? entry["cron"] : undefined)) : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// At most once a day is one minute and one hour, whatever the day, month and weekday fields allow.
|
|
72
|
+
export function atMostDaily(cron: unknown): boolean {
|
|
73
|
+
if (typeof cron !== "string") return false;
|
|
74
|
+
const fields = cron.trim().split(/\s+/);
|
|
75
|
+
return fields.length === 5 && NUMBER.test(fields[0] ?? "") && NUMBER.test(fields[1] ?? "");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A run outside CI cannot read whether the repository is private, so only a package.json that sets private refuses there.
|
|
79
|
+
function releaseFault(workflow: Workflow, owner: ScheduleOwner, publishesToNpm: boolean): string | undefined {
|
|
80
|
+
if (owner.visibility === "private" || !owner.publishes || !publishesToNpm) return "only a public package that publishes to npm releases on a schedule; " + BY_HAND;
|
|
81
|
+
const entries = crons(workflow.document);
|
|
82
|
+
if (entries.length === 1 && entries.every(atMostDaily)) return undefined;
|
|
83
|
+
return 'the release runs more than once a day; keep one cron at a minute and an hour, such as "7 3 * * *"';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function schedules(document: unknown): boolean {
|
|
56
87
|
const on = isRecord(document) ? document["on"] : undefined;
|
|
57
88
|
return isRecord(on) ? "schedule" in on : (names(on) ?? []).includes("schedule");
|
|
58
89
|
}
|
|
59
90
|
|
|
60
|
-
export function
|
|
91
|
+
export function releasesOnSchedule(workflows: readonly Workflow[], context: ScheduleContext): readonly Workflow[] {
|
|
61
92
|
const byPath = new Map(workflows.map((workflow) => [workflow.path, workflow] as const));
|
|
62
|
-
return workflows
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
return workflows.filter((workflow) => schedules(workflow.document) && workflowRuns(workflow, context, isReleaseCommand, byPath, [workflow.path]));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function findScheduledReleases(workflows: readonly Workflow[], context: ScheduleContext, owner: ScheduleOwner): readonly ScheduledRelease[] {
|
|
97
|
+
const byPath = new Map(workflows.map((workflow) => [workflow.path, workflow] as const));
|
|
98
|
+
const publishesToNpm = workflows.some((workflow) => workflowRuns(workflow, context, isPublishCommand, byPath, [workflow.path]));
|
|
99
|
+
return releasesOnSchedule(workflows, context).flatMap((workflow) => {
|
|
100
|
+
const fault = releaseFault(workflow, owner, publishesToNpm);
|
|
101
|
+
return fault === undefined ? [] : [{ location: locate(workflow.path, workflow.text.split("\n"))(["on"], "schedule", "on schedule"), fault }];
|
|
102
|
+
});
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
export function formatScheduleReport(scheduled: readonly ScheduledRelease[]): string {
|
|
68
|
-
return [
|
|
69
|
-
`ci-wiring: ${scheduled.length} workflow(s) release on a schedule:`,
|
|
70
|
-
...scheduled.map(({ location }) => ` ${location}: drop the schedule, so a release runs only when someone starts it`),
|
|
71
|
-
].join("\n");
|
|
106
|
+
return [`ci-wiring: ${scheduled.length} workflow(s) release on a schedule:`, ...scheduled.map(({ location, fault }) => ` ${location}: ${fault}`)].join("\n");
|
|
72
107
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { runsBin } from "./mutation-job.ts";
|
|
2
|
+
import { releasesOnSchedule, schedules } from "./release-schedule.ts";
|
|
3
|
+
import { jobRuns } from "./runner-policy.ts";
|
|
4
|
+
import { invokes, plainCommand, type Command } from "./shell-command.ts";
|
|
5
|
+
import { isRecord, locate, workflowJobs, type Workflow } from "./workflow-references.ts";
|
|
6
|
+
import type { ScheduleContext, ScheduleOwner } from "./release-schedule.ts";
|
|
7
|
+
|
|
8
|
+
export type ScheduledRun = {
|
|
9
|
+
readonly location: string;
|
|
10
|
+
readonly fault: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ADVISORY_BINS = ["checks-advisories", "advisories.ts"];
|
|
14
|
+
const INSTALL = ["bun", "install"];
|
|
15
|
+
const NUMBER = /^\d+$/;
|
|
16
|
+
const WEEKDAY = /^(?:[0-7]|sun|mon|tue|wed|thu|fri|sat)$/i;
|
|
17
|
+
const BY_HAND = "drop the schedule, so it runs only when someone starts it with gh workflow run";
|
|
18
|
+
|
|
19
|
+
function isAdvisoryBin([head = ""]: Command): boolean {
|
|
20
|
+
return ADVISORY_BINS.some((name) => head === name || head.endsWith(`/${name}`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function crons(document: unknown): readonly unknown[] {
|
|
24
|
+
const on = isRecord(document) ? document["on"] : undefined;
|
|
25
|
+
const schedule = isRecord(on) ? on["schedule"] : undefined;
|
|
26
|
+
return Array.isArray(schedule) ? schedule.map((entry) => (isRecord(entry) ? entry["cron"] : undefined)) : [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Once a week is one minute, one hour and one weekday, on any day of any month.
|
|
30
|
+
export function weekly(cron: unknown): boolean {
|
|
31
|
+
if (typeof cron !== "string") return false;
|
|
32
|
+
const [minute = "", hour = "", day = "", month = "", weekday = "", ...rest] = cron.trim().split(/\s+/);
|
|
33
|
+
return rest.length === 0 && NUMBER.test(minute) && NUMBER.test(hour) && day === "*" && month === "*" && WEEKDAY.test(weekday);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function scansAdvisoriesOnly(workflow: Workflow, context: ScheduleContext): boolean {
|
|
37
|
+
const files = (file: string) => context.files.get(file);
|
|
38
|
+
const runs = workflowJobs(workflow).flatMap(([, job]) => jobRuns(workflow, job));
|
|
39
|
+
const scans = runs.filter((run) => runsBin(run, context.scripts, files, isAdvisoryBin, true));
|
|
40
|
+
return scans.length > 0 && runs.every((run) => scans.includes(run) || invokes(plainCommand(run.script), INSTALL));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function advisoryFault(workflow: Workflow, owner: ScheduleOwner): string | undefined {
|
|
44
|
+
if (owner.visibility === "private") return "a private repository scans advisories by hand; " + BY_HAND;
|
|
45
|
+
if (!owner.publishes) return "a package.json that sets private publishes nothing to npm, so it scans advisories by hand; " + BY_HAND;
|
|
46
|
+
const entries = crons(workflow.document);
|
|
47
|
+
if (entries.length === 1 && entries.every(weekly)) return undefined;
|
|
48
|
+
return 'the advisory scan runs more than once a week; keep one cron at a minute, an hour and a weekday, such as "15 5 * * 1"';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A workflow that releases on a schedule is the release rule's to judge, so this rule passes over it.
|
|
52
|
+
export function findScheduledRuns(workflows: readonly Workflow[], context: ScheduleContext, owner: ScheduleOwner): readonly ScheduledRun[] {
|
|
53
|
+
const releasing = new Set(releasesOnSchedule(workflows, context));
|
|
54
|
+
return workflows.flatMap((workflow) => {
|
|
55
|
+
if (!schedules(workflow.document) || releasing.has(workflow)) return [];
|
|
56
|
+
const fault = scansAdvisoriesOnly(workflow, context) ? advisoryFault(workflow, owner) : BY_HAND;
|
|
57
|
+
if (fault === undefined) return [];
|
|
58
|
+
return [{ location: locate(workflow.path, workflow.text.split("\n"))(["on"], "schedule", "on schedule"), fault }];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatScheduledRunReport(scheduled: readonly ScheduledRun[]): string {
|
|
63
|
+
return [`ci-wiring: ${scheduled.length} workflow(s) run on a schedule:`, ...scheduled.map(({ location, fault }) => ` ${location}: ${fault}`)].join("\n");
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const HOSTED_FALLBACK = "ubuntu-latest";
|
|
2
|
+
const HOSTED_DEFAULT = "${{ vars.CI_RUNS_ON || 'ubuntu-latest' }}";
|
|
3
|
+
|
|
4
|
+
export function ciWorkflow(isPrivate: boolean, hasBuild: boolean): string {
|
|
5
|
+
const lines = [
|
|
6
|
+
"name: ci",
|
|
7
|
+
"",
|
|
8
|
+
"on:",
|
|
9
|
+
" pull_request:",
|
|
10
|
+
" types: [opened, synchronize]",
|
|
11
|
+
"",
|
|
12
|
+
"concurrency:",
|
|
13
|
+
" group: ci-${{ github.event.pull_request.number }}",
|
|
14
|
+
" cancel-in-progress: true",
|
|
15
|
+
"",
|
|
16
|
+
"jobs:",
|
|
17
|
+
" checks:",
|
|
18
|
+
" runs-on: " + (isPrivate ? HOSTED_DEFAULT : HOSTED_FALLBACK),
|
|
19
|
+
" timeout-minutes: 30",
|
|
20
|
+
" steps:",
|
|
21
|
+
" - uses: actions/checkout@v5",
|
|
22
|
+
" with:",
|
|
23
|
+
" fetch-depth: 0",
|
|
24
|
+
" - uses: oven-sh/setup-bun@v2",
|
|
25
|
+
" with:",
|
|
26
|
+
" bun-version-file: .bun-version",
|
|
27
|
+
" - run: bun install --frozen-lockfile",
|
|
28
|
+
];
|
|
29
|
+
if (hasBuild) {
|
|
30
|
+
lines.push(" - run: bun run build", " - run: git diff --exit-code");
|
|
31
|
+
}
|
|
32
|
+
lines.push(" - run: bun run lint", " - run: bun run typecheck", " - run: bun run test");
|
|
33
|
+
lines.push(
|
|
34
|
+
" - id: title",
|
|
35
|
+
" name: lint the title",
|
|
36
|
+
" env:",
|
|
37
|
+
" PR_TITLE: ${{ github.event.pull_request.title }}",
|
|
38
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
39
|
+
" run: bunx checks-commit-title",
|
|
40
|
+
"",
|
|
41
|
+
);
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
package/src/delivery/setup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
3
|
import { runMain, Usage } from "../core/main.ts";
|
|
4
|
+
import { ciWorkflow } from "./setup-workflow.ts";
|
|
4
5
|
import { TEMPLATES } from "../docs/doc-templates.ts";
|
|
5
6
|
import { scriptOptions } from "../testing/test.ts";
|
|
6
7
|
import { withServiceOverrides, type ServiceOverride } from "../quality/effect-scope.ts";
|
|
@@ -10,8 +11,6 @@ import severities from "../quality/presets/effect.language-service.json" with {
|
|
|
10
11
|
const NAME = "checks-setup";
|
|
11
12
|
const USAGE = "usage: " + NAME + " [--private]";
|
|
12
13
|
const PRIVATE = "--private";
|
|
13
|
-
const HOSTED_FALLBACK = "ubuntu-latest";
|
|
14
|
-
const HOSTED_DEFAULT = "${{ vars.CI_RUNS_ON || 'ubuntu-latest' }}";
|
|
15
14
|
const IGNORE_ENTRY = "node_modules/";
|
|
16
15
|
const KIT_ID = "@avi2dg/checks";
|
|
17
16
|
|
|
@@ -116,42 +115,6 @@ const KNIP_CONFIG = [
|
|
|
116
115
|
"",
|
|
117
116
|
].join("\n");
|
|
118
117
|
|
|
119
|
-
function workflow(isPrivate: boolean, hasBuild: boolean): string {
|
|
120
|
-
const lines = [
|
|
121
|
-
"name: ci",
|
|
122
|
-
"",
|
|
123
|
-
"on:",
|
|
124
|
-
" pull_request:",
|
|
125
|
-
" types: [opened, synchronize]",
|
|
126
|
-
"",
|
|
127
|
-
"jobs:",
|
|
128
|
-
" checks:",
|
|
129
|
-
" runs-on: " + (isPrivate ? HOSTED_DEFAULT : HOSTED_FALLBACK),
|
|
130
|
-
" steps:",
|
|
131
|
-
" - uses: actions/checkout@v5",
|
|
132
|
-
" with:",
|
|
133
|
-
" fetch-depth: 0",
|
|
134
|
-
" - uses: oven-sh/setup-bun@v2",
|
|
135
|
-
" with:",
|
|
136
|
-
" bun-version-file: .bun-version",
|
|
137
|
-
" - run: bun install --frozen-lockfile",
|
|
138
|
-
];
|
|
139
|
-
if (hasBuild) {
|
|
140
|
-
lines.push(" - run: bun run build", " - run: git diff --exit-code");
|
|
141
|
-
}
|
|
142
|
-
lines.push(" - run: bun run lint", " - run: bun run typecheck", " - run: bun run test");
|
|
143
|
-
lines.push(
|
|
144
|
-
" - id: title",
|
|
145
|
-
" name: lint the title",
|
|
146
|
-
" env:",
|
|
147
|
-
" PR_TITLE: ${{ github.event.pull_request.title }}",
|
|
148
|
-
" GH_TOKEN: ${{ github.token }}",
|
|
149
|
-
" run: bunx checks-commit-title",
|
|
150
|
-
"",
|
|
151
|
-
);
|
|
152
|
-
return lines.join("\n");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
118
|
const kitText = Effect.fn("kitText")(function* (file: string) {
|
|
156
119
|
const system = yield* FileSystem.FileSystem;
|
|
157
120
|
const path = yield* Path.Path;
|
|
@@ -385,7 +348,7 @@ const check = Effect.gen(function* () {
|
|
|
385
348
|
: OXLINT_CONFIG;
|
|
386
349
|
yield* setupTsconfig(root, oxlintText);
|
|
387
350
|
yield* setupFile(root, "knip.config.ts", KNIP_CONFIG);
|
|
388
|
-
yield* setupFile(root, ".github/workflows/ci.yml",
|
|
351
|
+
yield* setupFile(root, ".github/workflows/ci.yml", ciWorkflow(isPrivate, outcome.hasBuild));
|
|
389
352
|
for (const kind of Object.keys(TEMPLATES)) {
|
|
390
353
|
yield* setupFile(root, "templates/" + kind + ".md", yield* kitText("dist/templates/" + kind + ".md"));
|
|
391
354
|
}
|