@intentius/chant 0.124.29 → 0.124.30
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/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +4 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +27 -5
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +30 -0
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +49 -0
- package/dist/cli/registry.d.ts +17 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +24 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +16 -0
- package/dist/components/pr-run.d.ts +20 -2
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +26 -13
- package/dist/components/wave-plan.d.ts +11 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +26 -0
- package/dist/lifecycle/release-ledger.d.ts +8 -3
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +2 -0
- package/dist/lifecycle/run-ledger.d.ts +1 -1
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +9 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +1 -0
- package/dist/op/activities/shell.d.ts +6 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +1 -1
- package/dist/op/local-executor.d.ts +23 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/runtime.d.ts +27 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtime.js +20 -0
- package/dist/pr-forge.d.ts +23 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +44 -0
- package/dist/pr-loop.d.ts +10 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +7 -0
- package/dist/run-event-stream.d.ts +2 -2
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +2 -2
- package/dist/run-event.d.ts +15 -0
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +2 -0
- package/dist/wave-runtime.mjs +171 -171
- package/dist/workspace/ci-green-forge.d.ts +2 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +2 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +4 -2
- package/dist/workspace/ci-run.d.ts +222 -0
- package/dist/workspace/ci-run.d.ts.map +1 -0
- package/dist/workspace/ci-run.js +464 -0
- package/dist/workspace/ci-serve.d.ts +214 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -0
- package/dist/workspace/ci-serve.js +412 -0
- package/dist/workspace/pr-report.schema.json +5 -0
- package/dist/workspace/run-event.schema.json +14 -3
- package/dist/workspace/status-stewards.d.ts +2 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +1 -0
- package/dist/workspace/status.schema.json +12 -1
- package/package.json +1 -1
- package/src/cli/handlers/build.test.ts +15 -0
- package/src/cli/handlers/build.ts +4 -1
- package/src/cli/handlers/fan-out.test.ts +82 -0
- package/src/cli/handlers/fan-out.ts +34 -5
- package/src/cli/handlers/pr.ts +5 -0
- package/src/cli/main.ts +30 -0
- package/src/cli/parse-args.ts +35 -0
- package/src/cli/registry.ts +17 -1
- package/src/components/pr-pipeline.ts +32 -0
- package/src/components/pr-run.test.ts +127 -0
- package/src/components/pr-run.ts +32 -12
- package/src/components/wave-plan.ts +30 -0
- package/src/lifecycle/release-ledger.ts +9 -3
- package/src/lifecycle/run-ledger.ts +7 -1
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/shell.ts +7 -1
- package/src/op/local-executor.ts +27 -4
- package/src/op/runtime.ts +42 -0
- package/src/pr-forge.test.ts +25 -0
- package/src/pr-forge.ts +62 -0
- package/src/pr-loop.test.ts +13 -1
- package/src/pr-loop.ts +16 -0
- package/src/run-event-stream.ts +4 -3
- package/src/run-event.ts +17 -0
- package/src/workspace/ci-green-forge.ts +2 -0
- package/src/workspace/ci-green.ts +6 -2
- package/src/workspace/ci-run.test.ts +416 -0
- package/src/workspace/ci-run.ts +571 -0
- package/src/workspace/ci-serve.test.ts +268 -0
- package/src/workspace/ci-serve.ts +521 -0
- package/src/workspace/pr-report.schema.json +5 -0
- package/src/workspace/run-event.schema.json +14 -3
- package/src/workspace/status-stewards.ts +3 -0
- package/src/workspace/status.schema.json +12 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4095: `chant ci serve` polls the forge and runs `ci run` plan for each new
|
|
3
|
+
* pull request head and apply for each new target-branch commit, deduped in
|
|
4
|
+
* a state file, one apply per group through a git-ref lease, with each
|
|
5
|
+
* apply's resume record kept on the host, and at most `maxJobs` at once.
|
|
6
|
+
*
|
|
7
|
+
* Every test works in its own temp repository with a bare origin. The forge
|
|
8
|
+
* is a fake client, and the jobs' `chant` is a stub script outside the
|
|
9
|
+
* repository. Each test runs single `--once` passes in-process.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
16
|
+
import { cleanScratch, declaration, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
17
|
+
import { ciServeInterval, ciServeKey, ciServePass, formatCiServeEvent, readCiServeState, type CiServeEvent, type CiServeForge, type CiServeOptions } from "./ci-serve";
|
|
18
|
+
import { acquireLease, releaseLease } from "../lifecycle/lease";
|
|
19
|
+
import { prApplyGroup, prApplyRecordKey } from "../components/pr-pipeline";
|
|
20
|
+
import { parseArgs } from "../cli/parse-args";
|
|
21
|
+
import type { OpenPullRequest } from "../pr-forge";
|
|
22
|
+
|
|
23
|
+
afterAll(cleanScratch);
|
|
24
|
+
|
|
25
|
+
function commit(root: string, file: string): string {
|
|
26
|
+
writeFiles(root, { [file]: `${file} ${Math.random()}\n` });
|
|
27
|
+
for (const args of [["add", "-A"], ["commit", "-q", "-m", file]]) {
|
|
28
|
+
execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd: root, stdio: "ignore" });
|
|
29
|
+
}
|
|
30
|
+
return git(root, "rev-parse", "HEAD");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A clone on main with a bare origin and member `app`. */
|
|
34
|
+
function workspace(): { root: string; app: string; main: string } {
|
|
35
|
+
const origin = scratchDir("chant-ci-serve-origin-");
|
|
36
|
+
git(origin, "init", "-q", "--bare");
|
|
37
|
+
const root = scratchDir("chant-ci-serve-");
|
|
38
|
+
git(root, "init", "-q");
|
|
39
|
+
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
40
|
+
writeFiles(root, {
|
|
41
|
+
"chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }]),
|
|
42
|
+
".gitignore": ".chant/pr/\n.chant/pr-resume/\n",
|
|
43
|
+
"app/a.txt": "a\n",
|
|
44
|
+
});
|
|
45
|
+
git(root, "remote", "add", "origin", origin);
|
|
46
|
+
const main = commit(root, "app/init.txt");
|
|
47
|
+
git(root, "push", "-q", "origin", "main");
|
|
48
|
+
return { root, app: join(root, "app"), main };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A pull request's head commit, on a branch off main, pushed to the forge's refs/pull/<n>/head. */
|
|
52
|
+
function pullHead(root: string, n: number): string {
|
|
53
|
+
git(root, "checkout", "-q", "-b", `pr-${n}`);
|
|
54
|
+
const sha = commit(root, `app/pr-${n}.txt`);
|
|
55
|
+
git(root, "push", "-q", "origin", `${sha}:refs/pull/${n}/head`);
|
|
56
|
+
git(root, "checkout", "-q", "main");
|
|
57
|
+
return sha;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A stand-in for `chant` that keeps its arguments under `.chant/pr` and exits
|
|
62
|
+
* `$FAKE_EXIT`. As `pr-apply` it copies the resume record it was given to
|
|
63
|
+
* `.chant/pr/resume-in.json` and writes a new one naming `$FAKE_ATTEMPT`.
|
|
64
|
+
*/
|
|
65
|
+
function fakeChant(): string {
|
|
66
|
+
const file = join(scratchDir("chant-ci-serve-bin-"), "chant");
|
|
67
|
+
writeFileSync(
|
|
68
|
+
file,
|
|
69
|
+
[
|
|
70
|
+
"#!/bin/sh",
|
|
71
|
+
'stage="$2"',
|
|
72
|
+
"mkdir -p .chant/pr",
|
|
73
|
+
'printf "%s\\n" "$@" > ".chant/pr/$stage.args"',
|
|
74
|
+
'if [ "$stage" = pr-apply ]; then',
|
|
75
|
+
" if [ -f .chant/pr-resume/pr-apply.json ]; then cp .chant/pr-resume/pr-apply.json .chant/pr/resume-in.json; fi",
|
|
76
|
+
" mkdir -p .chant/pr-resume",
|
|
77
|
+
' echo "{\\"attempt\\": \\"${FAKE_ATTEMPT:-1}\\"}" > .chant/pr-resume/pr-apply.json',
|
|
78
|
+
"fi",
|
|
79
|
+
'if [ -n "$FAKE_SLEEP" ]; then sleep "$FAKE_SLEEP"; fi',
|
|
80
|
+
'exit "${FAKE_EXIT:-0}"',
|
|
81
|
+
"",
|
|
82
|
+
].join("\n"),
|
|
83
|
+
);
|
|
84
|
+
chmodSync(file, 0o755);
|
|
85
|
+
return file;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A forge whose open pull requests and branch head the test sets. */
|
|
89
|
+
function fakeForge(head: string): CiServeForge & { head: string; prs: OpenPullRequest[]; polls: number } {
|
|
90
|
+
const f = {
|
|
91
|
+
head,
|
|
92
|
+
prs: [] as OpenPullRequest[],
|
|
93
|
+
polls: 0,
|
|
94
|
+
async openPullRequests(base: string) {
|
|
95
|
+
f.polls++;
|
|
96
|
+
return f.prs.filter((p) => p.base === base);
|
|
97
|
+
},
|
|
98
|
+
async branchHead(branch: string) {
|
|
99
|
+
return branch === "main" ? f.head : undefined;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
return f;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pr = (number: number, headSha: string, baseSha: string, extra: Partial<OpenPullRequest> = {}): OpenPullRequest => ({ number, head: `pr-${number}`, headSha, base: "main", baseSha, author: "driver", fork: false, ...extra });
|
|
106
|
+
|
|
107
|
+
const HOST = { PATH: process.env.PATH, HOME: process.env.HOME, GITHUB_TOKEN: "forge-token", GITHUB_SERVER_URL: "http://forgejo.localhost", GITHUB_API_URL: "http://forgejo.localhost/api/v1" };
|
|
108
|
+
|
|
109
|
+
function serve(app: string, client: CiServeForge, extra: Partial<CiServeOptions> & { hostEnv?: Record<string, string | undefined> } = {}): { opts: CiServeOptions; events: CiServeEvent[]; state: string } {
|
|
110
|
+
const state = extra.stateDir ?? scratchDir("chant-ci-serve-state-");
|
|
111
|
+
const events: CiServeEvent[] = [];
|
|
112
|
+
const { hostEnv, ...rest } = extra;
|
|
113
|
+
const env = { ...HOST, ...hostEnv };
|
|
114
|
+
return {
|
|
115
|
+
state,
|
|
116
|
+
events,
|
|
117
|
+
opts: {
|
|
118
|
+
cwd: app,
|
|
119
|
+
forge: "forgejo",
|
|
120
|
+
repo: "demo/estate",
|
|
121
|
+
env: "prod",
|
|
122
|
+
client,
|
|
123
|
+
stateDir: state,
|
|
124
|
+
holder: "serve-test",
|
|
125
|
+
// A job image of its own, so no setup line downloads OpenTofu.
|
|
126
|
+
job: { image: "chant-ci-serve-test:local", chant: fakeChant(), hostEnv: env, passEnv: Object.keys(hostEnv ?? {}) },
|
|
127
|
+
log: (e) => events.push(e),
|
|
128
|
+
...rest,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const argsOf = (report: string, stage: string): string[] => readFileSync(join(report, `${stage}.args`), "utf-8").trimEnd().split("\n");
|
|
134
|
+
const ofType = <T extends CiServeEvent["type"]>(events: CiServeEvent[], type: T) => events.filter((e): e is Extract<CiServeEvent, { type: T }> => e.type === type);
|
|
135
|
+
|
|
136
|
+
describe("chant ci serve (#4095)", () => {
|
|
137
|
+
test("one poll plans a new pull request head once, and skips forks and authors not given", async () => {
|
|
138
|
+
const { root, app, main } = workspace();
|
|
139
|
+
const head = pullHead(root, 1);
|
|
140
|
+
const forge = fakeForge(main);
|
|
141
|
+
forge.prs = [pr(1, head, main), pr(2, head, main, { fork: true }), pr(3, head, main, { author: "stranger" })];
|
|
142
|
+
const { opts, events, state } = serve(app, forge, { authors: ["Driver"] });
|
|
143
|
+
|
|
144
|
+
const first = await ciServePass(opts);
|
|
145
|
+
expect(first.jobs).toHaveLength(1);
|
|
146
|
+
expect(first.jobs[0]).toMatchObject({ job: "plan", pr: 1, commit: head, base: main, status: "ok", attempts: 1 });
|
|
147
|
+
expect(ofType(events, "baseline")).toEqual([{ type: "baseline", commit: main }]);
|
|
148
|
+
expect(ofType(events, "skipped").map((e) => [e.pr, e.reason])).toEqual([[2, "fork"], [3, "author"]]);
|
|
149
|
+
const argv = argsOf(first.jobs[0]!.report!, "pr-plan");
|
|
150
|
+
expect(argv.slice(0, 5)).toEqual(["components", "pr-plan", "--base", main, "--pr"]);
|
|
151
|
+
expect(argv[5]).toBe("1");
|
|
152
|
+
expect(argv).toContain("--member");
|
|
153
|
+
|
|
154
|
+
const group = prApplyGroup("prod", "app");
|
|
155
|
+
const file = join(state, `${group}.json`);
|
|
156
|
+
const saved = readCiServeState(file, { forge: "forgejo", repo: "demo/estate", env: "prod", branch: "main" });
|
|
157
|
+
expect(saved.target).toBe(main);
|
|
158
|
+
expect(saved.jobs[ciServeKey({ job: "plan", pr: 1, commit: head })]).toMatchObject({ status: "ok" });
|
|
159
|
+
|
|
160
|
+
// The same head again: nothing runs.
|
|
161
|
+
const second = await ciServePass(opts);
|
|
162
|
+
expect(second.jobs).toEqual([]);
|
|
163
|
+
expect(ofType(second.events, "started")).toEqual([]);
|
|
164
|
+
expect(forge.polls).toBe(2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a merge to the target branch applies the new commit, measured from the head before it", async () => {
|
|
168
|
+
const { root, app, main } = workspace();
|
|
169
|
+
const forge = fakeForge(main);
|
|
170
|
+
const { opts, events } = serve(app, forge);
|
|
171
|
+
expect((await ciServePass(opts)).jobs).toEqual([]);
|
|
172
|
+
expect(ofType(events, "baseline")).toHaveLength(1);
|
|
173
|
+
|
|
174
|
+
const merged = commit(root, "app/merged.txt");
|
|
175
|
+
git(root, "push", "-q", "origin", "main");
|
|
176
|
+
forge.head = merged;
|
|
177
|
+
const pass = await ciServePass(opts);
|
|
178
|
+
expect(pass.jobs).toEqual([expect.objectContaining({ job: "apply", commit: merged, base: main, status: "ok" })]);
|
|
179
|
+
const argv = argsOf(pass.jobs[0]!.report!, "pr-apply");
|
|
180
|
+
expect(argv.slice(0, 4)).toEqual(["components", "pr-apply", "--base", main]);
|
|
181
|
+
expect(argv.slice(-2)).toEqual(["--resume", ".chant/pr-resume/pr-apply.json"]);
|
|
182
|
+
// The run was recorded on chant/lifecycle and pushed.
|
|
183
|
+
expect(git(root, "ls-remote", "origin", "chant/lifecycle")).toContain(git(root, "rev-parse", "chant/lifecycle"));
|
|
184
|
+
|
|
185
|
+
expect((await ciServePass(opts)).jobs).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("the group's lease held elsewhere holds the apply until it is released", async () => {
|
|
189
|
+
const { root, app, main } = workspace();
|
|
190
|
+
const forge = fakeForge(main);
|
|
191
|
+
const { opts, events } = serve(app, forge);
|
|
192
|
+
await ciServePass(opts);
|
|
193
|
+
const merged = commit(root, "app/merged.txt");
|
|
194
|
+
git(root, "push", "-q", "origin", "main");
|
|
195
|
+
forge.head = merged;
|
|
196
|
+
|
|
197
|
+
const group = prApplyGroup("prod", "app");
|
|
198
|
+
const other = await acquireLease(group, "other-host", { cwd: app, mode: "claim", requirePush: true });
|
|
199
|
+
expect(other.acquired).toBe(true);
|
|
200
|
+
const held = await ciServePass(opts);
|
|
201
|
+
expect(held.jobs).toEqual([]);
|
|
202
|
+
expect(ofType(events, "held")).toEqual([{ type: "held", job: "apply", commit: merged, reason: "lease", heldBy: "other-host" }]);
|
|
203
|
+
expect(formatCiServeEvent(ofType(events, "held")[0]!)).toContain("the apply lease is held by other-host");
|
|
204
|
+
|
|
205
|
+
expect(await releaseLease(group, "other-host", other.lease!.token, { cwd: app })).toBe(true);
|
|
206
|
+
const ran = await ciServePass(opts);
|
|
207
|
+
expect(ran.jobs).toEqual([expect.objectContaining({ job: "apply", commit: merged, base: main, status: "ok" })]);
|
|
208
|
+
// The serve released its own lease after the apply.
|
|
209
|
+
expect((await acquireLease(group, "after", { cwd: app, mode: "claim" })).acquired).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("a failed apply stays failed, and --rerun-failed runs it again with its resume record", async () => {
|
|
213
|
+
const { root, app, main } = workspace();
|
|
214
|
+
const forge = fakeForge(main);
|
|
215
|
+
const failing = serve(app, forge, { hostEnv: { FAKE_EXIT: "3", FAKE_ATTEMPT: "1" } });
|
|
216
|
+
await ciServePass(failing.opts);
|
|
217
|
+
const merged = commit(root, "app/merged.txt");
|
|
218
|
+
git(root, "push", "-q", "origin", "main");
|
|
219
|
+
forge.head = merged;
|
|
220
|
+
|
|
221
|
+
const failed = await ciServePass(failing.opts);
|
|
222
|
+
expect(failed.jobs).toEqual([expect.objectContaining({ job: "apply", status: "fail", attempts: 1, failure: expect.stringContaining("command-failed") })]);
|
|
223
|
+
const record = join(failing.state, "resume", `${prApplyRecordKey("prod", merged, "app")}.json`);
|
|
224
|
+
expect(JSON.parse(readFileSync(record, "utf-8"))).toEqual({ attempt: "1" });
|
|
225
|
+
expect(existsSync(join(failed.jobs[0]!.report!, "resume-in.json"))).toBe(false);
|
|
226
|
+
|
|
227
|
+
// Without --rerun-failed the failed apply is not run again.
|
|
228
|
+
expect((await ciServePass(failing.opts)).jobs).toEqual([]);
|
|
229
|
+
|
|
230
|
+
const retry = serve(app, forge, { stateDir: failing.state, rerunFailed: true, hostEnv: { FAKE_EXIT: "0", FAKE_ATTEMPT: "2" } });
|
|
231
|
+
const again = await ciServePass(retry.opts);
|
|
232
|
+
expect(again.jobs).toEqual([expect.objectContaining({ job: "apply", commit: merged, base: main, status: "ok", attempts: 2 })]);
|
|
233
|
+
expect(ofType(retry.events, "started")[0]).toMatchObject({ attempt: 2, resume: record });
|
|
234
|
+
// The job read the first attempt's record, and left its own.
|
|
235
|
+
expect(JSON.parse(readFileSync(join(again.jobs[0]!.report!, "resume-in.json"), "utf-8"))).toEqual({ attempt: "1" });
|
|
236
|
+
expect(JSON.parse(readFileSync(record, "utf-8"))).toEqual({ attempt: "2" });
|
|
237
|
+
// A passed apply is not run again, --rerun-failed or not.
|
|
238
|
+
expect((await ciServePass(retry.opts)).jobs).toEqual([]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("--max-jobs holds the jobs over the limit for the next poll", async () => {
|
|
242
|
+
const { root, app, main } = workspace();
|
|
243
|
+
const one = pullHead(root, 1);
|
|
244
|
+
const two = pullHead(root, 2);
|
|
245
|
+
const forge = fakeForge(main);
|
|
246
|
+
forge.prs = [pr(1, one, main), pr(2, two, main)];
|
|
247
|
+
const { opts, events } = serve(app, forge, { maxJobs: 1 });
|
|
248
|
+
|
|
249
|
+
const first = await ciServePass(opts);
|
|
250
|
+
expect(first.jobs.map((j) => [j.pr, j.status])).toEqual([[1, "ok"]]);
|
|
251
|
+
expect(ofType(events, "held")).toEqual([{ type: "held", job: "plan", commit: two, pr: 2, reason: "capacity" }]);
|
|
252
|
+
|
|
253
|
+
const second = await ciServePass(opts);
|
|
254
|
+
expect(second.jobs.map((j) => [j.pr, j.status])).toEqual([[2, "ok"]]);
|
|
255
|
+
expect((await ciServePass(opts)).jobs).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("the command line takes the repository, the limit, the authors and the interval", () => {
|
|
259
|
+
const args = parseArgs(["ci", "serve", "--forge", "forgejo", "--repo", "demo/estate", "--env", "local", "--max-jobs", "2", "--author", "a", "--author", "b", "--state", "/tmp/s", "--once", "--rerun-failed", "--interval", "45"]);
|
|
260
|
+
expect(args).toMatchObject({ command: "ci", path: "serve", forge: "forgejo", ciRepo: "demo/estate", env: "local", maxJobs: 2, ciAuthors: ["a", "b"], statePath: "/tmp/s", once: true, rerunFailed: true, interval: "45" });
|
|
261
|
+
expect(() => parseArgs(["ci", "serve", "--repo", "estate"])).toThrow(/owner\/name/);
|
|
262
|
+
expect(() => parseArgs(["ci", "serve", "--max-jobs", "0"])).toThrow(/1 or more/);
|
|
263
|
+
expect(ciServeInterval(undefined)).toBe(30_000);
|
|
264
|
+
expect(ciServeInterval("45")).toBe(45_000);
|
|
265
|
+
expect(ciServeInterval("2m")).toBe(120_000);
|
|
266
|
+
expect(() => ciServeInterval("0")).toThrow(/a second or more/);
|
|
267
|
+
});
|
|
268
|
+
});
|