@intentius/chant 0.124.33 → 0.124.35
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/graph.js +2 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +147 -51
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +284 -81
- package/dist/cli/handlers/release.d.ts +12 -0
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +125 -10
- package/dist/cli/handlers/run.d.ts +3 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +12 -2
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +37 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +24 -0
- package/dist/cli/registry.d.ts +17 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +8 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +3 -2
- package/dist/components/green-cover.d.ts +77 -0
- package/dist/components/green-cover.d.ts.map +1 -0
- package/dist/components/green-cover.js +153 -0
- package/dist/components/pr-pipeline.d.ts +34 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +39 -5
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/promote.d.ts +4 -1
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +6 -3
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +9 -0
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +15 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +11 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/graph-declared.d.ts +9 -1
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +18 -3
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +30 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +13 -2
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +5 -1
- package/dist/lifecycle/compose-refs.d.ts +52 -0
- package/dist/lifecycle/compose-refs.d.ts.map +1 -0
- package/dist/lifecycle/compose-refs.js +115 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.js +3 -2
- package/dist/lifecycle/green-apply.d.ts +39 -0
- package/dist/lifecycle/green-apply.d.ts.map +1 -0
- package/dist/lifecycle/green-apply.js +51 -0
- package/dist/lifecycle/publish-declaration.d.ts +4 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +15 -0
- package/dist/lifecycle/publish-npm.d.ts +7 -4
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +60 -13
- package/dist/lifecycle/release-compose.d.ts +91 -0
- package/dist/lifecycle/release-compose.d.ts.map +1 -0
- package/dist/lifecycle/release-compose.js +235 -0
- package/dist/lifecycle/release-cut.d.ts +8 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +2 -0
- package/dist/lifecycle/release-publish.d.ts +2 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +2 -1
- package/dist/lifecycle/release-store.d.ts +15 -0
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +5 -1
- package/dist/wave-runtime.mjs +226 -224
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +20 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +130 -0
- package/dist/workspace/checks/security.d.ts +16 -1
- package/dist/workspace/checks/security.d.ts.map +1 -1
- package/dist/workspace/checks/security.js +4 -4
- package/dist/workspace/checks.d.ts +1 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +4 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +5 -3
- package/dist/workspace/ci-green-workflow.d.ts +18 -2
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +27 -5
- package/dist/workspace/ci-green.d.ts +31 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +78 -4
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-run.d.ts +6 -0
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +2 -1
- package/dist/workspace/ci-serve.d.ts +94 -13
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +248 -30
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +18 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +9 -2
- package/dist/workspace/declaration.schema.json +28 -0
- package/dist/workspace/delivery-metrics.d.ts +3 -1
- package/dist/workspace/delivery-metrics.d.ts.map +1 -1
- package/dist/workspace/delivery-metrics.js +12 -3
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +4 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +1 -0
- package/dist/workspace/reason-codes.d.ts +9 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +10 -0
- package/dist/workspace/release-candidates.d.ts +28 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -1
- package/dist/workspace/release-candidates.js +38 -2
- package/dist/workspace/release-candidates.schema.json +51 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/releases.schema.json +30 -0
- package/package.json +1 -1
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/pr-green.test.ts +230 -0
- package/src/cli/handlers/pr.ts +148 -66
- package/src/cli/handlers/promote.test.ts +34 -0
- package/src/cli/handlers/promote.ts +302 -91
- package/src/cli/handlers/release-compose.e2e.test.ts +133 -0
- package/src/cli/handlers/release.ts +112 -10
- package/src/cli/handlers/run.ts +12 -1
- package/src/cli/main.ts +37 -2
- package/src/cli/parse-args.ts +18 -0
- package/src/cli/registry.ts +17 -1
- package/src/codegen/release-wiring.test.ts +56 -9
- package/src/components/auto-release.test.ts +22 -0
- package/src/components/auto-release.ts +9 -2
- package/src/components/green-cover.test.ts +170 -0
- package/src/components/green-cover.ts +186 -0
- package/src/components/pr-pipeline.test.ts +14 -4
- package/src/components/pr-pipeline.ts +56 -5
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/promote.test.ts +7 -0
- package/src/components/promote.ts +9 -4
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +24 -1
- package/src/components/release-promote.ts +15 -0
- package/src/deep-observation.ts +8 -0
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.test.ts +18 -0
- package/src/graph-declared.ts +17 -2
- package/src/graph-detail.test.ts +68 -1
- package/src/graph-detail.ts +29 -1
- package/src/graph-ir.ts +12 -4
- package/src/lifecycle/__fixtures__/publish-fakes.ts +5 -1
- package/src/lifecycle/compose-refs.ts +125 -0
- package/src/lifecycle/deep-observe.test.ts +20 -0
- package/src/lifecycle/deep-observe.ts +4 -2
- package/src/lifecycle/green-apply.ts +71 -0
- package/src/lifecycle/publish-declaration.ts +15 -0
- package/src/lifecycle/publish-npm.ts +55 -13
- package/src/lifecycle/publish-targets.test.ts +45 -0
- package/src/lifecycle/release-compose.test.ts +125 -0
- package/src/lifecycle/release-compose.ts +312 -0
- package/src/lifecycle/release-cut.ts +6 -0
- package/src/lifecycle/release-publish.ts +4 -1
- package/src/lifecycle/release-store.ts +16 -1
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +3 -2
- package/src/workspace/check.schema.json +20 -2
- package/src/workspace/checks/ci-workflow.test.ts +160 -0
- package/src/workspace/checks/ci-workflow.ts +135 -0
- package/src/workspace/checks/security.ts +4 -4
- package/src/workspace/checks.ts +4 -1
- package/src/workspace/ci-green-cli.ts +5 -3
- package/src/workspace/ci-green-workflow.test.ts +52 -0
- package/src/workspace/ci-green-workflow.ts +34 -5
- package/src/workspace/ci-green.test.ts +33 -0
- package/src/workspace/ci-green.ts +85 -4
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run.test.ts +18 -13
- package/src/workspace/ci-run.ts +8 -1
- package/src/workspace/ci-serve-land.e2e.test.ts +138 -0
- package/src/workspace/ci-serve-land.test.ts +176 -0
- package/src/workspace/ci-serve.test.ts +77 -43
- package/src/workspace/ci-serve.ts +268 -34
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +28 -0
- package/src/workspace/declaration.ts +32 -3
- package/src/workspace/delivery-metrics.ts +12 -3
- package/src/workspace/graph-contract.test.ts +3 -2
- package/src/workspace/init.ts +2 -2
- package/src/workspace/member-commands.test.ts +11 -4
- package/src/workspace/member-commands.ts +3 -0
- package/src/workspace/nested-graph.test.ts +4 -0
- package/src/workspace/nested-graph.ts +1 -0
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +10 -0
- package/src/workspace/release-candidates.schema.json +51 -0
- package/src/workspace/release-candidates.test.ts +30 -3
- package/src/workspace/release-candidates.ts +54 -2
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/releases.schema.json +30 -0
|
@@ -145,7 +145,7 @@ const envFile = (dir: string, stage: string): Record<string, string> =>
|
|
|
145
145
|
|
|
146
146
|
describe("the job as an Op (#4094)", () => {
|
|
147
147
|
test("the job's variables come from flags, literals and the runner's tokens, and nothing else of the runner's", () => {
|
|
148
|
-
const [plan, apply] = prLoopJobs("forgejo", "prod", { events: true });
|
|
148
|
+
const [plan, apply, record] = prLoopJobs("forgejo", "prod", { events: true });
|
|
149
149
|
const env = ciJobEnv(plan!, { forge: "forgejo", env: "prod", values: { base: "b".repeat(40), pr: 7, commit: "c".repeat(40) }, hostEnv: HOST, passEnv: ["AWS_ACCESS_KEY_ID"] });
|
|
150
150
|
expect(env).toMatchObject({
|
|
151
151
|
BASE_SHA: "b".repeat(40),
|
|
@@ -162,10 +162,12 @@ describe("the job as an Op (#4094)", () => {
|
|
|
162
162
|
expect(Object.values(env).some((v) => v.includes("${{"))).toBe(false);
|
|
163
163
|
|
|
164
164
|
expect(() => ciJobEnv(plan!, { forge: "forgejo", env: "prod", values: { base: "b".repeat(40), commit: "c".repeat(40) }, hostEnv: {} })).toThrow(CiRunError);
|
|
165
|
-
expect(() => ciJobEnv(
|
|
165
|
+
expect(() => ciJobEnv(record!, { forge: "forgejo", env: "prod", values: { commit: "c".repeat(40) }, hostEnv: {} })).toThrow(/pass --base <sha>/);
|
|
166
|
+
// #4168 step 9: the apply measures from the last applied green commit, so it needs no --base.
|
|
167
|
+
expect(ciJobEnv(apply!, { forge: "forgejo", env: "prod", values: { commit: "c".repeat(40) }, hostEnv: {} })).not.toHaveProperty("BASE_SHA");
|
|
166
168
|
|
|
167
|
-
const [,
|
|
168
|
-
const gl = ciJobEnv(
|
|
169
|
+
const [, , gitlabRecord] = prLoopJobs("gitlab", "prod");
|
|
170
|
+
const gl = ciJobEnv(gitlabRecord!, { forge: "gitlab", env: "prod", values: { base: "b".repeat(40), commit: "c".repeat(40) }, hostEnv: {} });
|
|
169
171
|
expect(gl).toEqual({ BASE_SHA: "b".repeat(40), CHANT_ENV: "prod", CI_COMMIT_SHA: "c".repeat(40) });
|
|
170
172
|
});
|
|
171
173
|
|
|
@@ -180,11 +182,13 @@ describe("the job as an Op (#4094)", () => {
|
|
|
180
182
|
expect(last.args).toEqual({ cmd: `npx chant${plan!.command.slice("chant".length)}`, cwd: "/w/app", env: { BASE_SHA: "b" }, inheritEnv: false });
|
|
181
183
|
expect(plan!.command).toContain("--member app");
|
|
182
184
|
|
|
183
|
-
// No setup or build lines: the job's phase alone. GitLab's
|
|
184
|
-
const [, gitlabApply] = prLoopJobs("gitlab", "prod");
|
|
185
|
-
const bare = ciJobOp({ job:
|
|
186
|
-
expect(bare.phases.map((p) => p.name)).toEqual(["
|
|
187
|
-
expect(ciJobCommand(
|
|
185
|
+
// No setup or build lines: the job's phase alone. GitLab's record keeps its base fallback in the command's shell.
|
|
186
|
+
const [, gitlabApply, gitlabRecord] = prLoopJobs("gitlab", "prod");
|
|
187
|
+
const bare = ciJobOp({ job: gitlabRecord!, env: "prod", cwd: "/w", jobEnv: {}, setup: [], build: [] });
|
|
188
|
+
expect(bare.phases.map((p) => p.name)).toEqual(["record"]);
|
|
189
|
+
expect(ciJobCommand(gitlabRecord!)).toBe(`${gitlabRecord!.setup![0]}\n${gitlabRecord!.command}`);
|
|
190
|
+
// #4168 step 9: the apply's green checkout is the generated job's, never ci run's: it runs at the commit it was given.
|
|
191
|
+
expect(ciJobCommand(gitlabApply!)).toBe(gitlabApply!.command);
|
|
188
192
|
|
|
189
193
|
// #4088's build and lint lines follow --before-script, with chant replaced in each.
|
|
190
194
|
const [built] = prLoopJobs("forgejo", "prod", { build: true });
|
|
@@ -298,7 +302,7 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
298
302
|
|
|
299
303
|
expect(apply.status).toBe("ok");
|
|
300
304
|
const applyArgs = readFileSync(join(reports, "apply", "pr-apply.args"), "utf-8").trimEnd().split("\n");
|
|
301
|
-
expect(applyArgs).toEqual(["components", "pr-apply", "--
|
|
305
|
+
expect(applyArgs).toEqual(["components", "pr-apply", "--green", "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--resume", ".chant/pr-resume/pr-apply.json"]);
|
|
302
306
|
expect(formatCiRun(apply)).toContain("signed by chant-runner");
|
|
303
307
|
});
|
|
304
308
|
|
|
@@ -671,7 +675,8 @@ describe("the job's secrets from the host (#4096)", () => {
|
|
|
671
675
|
const applyEnv = ciJobEnv(apply!, { forge: "forgejo", env: "prod", values: { base, commit: base }, hostEnv: HOST, secrets: ciSecretsFor(secrets, "apply") });
|
|
672
676
|
expect(applyEnv).toMatchObject({ DB_PASSWORD: "hunter22-db", DEPLOY_KEY: "deploy-key-value" });
|
|
673
677
|
// A secret can replace a forge token, and never the variables the runner sets.
|
|
674
|
-
const
|
|
678
|
+
const [, , record] = prLoopJobs("forgejo", "prod");
|
|
679
|
+
const over = ciJobEnv(record!, { forge: "forgejo", env: "prod", values: { base, commit: base }, hostEnv: HOST, secrets: { GITHUB_TOKEN: "from-file", BASE_SHA: "x", CHANT_RUNNER_KEY: "k" } });
|
|
675
680
|
expect(over).toMatchObject({ GITHUB_TOKEN: "from-file", BASE_SHA: base });
|
|
676
681
|
expect(over).not.toHaveProperty("CHANT_RUNNER_KEY");
|
|
677
682
|
expect(() => ciJobEnv(apply!, { forge: "forgejo", env: "prod", values: { base, commit: base }, hostEnv: HOST, passEnv: ["CHANT_RUNNER_KEY"] })).toThrow(/never passed to a job/);
|
|
@@ -791,10 +796,10 @@ describe("a generated forge job runs ci run --in-place (#4156)", () => {
|
|
|
791
796
|
expect(r.report).toBe(join(realpathSync(app), ".chant/pr"));
|
|
792
797
|
expect(r.record.phases.map((p) => p.name)).toEqual(["apply"]);
|
|
793
798
|
const argv = readFileSync(join(app, ".chant/pr/pr-apply.args"), "utf-8").trimEnd().split("\n");
|
|
794
|
-
expect(argv).toEqual(["components", "pr-apply", "--
|
|
799
|
+
expect(argv).toEqual(["components", "pr-apply", "--green", "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--resume", ".chant/pr-resume/pr-apply.json"]);
|
|
795
800
|
// The forge job's whole environment reaches the step, but never the runner key.
|
|
796
801
|
const env = envFile(join(app, ".chant/pr"), "pr-apply");
|
|
797
|
-
expect(env).toMatchObject({ NOT_FOR_JOBS: "leak", GITHUB_RUN_ID: "4242",
|
|
802
|
+
expect(env).toMatchObject({ NOT_FOR_JOBS: "leak", GITHUB_RUN_ID: "4242", GITHUB_SHA: sha });
|
|
798
803
|
expect(env).not.toHaveProperty("CHANT_RUNNER_KEY");
|
|
799
804
|
|
|
800
805
|
// The record: the forge's run, job and attempt as runOrigin, signed, and it verifies against trust.json.
|
package/src/workspace/ci-run.ts
CHANGED
|
@@ -423,6 +423,12 @@ export interface CiRunRequest {
|
|
|
423
423
|
/** `--env`: the environment the pipeline is for. */
|
|
424
424
|
env: string;
|
|
425
425
|
forge: ForgeKind;
|
|
426
|
+
/**
|
|
427
|
+
* Run the job's command without `--forge` (#4016 gap 6): `ci serve --forge
|
|
428
|
+
* git` reads a plain git server, which has no API for the command to ask.
|
|
429
|
+
* `forge` then only shapes the job's variables.
|
|
430
|
+
*/
|
|
431
|
+
noForge?: boolean;
|
|
426
432
|
/** `--base`, `--pr` and `--commit` (default `HEAD`). */
|
|
427
433
|
base?: string;
|
|
428
434
|
pr?: number;
|
|
@@ -668,7 +674,8 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
668
674
|
const isolated = !req.requireRunnerStart && !req.local && req.job === "apply" && securityGroupsAt(cwd).has("isolated-runners");
|
|
669
675
|
const started = req.requireRunnerStart || isolated ? runnerStartFor(req, { top, commit, hostEnv }) : undefined;
|
|
670
676
|
// A generated wave job's command, as prLoopWaveJobs writes it. #4161: a local job's command posts no status or note.
|
|
671
|
-
const
|
|
677
|
+
const forgeCommand = req.noForge ? found.command.replace(` --forge ${req.forge}`, "") : found.command;
|
|
678
|
+
const command = `${forgeCommand}${req.wave !== undefined ? ` --wave ${req.wave}` : ""}${req.local ? " --no-status" : ""}`;
|
|
672
679
|
const job = { ...found, command, ...(req.wave !== undefined ? { wave: req.wave } : {}) };
|
|
673
680
|
const own = ciJobEnv(job, {
|
|
674
681
|
forge: req.forge,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4168 4.4 and #4016 gap 6 end to end: `chant ci serve --forge git --land`,
|
|
3
|
+
* as a process, against a bare git remote with no forge API, no runner key
|
|
4
|
+
* and no signers.
|
|
5
|
+
*
|
|
6
|
+
* The build command records an Op run named `check` on the land branch's
|
|
7
|
+
* head, which passes unless the head holds `red.txt`. The serve judges
|
|
8
|
+
* `ci.green.preMerge` from that record: a green land moves main and the land
|
|
9
|
+
* branch goes; a red one leaves both. Each serve gets a fresh state
|
|
10
|
+
* directory, so its first poll only notes main's head and runs no job.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
18
|
+
import { cleanScratch, declaration, git, REPO, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
19
|
+
|
|
20
|
+
afterAll(cleanScratch);
|
|
21
|
+
|
|
22
|
+
const LOADER = pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href;
|
|
23
|
+
const GREEN = { branch: "main", phases: { check: ["check"], suites: ["suites"] }, require: ["check", "suites"], preMerge: ["check"], source: "runs" };
|
|
24
|
+
|
|
25
|
+
/** Records an unsigned `check` run on the checkout's HEAD: it fails when `red.txt` is there. */
|
|
26
|
+
const RECORD_CHECK = `
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
import { runOpLocally, OpRunFailure } from ${JSON.stringify(join(REPO, "packages/core/src/op/local-executor.ts"))};
|
|
29
|
+
const red = existsSync("red.txt");
|
|
30
|
+
const op = { name: "check", overview: "check", labels: { Env: "ci" }, phases: [{ name: "Run", steps: [{ kind: "activity", fn: "step", args: {} }] }] };
|
|
31
|
+
const fns = new Map([["step", async () => { if (red) throw new Error("red"); return {}; }]]);
|
|
32
|
+
try {
|
|
33
|
+
await runOpLocally(op, fns, {}, undefined, { cwd: process.cwd(), ledger: { cwd: process.cwd() } });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (!(err instanceof OpRunFailure)) throw err;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
function chant(cwd: string, ...args: string[]): Promise<{ code: number; out: string; err: string }> {
|
|
40
|
+
return new Promise((done) => {
|
|
41
|
+
const env: Record<string, string | undefined> = { ...process.env, GIT_TERMINAL_PROMPT: "0", NO_COLOR: "1" };
|
|
42
|
+
// No forge and no key: nothing here may lean on either.
|
|
43
|
+
for (const name of ["GITHUB_ACTIONS", "GITHUB_TOKEN", "GH_TOKEN", "CHANT_FORGE_TOKEN", "CHANT_RUNNER_KEY", "GITHUB_REPOSITORY"]) delete env[name];
|
|
44
|
+
const child = spawn(process.execPath, ["--import", LOADER, join(REPO, "packages/core/src/cli/main.ts"), ...args], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
45
|
+
let out = "";
|
|
46
|
+
let err = "";
|
|
47
|
+
child.stdout.on("data", (c) => (out += c));
|
|
48
|
+
child.stderr.on("data", (c) => (err += c));
|
|
49
|
+
child.on("close", (code) => done({ code: code ?? 1, out, err }));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function commit(root: string, file: string): string {
|
|
54
|
+
writeFiles(root, { [file]: `${file}\n` });
|
|
55
|
+
git(root, "add", "-A");
|
|
56
|
+
git(root, "commit", "-q", "-m", file);
|
|
57
|
+
return git(root, "rev-parse", "HEAD");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pushLand(root: string, name: string, files: string[]): string {
|
|
61
|
+
git(root, "checkout", "-q", "-b", `land-${name}`, "main");
|
|
62
|
+
let sha = "";
|
|
63
|
+
for (const f of files) sha = commit(root, f);
|
|
64
|
+
git(root, "push", "-q", "origin", `${sha}:refs/heads/chant/land/${name}`);
|
|
65
|
+
git(root, "checkout", "-q", "main");
|
|
66
|
+
return sha;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const remote = (origin: string, ref: string): string | null => {
|
|
70
|
+
try {
|
|
71
|
+
return git(origin, "rev-parse", "--verify", "--quiet", ref);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
describe("ci serve --forge git --land, end to end (#4168 4.4, #4016 gap 6)", () => {
|
|
78
|
+
let root: string;
|
|
79
|
+
let origin: string;
|
|
80
|
+
let main: string;
|
|
81
|
+
let build: string;
|
|
82
|
+
|
|
83
|
+
beforeAll(() => {
|
|
84
|
+
origin = scratchDir("chant-land-e2e-origin-");
|
|
85
|
+
git(origin, "init", "-q", "--bare");
|
|
86
|
+
root = scratchDir("chant-land-e2e-work-");
|
|
87
|
+
git(root, "init", "-q");
|
|
88
|
+
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
89
|
+
git(root, "config", "user.name", "t");
|
|
90
|
+
git(root, "config", "user.email", "t@example.com");
|
|
91
|
+
git(root, "remote", "add", "origin", origin);
|
|
92
|
+
writeFiles(root, {
|
|
93
|
+
"chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: GREEN } }),
|
|
94
|
+
"app/a.txt": "a\n",
|
|
95
|
+
});
|
|
96
|
+
main = commit(root, "init.txt");
|
|
97
|
+
git(root, "push", "-q", "origin", "main");
|
|
98
|
+
const script = join(scratchDir("chant-land-e2e-bin-"), "record-check.mts");
|
|
99
|
+
writeFileSync(script, RECORD_CHECK);
|
|
100
|
+
build = `${JSON.stringify(process.execPath)} --import ${JSON.stringify(LOADER)} ${JSON.stringify(script)}`;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const serve = (state: string) => chant(root, "ci", "serve", "--forge", "git", "--env", "prod", "--once", "--land", "--build-command", build, "--state", join(root, ".git", state));
|
|
104
|
+
|
|
105
|
+
test("a land whose pre-merge phase passes moves main to it, and the land branch is deleted", async () => {
|
|
106
|
+
const head = pushLand(root, "fix", ["fix-1.txt", "fix-2.txt"]);
|
|
107
|
+
const r = await serve("serve-green");
|
|
108
|
+
expect(r.err).toContain("no commit status is set");
|
|
109
|
+
expect(r.err).toContain(`chant/land/fix at ${head.slice(0, 12)} landed, and the land branch was deleted`);
|
|
110
|
+
expect(r.code).toBe(0);
|
|
111
|
+
expect(remote(origin, "refs/heads/main")).toBe(head);
|
|
112
|
+
expect(remote(origin, "refs/heads/chant/land/fix")).toBeNull();
|
|
113
|
+
main = head;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a land whose pre-merge phase fails leaves main and the land branch", async () => {
|
|
117
|
+
git(root, "fetch", "-q", "origin", "main");
|
|
118
|
+
git(root, "reset", "-q", "--hard", "origin/main");
|
|
119
|
+
const head = pushLand(root, "red", ["red.txt"]);
|
|
120
|
+
const r = await serve("serve-red");
|
|
121
|
+
expect(r.err).toContain(`chant/land/red at ${head.slice(0, 12)} was refused and left on the remote: the pre-merge phase check failed`);
|
|
122
|
+
expect(remote(origin, "refs/heads/main")).toBe(main);
|
|
123
|
+
expect(remote(origin, "refs/heads/chant/land/red")).toBe(head);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("--land without a build command, or without ci.green.preMerge, is refused before the first poll", async () => {
|
|
127
|
+
const noBuild = await chant(root, "ci", "serve", "--forge", "git", "--env", "prod", "--once", "--land");
|
|
128
|
+
expect(noBuild.code).toBe(1);
|
|
129
|
+
expect(noBuild.err).toContain("--land runs each land branch's pre-merge phases with --build-command");
|
|
130
|
+
|
|
131
|
+
const bare = scratchDir("chant-land-e2e-nopre-");
|
|
132
|
+
git(bare, "init", "-q");
|
|
133
|
+
writeFiles(bare, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: { ...GREEN, preMerge: undefined } } }) });
|
|
134
|
+
const noPre = await chant(bare, "ci", "serve", "--forge", "git", "--env", "prod", "--once", "--land", "--build-command", "true");
|
|
135
|
+
expect(noPre.code).toBe(1);
|
|
136
|
+
expect(noPre.err).toContain("the declaration names none");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4168 4.4 — `ci serve --land` fast-forwards the target to a pushed
|
|
3
|
+
* `chant/land/<name>` branch once its pre-merge phases pass on it, and
|
|
4
|
+
* #4016 gap 6 — `ci serve --forge git` reads a plain git server's refs.
|
|
5
|
+
*
|
|
6
|
+
* Each test works in its own clone with a bare origin. The build and the
|
|
7
|
+
* judge are fakes; `ci-serve-land.e2e.test.ts` runs the real ones.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { cleanScratch, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
13
|
+
import { ciServeLands, formatCiServeEvent, gitCiServeForge, newCiServeRuntime, readCiServeState, type CiServeEvent, type CiServeOptions } from "./ci-serve";
|
|
14
|
+
import type { CommitVerdict } from "./ci-green";
|
|
15
|
+
|
|
16
|
+
afterAll(cleanScratch);
|
|
17
|
+
|
|
18
|
+
function commit(root: string, file: string): string {
|
|
19
|
+
writeFiles(root, { [file]: `${file}\n` });
|
|
20
|
+
git(root, "add", "-A");
|
|
21
|
+
git(root, "commit", "-q", "-m", file);
|
|
22
|
+
return git(root, "rev-parse", "HEAD");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A clone on main with a bare origin. */
|
|
26
|
+
function clone(): { root: string; origin: string; main: string } {
|
|
27
|
+
const origin = scratchDir("chant-serve-land-origin-");
|
|
28
|
+
git(origin, "init", "-q", "--bare");
|
|
29
|
+
const root = scratchDir("chant-serve-land-");
|
|
30
|
+
git(root, "init", "-q");
|
|
31
|
+
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
32
|
+
git(root, "config", "user.name", "t");
|
|
33
|
+
git(root, "config", "user.email", "t@example.com");
|
|
34
|
+
git(root, "remote", "add", "origin", origin);
|
|
35
|
+
const main = commit(root, "a.txt");
|
|
36
|
+
git(root, "push", "-q", "origin", "main");
|
|
37
|
+
return { root, origin, main };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A commit on top of `from`, pushed as `chant/land/<name>`. */
|
|
41
|
+
function pushLand(root: string, name: string, from: string, file: string): string {
|
|
42
|
+
git(root, "checkout", "-q", "--detach", from);
|
|
43
|
+
const sha = commit(root, file);
|
|
44
|
+
git(root, "push", "-q", "--force", "origin", `${sha}:refs/heads/chant/land/${name}`);
|
|
45
|
+
git(root, "checkout", "-q", "main");
|
|
46
|
+
return sha;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const verdict = (phases: Record<string, "pass" | "fail" | "pending">): CommitVerdict => {
|
|
50
|
+
const list = Object.entries(phases).map(([phase, v]) => ({ phase, verdict: v, runs: [], unmatched: [] }));
|
|
51
|
+
return { verdict: list.some((p) => p.verdict === "fail") ? "fail" : list.some((p) => p.verdict === "pending") ? "pending" : "pass", phases: list, failure: null };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function options(root: string, log: CiServeEvent[], extra: Partial<CiServeOptions> = {}): CiServeOptions & { built: { commit: string; phases: string[]; reason: string }[] } {
|
|
55
|
+
const built: { commit: string; phases: string[]; reason: string }[] = [];
|
|
56
|
+
return {
|
|
57
|
+
cwd: root,
|
|
58
|
+
forge: "git",
|
|
59
|
+
repo: "origin",
|
|
60
|
+
env: "prod",
|
|
61
|
+
client: gitCiServeForge(root),
|
|
62
|
+
stateDir: join(root, ".git", "serve"),
|
|
63
|
+
buildCommand: "true",
|
|
64
|
+
build: async ({ request }) => (built.push({ ...request }), 0),
|
|
65
|
+
land: { phases: ["check"] },
|
|
66
|
+
judgeLand: async () => verdict({ check: "pass" }),
|
|
67
|
+
log: (e) => log.push(e),
|
|
68
|
+
built,
|
|
69
|
+
...extra,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const remoteHead = (origin: string, ref: string): string | undefined => {
|
|
74
|
+
try {
|
|
75
|
+
return git(origin, "rev-parse", "--verify", "--quiet", ref);
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
describe("the git forge kind (#4016 gap 6)", () => {
|
|
82
|
+
test("reads the branch head through ls-remote, lists no pull requests, and says when the branch is missing", async () => {
|
|
83
|
+
const { root, main } = clone();
|
|
84
|
+
const forge = gitCiServeForge(root);
|
|
85
|
+
expect(await forge.branchHead("main")).toBe(main);
|
|
86
|
+
expect(await forge.branchHead("nope")).toBeUndefined();
|
|
87
|
+
expect(await forge.openPullRequests("main")).toEqual([]);
|
|
88
|
+
expect(forge.pullRequestFor).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("ci serve --land (#4168 4.4)", () => {
|
|
93
|
+
test("a land whose pre-merge phases pass moves the target to it and deletes the land branch, once", async () => {
|
|
94
|
+
const { root, origin, main } = clone();
|
|
95
|
+
const head = pushLand(root, "fix", main, "b.txt");
|
|
96
|
+
const log: CiServeEvent[] = [];
|
|
97
|
+
const opts = options(root, log);
|
|
98
|
+
const rt = newCiServeRuntime();
|
|
99
|
+
const pass = await ciServeLands(opts, rt);
|
|
100
|
+
await Promise.all(pass.started);
|
|
101
|
+
|
|
102
|
+
expect(opts.built).toEqual([{ commit: head, phases: ["check"], reason: "land" }]);
|
|
103
|
+
expect(remoteHead(origin, "refs/heads/main")).toBe(head);
|
|
104
|
+
expect(remoteHead(origin, "refs/heads/chant/land/fix")).toBeUndefined();
|
|
105
|
+
const state = readCiServeState(join(root, ".git", "serve", "chant-apply-prod.json"), { forge: "git", repo: "origin", env: "prod", branch: "main" });
|
|
106
|
+
expect(state.lands?.[`land:fix:${head}`]).toMatchObject({ branch: "chant/land/fix", commit: head, status: "landed" });
|
|
107
|
+
expect(log.map(formatCiServeEvent)).toContain(`chant/land/fix at ${head.slice(0, 12)} landed, and the land branch was deleted`);
|
|
108
|
+
|
|
109
|
+
// Nothing is left to serve.
|
|
110
|
+
expect((await ciServeLands(opts, rt)).started).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a red pre-merge phase refuses the land and leaves the target and the land branch", async () => {
|
|
114
|
+
const { root, origin, main } = clone();
|
|
115
|
+
const head = pushLand(root, "red", main, "b.txt");
|
|
116
|
+
const log: CiServeEvent[] = [];
|
|
117
|
+
const opts = options(root, log, { judgeLand: async () => verdict({ check: "fail" }) });
|
|
118
|
+
await Promise.all((await ciServeLands(opts, newCiServeRuntime())).started);
|
|
119
|
+
|
|
120
|
+
expect(remoteHead(origin, "refs/heads/main")).toBe(main);
|
|
121
|
+
expect(remoteHead(origin, "refs/heads/chant/land/red")).toBe(head);
|
|
122
|
+
expect(log.map(formatCiServeEvent)).toContain(`chant/land/red at ${head.slice(0, 12)} was refused and left on the remote: the pre-merge phase check failed`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a phase with no finished run, or a build that fails, refuses the land", async () => {
|
|
126
|
+
const { root, origin, main } = clone();
|
|
127
|
+
const a = pushLand(root, "norun", main, "b.txt");
|
|
128
|
+
const b = pushLand(root, "broken", main, "c.txt");
|
|
129
|
+
const log: CiServeEvent[] = [];
|
|
130
|
+
const opts = options(root, log, {
|
|
131
|
+
maxJobs: 2,
|
|
132
|
+
judgeLand: async () => verdict({ check: "pending" }),
|
|
133
|
+
build: async ({ request }) => (request.commit === b ? 3 : 0),
|
|
134
|
+
});
|
|
135
|
+
await Promise.all((await ciServeLands(opts, newCiServeRuntime())).started);
|
|
136
|
+
|
|
137
|
+
expect(remoteHead(origin, "refs/heads/main")).toBe(main);
|
|
138
|
+
const lines = log.map(formatCiServeEvent);
|
|
139
|
+
expect(lines).toContain(`chant/land/norun at ${a.slice(0, 12)} was refused and left on the remote: the build recorded no finished run for check`);
|
|
140
|
+
expect(lines).toContain(`chant/land/broken at ${b.slice(0, 12)} was refused and left on the remote: the build of check exited 3`);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a land that does not descend from the target is refused without a build, and a new push to it is tried again", async () => {
|
|
144
|
+
const { root, origin, main } = clone();
|
|
145
|
+
const stale = pushLand(root, "old", main, "b.txt");
|
|
146
|
+
const moved = commit(root, "c.txt");
|
|
147
|
+
git(root, "push", "-q", "origin", "main");
|
|
148
|
+
const log: CiServeEvent[] = [];
|
|
149
|
+
const opts = options(root, log);
|
|
150
|
+
const rt = newCiServeRuntime();
|
|
151
|
+
await Promise.all((await ciServeLands(opts, rt)).started);
|
|
152
|
+
|
|
153
|
+
expect(opts.built).toEqual([]);
|
|
154
|
+
expect(remoteHead(origin, "refs/heads/main")).toBe(moved);
|
|
155
|
+
expect(log.map(formatCiServeEvent)).toContain(
|
|
156
|
+
`chant/land/old at ${stale.slice(0, 12)} was refused and left on the remote: ${stale.slice(0, 12)} does not descend from main at ${moved.slice(0, 12)}; rebase chant/land/old onto it and push again`,
|
|
157
|
+
);
|
|
158
|
+
// The same head is not tried twice.
|
|
159
|
+
expect((await ciServeLands(opts, rt)).started).toEqual([]);
|
|
160
|
+
|
|
161
|
+
// Rebased and pushed again, it lands.
|
|
162
|
+
const rebased = pushLand(root, "old", moved, "b.txt");
|
|
163
|
+
await Promise.all((await ciServeLands(opts, rt)).started);
|
|
164
|
+
expect(remoteHead(origin, "refs/heads/main")).toBe(rebased);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("lands wait for capacity", async () => {
|
|
168
|
+
const { root, main } = clone();
|
|
169
|
+
const head = pushLand(root, "fix", main, "b.txt");
|
|
170
|
+
const log: CiServeEvent[] = [];
|
|
171
|
+
const rt = newCiServeRuntime();
|
|
172
|
+
rt.inflight.set("busy", new Promise(() => {}));
|
|
173
|
+
expect((await ciServeLands(options(root, log), rt)).started).toEqual([]);
|
|
174
|
+
expect(log.map(formatCiServeEvent)).toContain(`chant/land/fix at ${head.slice(0, 12)} waits: --max-jobs jobs run`);
|
|
175
|
+
});
|
|
176
|
+
});
|