@intentius/chant-lexicon-forgejo 0.117.0 → 0.118.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/dist/components/generate-op-waves-pipeline.d.ts +3 -0
- package/dist/components/generate-op-waves-pipeline.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +3 -3
- package/src/components/generate-op-waves-pipeline.test.ts +7 -0
- package/src/components/generate-op-waves-pipeline.ts +17 -1
- package/src/components/op-waves.e2e.test.ts +222 -0
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* resume --op <name>` on that schedule. Forgejo has no API to re-run a run,
|
|
12
12
|
* so it dispatches the waves workflow again on its branch, with the
|
|
13
13
|
* `CHANT_FORGE_TOKEN` secret (a token with the `write:repository` scope).
|
|
14
|
+
*
|
|
15
|
+
* A `pr-review` wave (#3684) gets `<name>-plans.yml` on pull requests, as on
|
|
16
|
+
* GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
14
17
|
*/
|
|
15
18
|
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
16
19
|
import type { OpWavesPipelineOptions, OpWavesPipelineResult } from "@intentius/chant/lexicon";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generate-op-waves-pipeline.d.ts","sourceRoot":"","sources":["../../src/components/generate-op-waves-pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAMjF,6EAA6E;AAC7E,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,sBAAsB,EAC/B,cAAc,GAAE,qBAA0B,GACzC,qBAAqB,CA0DvB"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "ba516f57c3a548ed384d4bfcc35134a752f9b9e61a6a3d4ca167ee028f61a005",
|
|
5
5
|
"meta.json": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
|
6
6
|
"types/index.d.ts": "66204549b2a864ab5489e3f02c1878e73ab4b3c9d938cfb480b66f71543a43de",
|
|
7
7
|
"rules/delegate-to-github.ts": "1060cda40f4b73d6cca3ba3fa13b80b4886147d4034c88584c2d80c5510dbe5e",
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
"rules/wfj011.ts": "fcb8bf6685d744af20ad804ce929488257995c74b9a3353b2eacae3768d1c83e",
|
|
10
10
|
"skills/chant-forgejo.md": "f28d7fb69fb1cfd992496ef41fda0ea5ccd54a29b84ea1c501a44f6a3992c4d5"
|
|
11
11
|
},
|
|
12
|
-
"composite": "
|
|
12
|
+
"composite": "55b4cd03f51850c9887c8af5d74b7101952ff326184355ac96c1f1ef004662ff"
|
|
13
13
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-forgejo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.118.0",
|
|
4
4
|
"description": "Forgejo / Codeberg / Gitea Actions lexicon for chant — a thin GitHub Actions dialect",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"zod": "^4.3.6",
|
|
57
|
-
"@intentius/chant": "^0.
|
|
58
|
-
"@intentius/chant-lexicon-github": "^0.
|
|
57
|
+
"@intentius/chant": "^0.118.0",
|
|
58
|
+
"@intentius/chant-lexicon-github": "^0.118.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"generate": "tsx src/codegen/generate-cli.ts",
|
|
@@ -40,4 +40,11 @@ describe("generateForgejoOpWavesPipeline", () => {
|
|
|
40
40
|
expect(yaml).not.toContain("github.token");
|
|
41
41
|
expect(yaml).not.toMatch(/^permissions:/m);
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
test("a pr-review wave adds a pull request workflow that records the head's plans (#3684)", () => {
|
|
45
|
+
const { files } = generateForgejoOpWavesPipeline({ ...spec, waves: spec.waves.map((w, i) => (i === spec.waves.length - 1 ? { ...w, approval: "pr-review" as const } : w)) }, { specFile: "waves.json" });
|
|
46
|
+
expect(files.map((f) => f.name)).toEqual([`${spec.name}.yml`, `${spec.name}-plans.yml`]);
|
|
47
|
+
expect(files[1]!.yaml).toContain("pull_request:");
|
|
48
|
+
expect(files[1]!.yaml).toContain("run: chant run wave --spec waves.json --record-plans");
|
|
49
|
+
});
|
|
43
50
|
});
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* resume --op <name>` on that schedule. Forgejo has no API to re-run a run,
|
|
12
12
|
* so it dispatches the waves workflow again on its branch, with the
|
|
13
13
|
* `CHANT_FORGE_TOKEN` secret (a token with the `write:repository` scope).
|
|
14
|
+
*
|
|
15
|
+
* A `pr-review` wave (#3684) gets `<name>-plans.yml` on pull requests, as on
|
|
16
|
+
* GitHub. Forgejo's job token reads reviews with no permission to ask for.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import { buildGithubOpWavesDoc } from "@intentius/chant-lexicon-github/components/generate-op-waves-pipeline";
|
|
@@ -29,7 +32,7 @@ export function generateForgejoOpWavesPipeline(
|
|
|
29
32
|
options: OpWavesPipelineOptions,
|
|
30
33
|
dialectOptions: ForgejoDialectOptions = {},
|
|
31
34
|
): OpWavesPipelineResult {
|
|
32
|
-
const { doc, jobs, resumeDoc } = buildGithubOpWavesDoc(spec, options);
|
|
35
|
+
const { doc, jobs, resumeDoc, plansDoc } = buildGithubOpWavesDoc(spec, options);
|
|
33
36
|
const jobsDoc = Object.fromEntries(
|
|
34
37
|
Object.entries(doc.jobsDoc).map(([name, job]) => {
|
|
35
38
|
const { environment: _dropped, ...rest } = job as Record<string, unknown>;
|
|
@@ -72,5 +75,18 @@ export function generateForgejoOpWavesPipeline(
|
|
|
72
75
|
}),
|
|
73
76
|
});
|
|
74
77
|
}
|
|
78
|
+
if (plansDoc) {
|
|
79
|
+
files.push({
|
|
80
|
+
name: `${spec.name}-plans.yml`,
|
|
81
|
+
yaml: emitOpPipelineYAML({
|
|
82
|
+
name: plansDoc.name!,
|
|
83
|
+
on: forgejoize(plansDoc.on, dialectOptions),
|
|
84
|
+
...(plansDoc.env ? { env: forgejoize(plansDoc.env, dialectOptions) } : {}),
|
|
85
|
+
concurrency: forgejoize(plansDoc.concurrency, dialectOptions),
|
|
86
|
+
permissions: {},
|
|
87
|
+
jobsDoc: forgejoize(plansDoc.jobsDoc, dialectOptions),
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
75
91
|
return { files, jobs };
|
|
76
92
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3690 end to end on a real Forgejo with a runner: a two-wave Op pipeline,
|
|
3
|
+
* rendered by {@link generateForgejoOpWavesPipeline}, whose jobs run this
|
|
4
|
+
* checkout's `chant run wave`.
|
|
5
|
+
*
|
|
6
|
+
* - Wave 1 (`dev`, gate `on-destructive`) plans a destructive run and waits
|
|
7
|
+
* at its gate (exit 3). Wave 2 (`prod`, gate `never`) needs it, so it never
|
|
8
|
+
* starts and applies nothing.
|
|
9
|
+
* - `chant approve` of the digest wave 1 printed, then a dispatch of the
|
|
10
|
+
* workflow: wave 1 applies and wave 2 runs and applies.
|
|
11
|
+
* - A broken copy of the workflow with wave 2's `needs` edge removed: wave 1
|
|
12
|
+
* still waits, wave 2 runs and applies, and the check the first run passed
|
|
13
|
+
* ({@link waitingWaveStoppedNext}) catches it.
|
|
14
|
+
*
|
|
15
|
+
* The plan and apply commands are shell scripts in the repository: plan
|
|
16
|
+
* writes a fixed digest with `destructive: true`, apply prints a marker line.
|
|
17
|
+
* This checkout's `@intentius/chant` is packed into the repository and each
|
|
18
|
+
* job installs it, so the jobs run the code under test, not a release.
|
|
19
|
+
*
|
|
20
|
+
* Runs only with CHANT_E2E_FORGEJO_URL and CHANT_E2E_FORGEJO_TOKEN (a token
|
|
21
|
+
* that creates and deletes repositories and pushes to them), against a
|
|
22
|
+
* Forgejo whose runner takes `docker` jobs and reaches the npm registry.
|
|
23
|
+
* chant's CI has no Forgejo, so its e2e job skips this file, as it skips the
|
|
24
|
+
* other Forgejo e2e (`packages/core/src/op/gate-resume.e2e.test.ts`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { dirname, join, resolve } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
33
|
+
import type { OpWavesSpec } from "@intentius/chant/op/op-waves";
|
|
34
|
+
import { generateForgejoOpWavesPipeline } from "./generate-op-waves-pipeline";
|
|
35
|
+
|
|
36
|
+
const URL_ = process.env.CHANT_E2E_FORGEJO_URL?.replace(/\/+$/, "");
|
|
37
|
+
const TOKEN = process.env.CHANT_E2E_FORGEJO_TOKEN;
|
|
38
|
+
const api = `${URL_}/api/v1`;
|
|
39
|
+
|
|
40
|
+
const CORE = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../packages/core");
|
|
41
|
+
const CHANT = join(CORE, "bin/chant");
|
|
42
|
+
|
|
43
|
+
const SPEC: OpWavesSpec = {
|
|
44
|
+
name: "demo",
|
|
45
|
+
op: "demo",
|
|
46
|
+
plan: ["sh", "plan.sh", "{target}", "{plan}"],
|
|
47
|
+
apply: ["sh", "apply.sh", "{target}"],
|
|
48
|
+
waves: [
|
|
49
|
+
{ name: "dev", runs: [{ target: "dev" }], gate: "on-destructive" },
|
|
50
|
+
{ name: "prod", runs: [{ target: "prod" }], gate: "never" },
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PLAN_SH = `# Every run's plan is destructive, with a digest fixed by its target.
|
|
55
|
+
printf '{"planDigest":"sha256:%s","destructive":true}' "$(printf '%s' "$1" | sha256sum | cut -d' ' -f1)" > "$2"
|
|
56
|
+
`;
|
|
57
|
+
const APPLY_SH = `echo "CHANT_E2E_APPLIED $1"\n`;
|
|
58
|
+
|
|
59
|
+
async function call(method: string, path: string, body?: unknown): Promise<any> { // eslint-disable-line @typescript-eslint/no-explicit-any -- untyped forge JSON
|
|
60
|
+
const res = await fetch(`${api}${path}`, {
|
|
61
|
+
method,
|
|
62
|
+
headers: { Authorization: `token ${TOKEN}`, "Content-Type": "application/json" },
|
|
63
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
64
|
+
});
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${text.slice(0, 300)}`);
|
|
67
|
+
try {
|
|
68
|
+
return text ? JSON.parse(text) : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
75
|
+
const git = (cwd: string, ...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
76
|
+
|
|
77
|
+
interface Run {
|
|
78
|
+
id: number;
|
|
79
|
+
status: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One job of a finished run: its name, how it ended and its log. */
|
|
83
|
+
interface JobOutcome {
|
|
84
|
+
name: string;
|
|
85
|
+
status: string;
|
|
86
|
+
log: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function finishedRun(repo: string, after: number, timeoutMs = 600_000): Promise<Run> {
|
|
90
|
+
const end = Date.now() + timeoutMs;
|
|
91
|
+
while (Date.now() < end) {
|
|
92
|
+
const listed = await call("GET", `/repos/${repo}/actions/runs?limit=50`);
|
|
93
|
+
const run = ((listed.workflow_runs ?? listed) as Run[]).find((r) => r.id > after);
|
|
94
|
+
if (run && !["waiting", "running", "blocked", "unknown"].includes(run.status)) return run;
|
|
95
|
+
await sleep(3000);
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`no finished run in ${repo} within ${timeoutMs}ms`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function jobsOf(repo: string, run: number): Promise<Record<string, JobOutcome>> {
|
|
101
|
+
const listed = await call("GET", `/repos/${repo}/actions/runs/${run}/jobs`);
|
|
102
|
+
const out: Record<string, JobOutcome> = {};
|
|
103
|
+
for (const job of (listed.jobs ?? listed) as Array<{ id: number; name: string; status: string; conclusion?: string }>) {
|
|
104
|
+
const log = await call("GET", `/repos/${repo}/actions/jobs/${job.id}/logs`).catch(() => "");
|
|
105
|
+
out[job.name] = { name: job.name, status: job.conclusion || job.status, log: String(log ?? "") };
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const applied = (job: JobOutcome | undefined, target: string) => !!job && job.log.includes(`CHANT_E2E_APPLIED ${target}`);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* What the issue asks of a waiting wave: wave 1 waited at its gate, and wave
|
|
114
|
+
* 2 neither succeeded nor applied anything. Returns why not, or null.
|
|
115
|
+
*/
|
|
116
|
+
function waitingWaveStoppedNext(jobs: Record<string, JobOutcome>): string | null {
|
|
117
|
+
const first = jobs["wave-1-dev"];
|
|
118
|
+
const second = jobs["wave-2-prod"];
|
|
119
|
+
if (!first || first.status !== "failure" || !first.log.includes("Nothing in wave 1 or after it ran")) {
|
|
120
|
+
return `wave 1 did not wait at its gate (${first?.status ?? "missing"})`;
|
|
121
|
+
}
|
|
122
|
+
if (applied(first, "dev")) return "wave 1 applied while waiting";
|
|
123
|
+
if (second && (second.status === "success" || applied(second, "prod"))) return `wave 2 ran (${second.status}) while wave 1 waited`;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
describe.skipIf(!URL_ || !TOKEN)("a waiting Op wave stops the next on Forgejo (#3690)", () => {
|
|
128
|
+
const stamp = Date.now().toString(36);
|
|
129
|
+
const work = mkdtempSync(join(tmpdir(), "chant-e2e-waves-"));
|
|
130
|
+
const created: string[] = [];
|
|
131
|
+
let owner = "";
|
|
132
|
+
let tarball = "";
|
|
133
|
+
|
|
134
|
+
beforeAll(() => {
|
|
135
|
+
// This checkout's chant, for the jobs to install. Its declaration build
|
|
136
|
+
// is skipped: the bin runs the TypeScript sources through tsx.
|
|
137
|
+
const out = execFileSync("npm", ["pack", "--ignore-scripts", "--silent", "--pack-destination", work], { cwd: CORE, encoding: "utf-8" });
|
|
138
|
+
tarball = join(work, out.trim().split("\n").pop()!);
|
|
139
|
+
}, 300_000);
|
|
140
|
+
|
|
141
|
+
afterAll(async () => {
|
|
142
|
+
for (const repo of created) await call("DELETE", `/repos/${repo}`).catch(() => undefined);
|
|
143
|
+
rmSync(work, { recursive: true, force: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A repository whose `main` holds two commits: the spec, scripts and chant
|
|
148
|
+
* first (the base the gate policy is read at), then the workflow. One push
|
|
149
|
+
* of both runs the workflow once, at the second.
|
|
150
|
+
*/
|
|
151
|
+
async function pushRepo(name: string, workflow: string): Promise<{ repo: string; dir: string }> {
|
|
152
|
+
owner ||= (await call("GET", "/user")).login;
|
|
153
|
+
await call("POST", "/user/repos", { name, auto_init: false, default_branch: "main", private: false });
|
|
154
|
+
const repo = `${owner}/${name}`;
|
|
155
|
+
created.push(repo);
|
|
156
|
+
const dir = join(work, name);
|
|
157
|
+
mkdirSync(dir);
|
|
158
|
+
git(dir, "init", "-q", "-b", "main");
|
|
159
|
+
git(dir, "config", "user.name", "chant e2e");
|
|
160
|
+
git(dir, "config", "user.email", "chant-e2e@example.invalid");
|
|
161
|
+
writeFileSync(join(dir, "waves.json"), JSON.stringify(SPEC, null, 2) + "\n");
|
|
162
|
+
writeFileSync(join(dir, "plan.sh"), PLAN_SH);
|
|
163
|
+
writeFileSync(join(dir, "apply.sh"), APPLY_SH);
|
|
164
|
+
writeFileSync(join(dir, ".gitignore"), ".chant/\n");
|
|
165
|
+
execFileSync("cp", [tarball, join(dir, "chant.tgz")]);
|
|
166
|
+
git(dir, "add", ".");
|
|
167
|
+
git(dir, "commit", "-q", "-m", "waves spec, plan and apply");
|
|
168
|
+
mkdirSync(join(dir, ".forgejo/workflows"), { recursive: true });
|
|
169
|
+
writeFileSync(join(dir, ".forgejo/workflows/demo.yml"), workflow);
|
|
170
|
+
git(dir, "add", ".");
|
|
171
|
+
git(dir, "commit", "-q", "-m", "waves workflow");
|
|
172
|
+
const remote = new URL(`${URL_}/${repo}.git`);
|
|
173
|
+
remote.username = owner;
|
|
174
|
+
remote.password = TOKEN!;
|
|
175
|
+
git(dir, "remote", "add", "origin", remote.toString());
|
|
176
|
+
git(dir, "push", "-q", "origin", "main");
|
|
177
|
+
return { repo, dir };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { files } = generateForgejoOpWavesPipeline(SPEC, { specFile: "waves.json", beforeScript: ["npm install --global --no-audit --no-fund ./chant.tgz"] });
|
|
181
|
+
const workflow = files[0]!.yaml;
|
|
182
|
+
const needsEdge = /\n\s+needs:\n\s+- wave-1-dev/;
|
|
183
|
+
|
|
184
|
+
test("the rendered workflow chains wave 2 to wave 1", () => {
|
|
185
|
+
expect(workflow).toMatch(needsEdge);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("wave 1 waits and wave 2 never starts; after chant approve a rerun applies both; without the needs edge the check fails", async () => {
|
|
189
|
+
const [good, broken] = await Promise.all([
|
|
190
|
+
pushRepo(`chant-e2e-waves-${stamp}`, workflow),
|
|
191
|
+
pushRepo(`chant-e2e-waves-broken-${stamp}`, workflow.replace(needsEdge, "")),
|
|
192
|
+
]);
|
|
193
|
+
const [waited, brokenRun] = await Promise.all([finishedRun(good.repo, 0), finishedRun(broken.repo, 0)]);
|
|
194
|
+
|
|
195
|
+
// Wave 1 waits at its gate; wave 2 never starts.
|
|
196
|
+
const first = await jobsOf(good.repo, waited.id);
|
|
197
|
+
expect(waitingWaveStoppedNext(first)).toBeNull();
|
|
198
|
+
expect(first["wave-2-prod"]?.status).not.toBe("success");
|
|
199
|
+
|
|
200
|
+
// Without the needs edge, wave 2 runs and applies while wave 1 waits, and the check says so.
|
|
201
|
+
const unchained = await jobsOf(broken.repo, brokenRun.id);
|
|
202
|
+
expect(unchained["wave-1-dev"]?.status).toBe("failure");
|
|
203
|
+
expect(applied(unchained["wave-2-prod"], "prod")).toBe(true);
|
|
204
|
+
expect(waitingWaveStoppedNext(unchained)).toMatch(/wave 2 ran/);
|
|
205
|
+
|
|
206
|
+
// Approve the digest wave 1 printed, from a checkout of the repository.
|
|
207
|
+
const approve = first["wave-1-dev"]!.log.match(/then approve: (chant approve \S+ \S+ --plan \S+)/)?.[1];
|
|
208
|
+
expect(approve, "wave 1 printed the approve command").toBeDefined();
|
|
209
|
+
const argv = approve!.split(" ").slice(1);
|
|
210
|
+
execFileSync(CHANT, argv, { cwd: good.dir, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
211
|
+
expect(git(good.dir, "ls-remote", "origin", "refs/heads/chant/lifecycle")).toContain("chant/lifecycle");
|
|
212
|
+
|
|
213
|
+
// A rerun applies wave 1, then wave 2 runs and applies.
|
|
214
|
+
await call("POST", `/repos/${good.repo}/actions/workflows/demo.yml/dispatches`, { ref: "main" });
|
|
215
|
+
const rerun = await finishedRun(good.repo, waited.id);
|
|
216
|
+
const second = await jobsOf(good.repo, rerun.id);
|
|
217
|
+
expect(second["wave-1-dev"]?.status).toBe("success");
|
|
218
|
+
expect(applied(second["wave-1-dev"], "dev")).toBe(true);
|
|
219
|
+
expect(second["wave-2-prod"]?.status).toBe("success");
|
|
220
|
+
expect(applied(second["wave-2-prod"], "prod")).toBe(true);
|
|
221
|
+
}, 1_500_000);
|
|
222
|
+
});
|