@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant release compose` end to end (#4168 4.3, mode b): a chosen trunk
|
|
3
|
+
* change composed onto a released commit while trunk carries unready work,
|
|
4
|
+
* a conflict that stops with nothing written, the tick judging the pushed
|
|
5
|
+
* compose branch, and a cut of the composed commit that records its base,
|
|
6
|
+
* its sources and a publish channel. A temp repo with a temp bare remote;
|
|
7
|
+
* the tick's forge is a fake that answers check runs. No key or signer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { REPO } from "../../workspace/__fixtures__/contract-repo";
|
|
17
|
+
import { ciTick } from "../../workspace/ci-green";
|
|
18
|
+
import type { CheckRun, CiForge } from "../../workspace/ci-green-forge";
|
|
19
|
+
|
|
20
|
+
const CHANT = [process.execPath, "--import", pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href, join(REPO, "packages", "core", "src", "cli", "main.ts")];
|
|
21
|
+
const CI_VARS = ["CI", "GITHUB_ACTIONS", "GITHUB_RUN_ID", "GITHUB_SHA", "GITHUB_ACTOR", "GITLAB_CI", "CI_PIPELINE_ID", "CHANT_ENV", "CHANT_RELEASE_LABEL"];
|
|
22
|
+
|
|
23
|
+
function chant(cwd: string, args: string[]): { code: number; stdout: string; stderr: string } {
|
|
24
|
+
const env: NodeJS.ProcessEnv = { ...process.env, USER: "tester" };
|
|
25
|
+
for (const k of CI_VARS) delete env[k];
|
|
26
|
+
const r = spawnSync(CHANT[0], [...CHANT.slice(1), ...args], { cwd, encoding: "utf-8", env, timeout: 120_000 });
|
|
27
|
+
return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function git(cwd: string, ...args: string[]): string {
|
|
31
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
32
|
+
if (r.status !== 0) throw new Error(r.stderr);
|
|
33
|
+
return r.stdout.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const BUILD = `mkdir -p dist && printf '{"env":"%s","marker":"%s","fix":"%s"}' "$CHANT_ENV" "$(cat marker.txt)" "$(cat fix.txt 2>/dev/null)" > dist/api.template.json`;
|
|
37
|
+
const PASSING: CheckRun[] = [{ id: 1, name: "test", status: "completed", conclusion: "success", url: null } as unknown as CheckRun];
|
|
38
|
+
|
|
39
|
+
describe("chant release compose", () => {
|
|
40
|
+
test(
|
|
41
|
+
"composes a fix onto a released commit, stops on a conflict, and cuts the composed commit once the tick has judged it green",
|
|
42
|
+
async () => {
|
|
43
|
+
const root = mkdtempSync(join(tmpdir(), "chant-release-compose-"));
|
|
44
|
+
try {
|
|
45
|
+
const dir = join(root, "work");
|
|
46
|
+
const remote = join(root, "origin.git");
|
|
47
|
+
git(root, "init", "-q", "--bare", "-b", "main", remote);
|
|
48
|
+
git(root, "init", "-q", "-b", "main", dir);
|
|
49
|
+
git(dir, "config", "user.email", "t@chant.dev");
|
|
50
|
+
git(dir, "config", "user.name", "T");
|
|
51
|
+
writeFileSync(
|
|
52
|
+
join(dir, "chant.workspace.json"),
|
|
53
|
+
JSON.stringify({ name: "w", schema: 1, members: [{ name: "api", dir: ".", kind: "chant" }], ci: { green: { branch: "main", phases: { test: ["test"] }, require: ["test"] } } }, null, 2),
|
|
54
|
+
);
|
|
55
|
+
writeFileSync(join(dir, "chant.config.ts"), `export default { environments: ["dev", "prod"] };\n`);
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(dir, "api.component.ts"),
|
|
58
|
+
`export const api = { name: "api", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "dist/api.template.json" }] }] };\n`,
|
|
59
|
+
);
|
|
60
|
+
writeFileSync(join(dir, ".gitignore"), "dist/\n");
|
|
61
|
+
writeFileSync(join(dir, "marker.txt"), "one");
|
|
62
|
+
git(dir, "add", ".");
|
|
63
|
+
git(dir, "commit", "-q", "-m", "one (#11)");
|
|
64
|
+
const one = git(dir, "rev-parse", "HEAD");
|
|
65
|
+
// Trunk moves on: unready work, a fix, and work on top of the unready work.
|
|
66
|
+
writeFileSync(join(dir, "marker.txt"), "two");
|
|
67
|
+
git(dir, "commit", "-q", "-am", "unready (#12)");
|
|
68
|
+
writeFileSync(join(dir, "fix.txt"), "fixed");
|
|
69
|
+
git(dir, "add", "fix.txt");
|
|
70
|
+
git(dir, "commit", "-q", "-m", "the fix (#13)\n\nChant-Record: work:fix-1");
|
|
71
|
+
const fix = git(dir, "rev-parse", "HEAD");
|
|
72
|
+
writeFileSync(join(dir, "marker.txt"), "four");
|
|
73
|
+
git(dir, "commit", "-q", "-am", "more on unready (#14)");
|
|
74
|
+
git(dir, "remote", "add", "origin", remote);
|
|
75
|
+
git(dir, "push", "-q", "origin", "main");
|
|
76
|
+
git(dir, "fetch", "-q", "origin");
|
|
77
|
+
|
|
78
|
+
// A change that needs the unready one stops, naming it and the file, and writes nothing.
|
|
79
|
+
const conflict = chant(dir, ["release", "compose", "bad", "--base", one, "--pr", "14", "--yes"]);
|
|
80
|
+
expect(conflict.code).toBe(1);
|
|
81
|
+
expect(conflict.stderr).toContain("[compose-conflict]");
|
|
82
|
+
expect(conflict.stderr).toContain("marker.txt");
|
|
83
|
+
expect(spawnSync("git", ["rev-parse", "--verify", "--quiet", "refs/heads/chant/compose/bad"], { cwd: remote }).status).not.toBe(0);
|
|
84
|
+
|
|
85
|
+
const dry = chant(dir, ["release", "compose", "hotfix", "--base", one, "--work", "fix-1", "--json"]);
|
|
86
|
+
expect(dry.code, dry.stderr).toBe(0);
|
|
87
|
+
expect(JSON.parse(dry.stdout)).toMatchObject({ dryRun: true, written: false, pushed: false, picked: [{ commit: fix, pr: 13, work: ["fix-1"] }] });
|
|
88
|
+
|
|
89
|
+
const composed = chant(dir, ["release", "compose", "hotfix", "--base", one, "--pr", "13", "--yes", "--json"]);
|
|
90
|
+
expect(composed.code, composed.stderr).toBe(0);
|
|
91
|
+
const tip = JSON.parse(composed.stdout).tip as string;
|
|
92
|
+
expect(git(remote, "rev-parse", "refs/heads/chant/compose/hotfix")).toBe(tip);
|
|
93
|
+
expect(git(dir, "show", "-s", "--format=%(trailers:key=Chant-Composed-From,valueonly)", tip)).toBe(fix);
|
|
94
|
+
|
|
95
|
+
// Not green yet, so no cut.
|
|
96
|
+
const early = chant(dir, ["release", "cut", "v1.0.1", "--at", tip, "--build", BUILD]);
|
|
97
|
+
expect(early.code).toBe(1);
|
|
98
|
+
expect(early.stderr).toContain("[release-commit-not-green]");
|
|
99
|
+
|
|
100
|
+
// The tick walks the compose branch and tags the composed commit green.
|
|
101
|
+
const forge: CiForge = { kind: "github", checkRuns: async () => PASSING };
|
|
102
|
+
const tick = await ciTick({ cwd: dir, forge, now: new Date() });
|
|
103
|
+
expect(tick.composed.map((b) => b.branch)).toEqual(["chant/compose/hotfix"]);
|
|
104
|
+
expect(tick.made).toContain(`ci/green/${tip}`);
|
|
105
|
+
|
|
106
|
+
const cut = chant(dir, ["release", "cut", "v1.0.1", "--at", tip, "--build", BUILD, "--channel", "hotfix", "--yes", "--json"]);
|
|
107
|
+
expect(cut.code, cut.stderr).toBe(0);
|
|
108
|
+
const result = JSON.parse(cut.stdout);
|
|
109
|
+
expect(result.release.commit).toBe(tip);
|
|
110
|
+
expect(result.composed).toEqual({ branch: "chant/compose/hotfix", base: one, sources: [fix] });
|
|
111
|
+
expect(result.cut).toMatchObject({ label: "v1.0.1", channel: "hotfix", composed: { base: one, sources: [fix] }, signature: null });
|
|
112
|
+
|
|
113
|
+
// The release carries the fix on the released marker, not trunk's unready one.
|
|
114
|
+
const out = join(root, "export");
|
|
115
|
+
expect(chant(dir, ["release", "export", "v1.0.1", "--env", "prod", "-o", out]).code).toBe(0);
|
|
116
|
+
expect(JSON.parse(spawnSync("cat", [join(out, "dist", "api.template.json")], { encoding: "utf-8" }).stdout)).toEqual({ env: "prod", marker: "one", fix: "fixed" });
|
|
117
|
+
|
|
118
|
+
// A commit that is on neither trunk nor a compose branch is still refused.
|
|
119
|
+
git(dir, "checkout", "-q", "-b", "side", one);
|
|
120
|
+
writeFileSync(join(dir, "marker.txt"), "side");
|
|
121
|
+
git(dir, "commit", "-q", "-am", "side");
|
|
122
|
+
const side = git(dir, "rev-parse", "HEAD");
|
|
123
|
+
git(dir, "checkout", "-q", "main");
|
|
124
|
+
const refused = chant(dir, ["release", "cut", "v9", "--at", side, "--build", BUILD]);
|
|
125
|
+
expect(refused.code).toBe(1);
|
|
126
|
+
expect(refused.stderr).toContain("[release-commit-not-on-trunk]");
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(root, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
300_000,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* release candidates [--env <env>] [--json]
|
|
21
21
|
* the green commits of trunk a release can be cut from, each with what it
|
|
22
22
|
* would ship, and what each environment runs (#4168 4.2)
|
|
23
|
+
* release compose <name> [--base <label|sha>] (--pr <n> | --work <id>)... [--yes] [--json]
|
|
24
|
+
* pick chosen trunk changes onto a base, each with a Chant-Composed-From
|
|
25
|
+
* trailer, on the pushed branch chant/compose/<name> (#4168 4.3); a dry
|
|
26
|
+
* run unless --yes. `release cut` takes a green commit of that branch,
|
|
27
|
+
* and `--channel <tag>` makes its publish move that tag, not latest
|
|
23
28
|
* release list [--json]
|
|
24
29
|
* every release of this member with its targets, its cuts and where it runs;
|
|
25
30
|
* `--json` prints the read contract's `releases` document
|
|
@@ -42,7 +47,7 @@ import { parseParamFlags } from "../build-params-cli";
|
|
|
42
47
|
import type { CommandContext } from "../registry";
|
|
43
48
|
|
|
44
49
|
const USAGE =
|
|
45
|
-
"chant release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json] | release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json] | release verify <label|digest> [--json] | release candidates [--env <env>] [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
|
|
50
|
+
"chant release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--channel <tag>] [--json] | release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json] | release verify <label|digest> [--json] | release candidates [--env <env>] [--json] | release compose <name> [--base <label|sha>] (--pr <n> | --work <id>)... [--yes] [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
|
|
46
51
|
|
|
47
52
|
/** The build inputs `release cut` and `release compile` share. */
|
|
48
53
|
function compileInputs(
|
|
@@ -155,6 +160,7 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
155
160
|
let trunk: import("../../lifecycle/release-workstation").Trunk | null = null;
|
|
156
161
|
let signers: readonly import("../../workspace/trust/trust-files").Signer[] = [];
|
|
157
162
|
let member: string | null = null;
|
|
163
|
+
let composed: { branch: string; base: string; sources: string[] } | undefined;
|
|
158
164
|
if (args.at !== undefined && !top) {
|
|
159
165
|
return fail("not-a-git-repository", "--at names a commit, and this directory is not in a git repository");
|
|
160
166
|
}
|
|
@@ -169,7 +175,18 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
169
175
|
const { releaseRuleFrom } = await import("../../workspace/identity");
|
|
170
176
|
const source = scopeSource(cwd);
|
|
171
177
|
trunk = ws.resolveTrunk(top, source.declaration?.ci?.green?.branch ?? null);
|
|
172
|
-
|
|
178
|
+
try {
|
|
179
|
+
ws.checkOnTrunk(top, commit, trunk);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// #4168 4.3: a commit of a pushed compose branch is cut too, and the cut records what it carries.
|
|
182
|
+
if (!(err instanceof ws.WorkstationCheckError) || err.code !== "release-commit-not-on-trunk") throw err;
|
|
183
|
+
const refs = await import("../../lifecycle/compose-refs");
|
|
184
|
+
const on = refs.composeBranchContaining(top, commit, trunk.remote);
|
|
185
|
+
const trunkRef = `refs/remotes/${trunk.remote}/${trunk.branch}`;
|
|
186
|
+
const composition = on ? refs.compositionOnTrunk(top, commit, trunkRef, on.branch) : null;
|
|
187
|
+
if (!on || !composition) throw err;
|
|
188
|
+
composed = { branch: on.branch, base: composition.base, sources: composition.sources };
|
|
189
|
+
}
|
|
173
190
|
signers = releaseRuleFrom(source).rule.signers.signers;
|
|
174
191
|
const { resolveMemberLedger } = await import("../../lifecycle/member-ledger");
|
|
175
192
|
const ledger = await resolveMemberLedger(cwd);
|
|
@@ -206,7 +223,15 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
206
223
|
if (args.at !== undefined && top && commit) checkout = at.checkoutCommit(top, cwd, commit);
|
|
207
224
|
const cutCwd = checkout?.memberDir ?? cwd;
|
|
208
225
|
const compiler = spawnTargetCompiler(compileInputs(ctx, params, cutCwd, label));
|
|
209
|
-
const request = {
|
|
226
|
+
const request = {
|
|
227
|
+
cwd: cutCwd,
|
|
228
|
+
compiler,
|
|
229
|
+
label,
|
|
230
|
+
actor,
|
|
231
|
+
...(args.sign !== undefined ? { sign: args.sign } : {}),
|
|
232
|
+
...(composed ? { composed } : {}),
|
|
233
|
+
...(args.channel ? { channel: args.channel } : {}),
|
|
234
|
+
};
|
|
210
235
|
|
|
211
236
|
if (dryRun) {
|
|
212
237
|
const plan = await planRelease(request);
|
|
@@ -233,6 +258,8 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
233
258
|
actorSource: who.source,
|
|
234
259
|
run: ci ? { runId: ci.runId, runOrigin: ci.runOrigin } : { runId: null, runOrigin: { forge: "local" as const } },
|
|
235
260
|
trunk,
|
|
261
|
+
composed: composed ?? null,
|
|
262
|
+
channel: args.channel ?? null,
|
|
236
263
|
at: args.at !== undefined ? { ref: args.at, commit } : null,
|
|
237
264
|
sign: args.sign !== undefined ? { key: publicKey !== null ? publicKey.split(" ")[0] : null, readable: publicKey !== null } : null,
|
|
238
265
|
tag: tag ? { name: tag.name, exists: tag.state === "present" } : null,
|
|
@@ -244,7 +271,13 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
244
271
|
return 0;
|
|
245
272
|
}
|
|
246
273
|
const name = plan.label ? formatBold(plan.label) : "an unlabelled release";
|
|
247
|
-
|
|
274
|
+
const where = composed
|
|
275
|
+
? ` (on ${trunk!.remote}/${composed.branch}, composed on ${composed.base.slice(0, 12)} from ${composed.sources.length} trunk commit(s), green)`
|
|
276
|
+
: trunk
|
|
277
|
+
? ` (on ${trunk.remote}/${trunk.branch}, green)`
|
|
278
|
+
: "";
|
|
279
|
+
console.log(`Dry run: cut ${name} from commit ${plan.commit.slice(0, 12)}${where}`);
|
|
280
|
+
if (args.channel) console.log(` channel ${args.channel}: a publish moves ${args.channel}, not latest`);
|
|
248
281
|
if (plan.alreadyCut) console.log(` already cut as ${plan.alreadyCut}: the cut would record nothing new`);
|
|
249
282
|
console.log(` actor ${actor} (from ${describeActorSource(who.source)})`);
|
|
250
283
|
console.log(` run ${ci ? `${ci.runId} (${ci.runOrigin.forge})` : "minted on this machine at the cut (local)"}`);
|
|
@@ -299,7 +332,7 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
299
332
|
if (args.json) {
|
|
300
333
|
console.log(
|
|
301
334
|
JSON.stringify(
|
|
302
|
-
{ dryRun: false, release: result.release, label: result.label, cut: result.cut, stored: result.stored, reused: result.reused, actor, actorSource: who.source, tag: tagged, next },
|
|
335
|
+
{ dryRun: false, release: result.release, label: result.label, cut: result.cut, stored: result.stored, reused: result.reused, actor, actorSource: who.source, tag: tagged, composed: composed ?? null, channel: args.channel ?? null, next },
|
|
303
336
|
null,
|
|
304
337
|
2,
|
|
305
338
|
),
|
|
@@ -308,7 +341,8 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
|
|
|
308
341
|
const name = result.label ? formatBold(result.label) : "unlabelled release";
|
|
309
342
|
if (result.cut) console.error(formatSuccess(`Cut ${name}: ${result.release.releaseDigest}`));
|
|
310
343
|
else console.error(formatSuccess(`Already cut${result.label ? ` as ${formatBold(result.label)}` : ""}: ${result.release.releaseDigest}`));
|
|
311
|
-
console.error(` commit ${result.release.commit}`);
|
|
344
|
+
console.error(` commit ${result.release.commit}${composed ? `, composed on ${composed.base.slice(0, 12)} on ${composed.branch}` : ""}`);
|
|
345
|
+
if (args.channel) console.error(` channel ${args.channel}`);
|
|
312
346
|
if (result.cut) console.error(` ${result.cut.signature ? `signed by ${result.cut.actor}` : `not signed; actor ${result.cut.actor}`}, run ${result.cut.runId}`);
|
|
313
347
|
console.error(` artifacts ${result.release.artifactDigests.length}, targets ${result.release.targets.map((t) => `${t.env} (${t.templates.length} template${t.templates.length === 1 ? "" : "s"})`).join(", ")}`);
|
|
314
348
|
for (const r of result.reused) console.error(` ${r.component}: artifacts unchanged, carried from ${r.from}`);
|
|
@@ -386,7 +420,8 @@ export async function runReleasePublish(ctx: CommandContext): Promise<number> {
|
|
|
386
420
|
// The warnings go to stderr with --json too, before any target runs.
|
|
387
421
|
if (signatureWarning) console.error(formatWarning({ message: signatureWarning }));
|
|
388
422
|
if (warning) console.error(formatWarning({ message: warning }));
|
|
389
|
-
|
|
423
|
+
// #4168 4.3: a release cut with --channel moves that tag, not latest.
|
|
424
|
+
const pctx = publishContext(prepared, { cwd, tag: prepared.channel, log: (line) => (args.json ? undefined : console.error(` ${line}`)) });
|
|
390
425
|
|
|
391
426
|
// Every target is planned before any publishes, so a missing file or credential stops the publish with nothing sent.
|
|
392
427
|
const progress = publishProgress(await readPublishRecords(release.releaseDigest, { cwd }).catch(() => []));
|
|
@@ -539,9 +574,14 @@ export async function runReleaseCandidates(ctx: CommandContext): Promise<number>
|
|
|
539
574
|
for (const c of doc.candidates) {
|
|
540
575
|
const tags = [...(c.newFor.length > 0 ? [`new for ${c.newFor.join(", ")}`] : []), ...(c.cut.length > 0 ? [`cut as ${c.cut.join(", ")}`] : [])];
|
|
541
576
|
console.log(`${c.commit.slice(0, 12)} ${c.subject}${tags.length > 0 ? ` (${tags.join("; ")})` : ""}`);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
577
|
+
// #4168 4.4: by work item, then pull request, then commit.
|
|
578
|
+
const subjectOf = new Map(c.changes.map((ch) => [ch.commit, ch.subject]));
|
|
579
|
+
for (const g of c.groups) {
|
|
580
|
+
console.log(` ${g.work !== null ? `work:${g.work}` : "no work item"}`);
|
|
581
|
+
for (const p of g.pullRequests) {
|
|
582
|
+
console.log(` ${p.pr !== null ? `#${p.pr}` : "no pull request"}`);
|
|
583
|
+
for (const sha of p.commits) console.log(` ${sha.slice(0, 12)} ${subjectOf.get(sha) ?? ""}`);
|
|
584
|
+
}
|
|
545
585
|
}
|
|
546
586
|
for (const f of c.flags) console.log(` flag ${f.id} (${f.member} ${f.param}) changed`);
|
|
547
587
|
}
|
|
@@ -661,3 +701,65 @@ export async function runReleaseUnknown(ctx: CommandContext): Promise<number> {
|
|
|
661
701
|
console.error(formatError({ message: `Unknown release subcommand: ${ctx.args.path}`, hint: USAGE }));
|
|
662
702
|
return 1;
|
|
663
703
|
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* `release compose <name> [--base <label|sha>] (--pr <n> | --work <id>)... [--yes] [--json]`
|
|
707
|
+
* (#4168 4.3): pick the chosen trunk changes onto a base, on
|
|
708
|
+
* `chant/compose/<name>` (../../lifecycle/release-compose.ts). A dry run
|
|
709
|
+
* unless `--yes`, which moves the branch and pushes it so CI builds it.
|
|
710
|
+
*/
|
|
711
|
+
export async function runReleaseCompose(ctx: CommandContext): Promise<number> {
|
|
712
|
+
const { args } = ctx;
|
|
713
|
+
const name = args.extraPositional;
|
|
714
|
+
if (!name) {
|
|
715
|
+
console.error(formatError({ message: "release compose needs a name for the compose branch", hint: USAGE }));
|
|
716
|
+
return 1;
|
|
717
|
+
}
|
|
718
|
+
const cwd = resolve(".");
|
|
719
|
+
const { composeRelease, ComposeError } = await import("../../lifecycle/release-compose");
|
|
720
|
+
const ws = await import("../../lifecycle/release-workstation");
|
|
721
|
+
const { scopeSource } = await import("../../workspace/write-scope");
|
|
722
|
+
const top = (() => {
|
|
723
|
+
try {
|
|
724
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
725
|
+
} catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
})();
|
|
729
|
+
if (!top) {
|
|
730
|
+
console.error(formatError({ message: "a compose picks commits, and this directory is not in a git repository [not-a-git-repository]" }));
|
|
731
|
+
return 1;
|
|
732
|
+
}
|
|
733
|
+
const trunk = ws.resolveTrunk(top, scopeSource(cwd).declaration?.ci?.green?.branch ?? null);
|
|
734
|
+
try {
|
|
735
|
+
const r = await composeRelease({
|
|
736
|
+
cwd,
|
|
737
|
+
name,
|
|
738
|
+
...(args.base !== undefined ? { base: args.base } : {}),
|
|
739
|
+
prs: args.prs ?? [],
|
|
740
|
+
works: args.works ?? [],
|
|
741
|
+
yes: args.yes === true,
|
|
742
|
+
trunk,
|
|
743
|
+
});
|
|
744
|
+
if (args.json) {
|
|
745
|
+
console.log(JSON.stringify({ dryRun: args.yes !== true, ...r, next: r.written || r.pushed ? [`chant release cut <label> --at ${r.tip}`] : [sameWithYes(process.argv.slice(2))] }, null, 2));
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
const verb = args.yes === true ? "Composed" : "Dry run: would compose";
|
|
749
|
+
console.log(`${verb} ${formatBold(r.branch)} on ${r.base.slice(0, 12)}${r.from ? `, after ${r.from.slice(0, 12)}` : ""}`);
|
|
750
|
+
for (const s of r.picked) console.log(` pick ${s.commit.slice(0, 12)} ${s.subject} [${s.chosenBy.join(", ")}]`);
|
|
751
|
+
for (const s of r.already) console.log(` carried already ${s.commit.slice(0, 12)} ${s.subject}`);
|
|
752
|
+
if (r.picked.length === 0) console.log(" nothing new to pick");
|
|
753
|
+
console.log(` tip ${r.tip.slice(0, 12)}${r.pushed ? `, pushed to ${trunk.remote}` : ""}`);
|
|
754
|
+
if (args.yes !== true) console.error(`Dry run: no branch was moved or pushed. Run again with --yes: ${sameWithYes(process.argv.slice(2))}`);
|
|
755
|
+
else console.error(`Next: once CI tags ${r.tip.slice(0, 12)} green, cut it: chant release cut <label> --at ${r.tip} --channel <channel>`);
|
|
756
|
+
return 0;
|
|
757
|
+
} catch (err) {
|
|
758
|
+
if (err instanceof ComposeError) {
|
|
759
|
+
if (args.json) console.log(JSON.stringify({ error: { code: err.code, message: err.message, ...(err.conflict ? { conflict: err.conflict } : {}) } }, null, 2));
|
|
760
|
+
console.error(formatError({ message: `${err.message} [${err.code}]`, ...(err.fix ? { hint: err.fix } : {}) }));
|
|
761
|
+
return 1;
|
|
762
|
+
}
|
|
763
|
+
throw err;
|
|
764
|
+
}
|
|
765
|
+
}
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -933,6 +933,8 @@ export async function recordAutoReleasesForRun(
|
|
|
933
933
|
disabled: boolean,
|
|
934
934
|
runOrigin: RunOrigin = { forge: "local" },
|
|
935
935
|
releaseDigest?: string,
|
|
936
|
+
/** #4222: the digest of the templates the release records for a component in `env`, for one that publishes no artifact. */
|
|
937
|
+
templatesDigestOf?: (component: string) => string | null,
|
|
936
938
|
): Promise<ReleaseRecord[]> {
|
|
937
939
|
const recorded: ReleaseRecord[] = [];
|
|
938
940
|
for (const componentResult of results) {
|
|
@@ -951,6 +953,7 @@ export async function recordAutoReleasesForRun(
|
|
|
951
953
|
...(gateAdmission(componentResult) ? { admission: gateAdmission(componentResult) } : {}),
|
|
952
954
|
// #4043: the release the deploy was made under, when `--release` named one.
|
|
953
955
|
...(releaseDigest !== undefined ? { releaseDigest } : {}),
|
|
956
|
+
...(releaseDigest !== undefined && templatesDigestOf?.(componentResult.component) ? { templatesDigest: templatesDigestOf(componentResult.component)! } : {}),
|
|
954
957
|
},
|
|
955
958
|
{ disabled },
|
|
956
959
|
);
|
|
@@ -1083,6 +1086,13 @@ export async function resolveRunRelease(ref: string, env: string, cwd: string) {
|
|
|
1083
1086
|
}
|
|
1084
1087
|
|
|
1085
1088
|
/** A rebuilt image need not digest as the release's own did; say so, as promote does. Never fails the run. */
|
|
1089
|
+
/** #4222: a component's templates digest in the release's target for `env`, or null when the release has no such target. */
|
|
1090
|
+
async function releaseTemplatesDigestOf(release: import("../../lifecycle/release-store").Release, env: string): Promise<(component: string) => string | null> {
|
|
1091
|
+
const { targetFor, releaseTemplatesDigest } = await import("../../components/release-promote");
|
|
1092
|
+
const found = targetFor(release, env);
|
|
1093
|
+
return (component) => ("target" in found ? releaseTemplatesDigest(found.target, component) : null);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1086
1096
|
async function warnOnArtifactMismatch(release: import("../../lifecycle/release-store").Release, recorded: readonly ReleaseRecord[]): Promise<void> {
|
|
1087
1097
|
try {
|
|
1088
1098
|
const { releaseArtifactMismatches } = await import("../../components/release-promote");
|
|
@@ -1270,7 +1280,8 @@ async function runComponentSelection(ctx: CommandContext): Promise<number> {
|
|
|
1270
1280
|
if (result.success && result.run) {
|
|
1271
1281
|
// #3955: a run that records nothing has no release to report either.
|
|
1272
1282
|
const disabled = resolveAutoReleaseDisabled(config, ctx.args.noReleaseRecord) || ledgerRecordingOff();
|
|
1273
|
-
const
|
|
1283
|
+
const templatesDigestOf = underRelease ? await releaseTemplatesDigestOf(underRelease.release, env) : undefined;
|
|
1284
|
+
const recorded = await recordAutoReleasesForRun(result.run.results, env, release.runId, disabled, release.runOrigin, underRelease?.releaseDigest, templatesDigestOf);
|
|
1274
1285
|
if (underRelease) await warnOnArtifactMismatch(underRelease.release, recorded);
|
|
1275
1286
|
// `--digest-file` (#2602): the releases this run recorded, one
|
|
1276
1287
|
// `<component>=<digest>` line each, the form `chant components promote
|
package/src/cli/main.ts
CHANGED
|
@@ -342,6 +342,12 @@ CI:
|
|
|
342
342
|
key. --trust commits its runner entry on a branch of its
|
|
343
343
|
own and pushes it for review; the target branch is never
|
|
344
344
|
written
|
|
345
|
+
ci init --workflow trunk-curated|trunk-curated+apply-on-merge
|
|
346
|
+
[--forge github|gitlab|forgejo] [--security <group>]... [--yes]
|
|
347
|
+
Set the workspace up for a prescribed delivery workflow:
|
|
348
|
+
print what it would write as a diff, then write it on
|
|
349
|
+
--yes or a yes at the prompt and run the generators. A
|
|
350
|
+
rerun proposes only what workspace check still reports
|
|
345
351
|
ci workflow [--workflow <name>]... [--output <file>] Write
|
|
346
352
|
[--chant <command>] [--install <command>]
|
|
347
353
|
[--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>]
|
|
@@ -362,7 +368,7 @@ CI:
|
|
|
362
368
|
Releases:
|
|
363
369
|
release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param <k=v>]
|
|
364
370
|
[--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>]
|
|
365
|
-
[--actor <name>] [--sign [<key file>]] [--json]
|
|
371
|
+
[--actor <name>] [--sign [<key file>]] [--channel <tag>] [--json]
|
|
366
372
|
A dry run of a cut of this member from HEAD, which
|
|
367
373
|
must be clean, on trunk and pushed, and carry a
|
|
368
374
|
ci/green tag: compile every environment in the
|
|
@@ -383,6 +389,18 @@ Releases:
|
|
|
383
389
|
trunk from a temporary worktree instead of HEAD, so
|
|
384
390
|
the checkout need not be clean. The label reaches
|
|
385
391
|
every compile and build as CHANT_RELEASE_LABEL.
|
|
392
|
+
A green commit of a pushed chant/compose/<name>
|
|
393
|
+
branch is cut too, recording its base and sources.
|
|
394
|
+
--channel <tag> records the tag a publish moves in
|
|
395
|
+
place of latest.
|
|
396
|
+
release compose <name> [--base <label|sha>] (--pr <n> | --work <id>)...
|
|
397
|
+
[--yes] [--json]
|
|
398
|
+
Cherry-pick the chosen trunk changes, in trunk order,
|
|
399
|
+
onto the base, each with a Chant-Composed-From
|
|
400
|
+
trailer, on chant/compose/<name>. Appends to a branch
|
|
401
|
+
that exists; stops on a conflict, naming the change
|
|
402
|
+
and files, and writes nothing. A dry run unless --yes,
|
|
403
|
+
which moves the branch and pushes it.
|
|
386
404
|
release candidates [--env <env>] [--json]
|
|
387
405
|
The green commits of trunk a release can be cut from,
|
|
388
406
|
newest first, each with the commits, work items, pull
|
|
@@ -405,6 +423,11 @@ Releases:
|
|
|
405
423
|
release verify <label|digest> [--json]
|
|
406
424
|
Check, offline, that the release was signed by a key
|
|
407
425
|
.chant/allowed_signers lists at the release's commit.
|
|
426
|
+
release workflow [--forge github|gitlab|forgejo] [--install <command>]
|
|
427
|
+
[--build <command>] [--chant <command>] [--output <file>]
|
|
428
|
+
Write the publish workflow, run on a release/** tag.
|
|
429
|
+
Under supply-chain it passes --require-provenance, and
|
|
430
|
+
under verified-releases it runs release verify first
|
|
408
431
|
release list [--json] Every release of this member with its targets, its
|
|
409
432
|
cuts and the environments that run it.
|
|
410
433
|
release export <label|digest> --env <env> [-o <dir>] [--json]
|
|
@@ -999,6 +1022,10 @@ Component release ledger + status:
|
|
|
999
1022
|
planned after the one before applied, a moved
|
|
1000
1023
|
wave waiting at <name>-wave-<k>, with --wave <k>
|
|
1001
1024
|
for one wave per CI job;
|
|
1025
|
+
--green: apply the green commit at HEAD, measured
|
|
1026
|
+
from the one last applied to --env, under the
|
|
1027
|
+
approved plans of the merges since, else at gate
|
|
1028
|
+
op green-<sha>;
|
|
1002
1029
|
--events-json, both commands: the run-event
|
|
1003
1030
|
stream on stdout; --report-url <s3 or file URL>,
|
|
1004
1031
|
both commands: copy the report, the change set
|
|
@@ -1022,7 +1049,11 @@ Component release ledger + status:
|
|
|
1022
1049
|
[--component <name> [--digest <sha256:...>]];
|
|
1023
1050
|
--from defaults to the environment before --to
|
|
1024
1051
|
in the config's environments; --dry-run prints
|
|
1025
|
-
the plan; the target's gates still apply
|
|
1052
|
+
the plan; the target's gates still apply. Under
|
|
1053
|
+
--release a dependent plans against its
|
|
1054
|
+
upstream's planned outputs and plans again once
|
|
1055
|
+
it applies; --wave-gate <gate> checks each wave
|
|
1056
|
+
at <gate>-wave-<k>; a stop writes --resume <file>)
|
|
1026
1057
|
components rollback <env> Redeploy an earlier release of an environment
|
|
1027
1058
|
from its recorded digest (--component <name>
|
|
1028
1059
|
[--digest <sha256:...>]; defaults to the release
|
|
@@ -1437,6 +1468,7 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1437
1468
|
{ name: "ci list", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiList(ctx) },
|
|
1438
1469
|
{ name: "ci tick", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiTick(ctx) },
|
|
1439
1470
|
{ name: "ci workflow", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiWorkflow(ctx) },
|
|
1471
|
+
{ name: "ci init", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-init")).runCiInit(ctx) },
|
|
1440
1472
|
// #4094: one pull-request job as a pipeline Op in a fresh checkout, the chant runner's first slice.
|
|
1441
1473
|
{ name: "ci run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run")).runCiRun(ctx) },
|
|
1442
1474
|
// #4098: the run index at the report URL.
|
|
@@ -1450,6 +1482,9 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1450
1482
|
{ name: "release publish", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleasePublish(ctx) },
|
|
1451
1483
|
// #4049: check a release's signature offline.
|
|
1452
1484
|
{ name: "release verify", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseVerify(ctx) },
|
|
1485
|
+
{ name: "release workflow", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/release-workflow")).runReleaseWorkflow(ctx) },
|
|
1486
|
+
// #4168 4.3: mode (b), chosen changes composed onto a base on chant/compose/<name>.
|
|
1487
|
+
{ name: "release compose", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCompose(ctx) },
|
|
1453
1488
|
{ name: "release candidates", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCandidates(ctx) },
|
|
1454
1489
|
{ name: "release list", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseList(ctx) },
|
|
1455
1490
|
{ name: "release export", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseExport(ctx) },
|
package/src/cli/parse-args.ts
CHANGED
|
@@ -385,6 +385,12 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
385
385
|
const pr = Number(raw);
|
|
386
386
|
if (!Number.isInteger(pr) || pr < 1) throw new Error(`--pr needs a pull or merge request number: got "${raw ?? ""}"`);
|
|
387
387
|
result.pr = pr;
|
|
388
|
+
// `chant release compose --pr <n>...` (#4168 4.3) reads every one.
|
|
389
|
+
(result.prs ??= []).push(pr);
|
|
390
|
+
} else if (arg === "--channel") {
|
|
391
|
+
// `chant release cut --channel <tag>` (#4168 4.3): the tag a publish moves in place of latest.
|
|
392
|
+
result.channel = args[++i];
|
|
393
|
+
if (!result.channel || !/^[A-Za-z][A-Za-z0-9._-]*$/.test(result.channel)) throw new Error(`--channel needs a channel name, such as next or hotfix: got "${result.channel ?? ""}"`);
|
|
388
394
|
} else if (arg === "--forge") {
|
|
389
395
|
result.forge = args[++i];
|
|
390
396
|
if (!result.forge || result.forge.startsWith("-")) throw new Error("--forge needs a forge: --forge github|gitlab|forgejo");
|
|
@@ -410,6 +416,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
410
416
|
result.requireProvenance = true;
|
|
411
417
|
} else if (arg === "--pr-loop") {
|
|
412
418
|
result.prLoop = true;
|
|
419
|
+
} else if (arg === "--green") {
|
|
420
|
+
// `chant components pr-apply --green` (#4168 step 9): apply the green commit at HEAD.
|
|
421
|
+
result.green = true;
|
|
413
422
|
} else if (arg === "--apply-on-merge") {
|
|
414
423
|
// `chant build --pr-loop --apply-on-merge` and `chant ci serve --apply-on-merge` (#4168): opt into applying on merge.
|
|
415
424
|
result.applyOnMerge = true;
|
|
@@ -421,6 +430,9 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
421
430
|
const line = args[++i];
|
|
422
431
|
if (line === undefined || line === "") throw new Error("--build-command needs a shell line: --build-command '<line>'");
|
|
423
432
|
result.ciBuildCommand = line;
|
|
433
|
+
} else if (arg === "--land") {
|
|
434
|
+
// `chant ci serve --land` (#4168 4.4): fast-forward the target to a chant/land/<name> branch once its pre-merge phases pass.
|
|
435
|
+
result.ciLand = true;
|
|
424
436
|
} else if (arg === "--pr-build-command") {
|
|
425
437
|
const line = args[++i];
|
|
426
438
|
if (line === undefined || line === "") throw new Error("--pr-build-command needs a shell line: --pr-build-command '<line>'");
|
|
@@ -540,6 +552,11 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
540
552
|
// `chant workspace wip --branch <branch>` (#3172)
|
|
541
553
|
result.branch = args[++i];
|
|
542
554
|
if (!result.branch || result.branch.startsWith("-")) throw new Error("--branch needs a branch name: --branch <branch>");
|
|
555
|
+
} else if (arg === "--security") {
|
|
556
|
+
// `chant ci init --security <group>`, repeatable (#4168 step 6): a security group to turn on.
|
|
557
|
+
const group = args[++i];
|
|
558
|
+
if (!group || group.startsWith("-")) throw new Error("--security needs a group: --security verified-releases|supply-chain|who-ran-it|review-rules|isolated-runners");
|
|
559
|
+
result.securityGroups = [...(result.securityGroups ?? []), group];
|
|
543
560
|
} else if (arg === "--workflow") {
|
|
544
561
|
// `chant ci workflow --workflow <name>`, repeatable (#3573)
|
|
545
562
|
const name = args[++i];
|
|
@@ -817,6 +834,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
817
834
|
const value = args[++i];
|
|
818
835
|
if (!value || value.startsWith("-")) throw new Error("--work needs a work item id: --work <id>");
|
|
819
836
|
result.work = value;
|
|
837
|
+
(result.works ??= []).push(value);
|
|
820
838
|
} else if (arg === "--holder" || arg === "--ttl" || arg === "--token" || arg === "--outcome") {
|
|
821
839
|
// `chant workspace work claim|renew|release <id>` (#2732)
|
|
822
840
|
const value = args[++i];
|
package/src/cli/registry.ts
CHANGED
|
@@ -302,6 +302,10 @@ export interface ParsedArgs {
|
|
|
302
302
|
* pr-apply finds it from the commit through `--forge` when omitted.
|
|
303
303
|
*/
|
|
304
304
|
pr?: number;
|
|
305
|
+
/** Every `--pr <n>`, in order: `chant release compose` (#4168 4.3) takes several. */
|
|
306
|
+
prs?: number[];
|
|
307
|
+
/** `chant release cut --channel <tag>` (#4168 4.3): the tag a publish of the release moves in place of `latest`. */
|
|
308
|
+
channel?: string;
|
|
305
309
|
/**
|
|
306
310
|
* `chant components pr-plan|pr-apply --forge github|gitlab|forgejo` (#3183)
|
|
307
311
|
* — post the note and the commit status on this forge, with the client
|
|
@@ -335,10 +339,18 @@ export interface ParsedArgs {
|
|
|
335
339
|
* #4168, in place of recording it. Opt-in: a merge deploys nothing without it.
|
|
336
340
|
*/
|
|
337
341
|
applyOnMerge?: boolean;
|
|
342
|
+
/**
|
|
343
|
+
* `chant components pr-apply --green` (#4168 step 9): apply the green commit
|
|
344
|
+
* at HEAD, measured from the one last applied to the environment, under the
|
|
345
|
+
* approved plans of the merges between or a gate of its own.
|
|
346
|
+
*/
|
|
347
|
+
green?: boolean;
|
|
338
348
|
/** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
|
|
339
349
|
prBuild?: boolean;
|
|
340
350
|
/** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
|
|
341
351
|
ciBuildCommand?: string;
|
|
352
|
+
/** `chant ci serve --land` (#4168 4.4) — fast-forward the target branch to each pushed `chant/land/<name>` branch whose `ci.green.preMerge` phases pass on it. */
|
|
353
|
+
ciLand?: boolean;
|
|
342
354
|
/** `chant build --pr-loop --pr-build-command <line>` (#4088) — the build line to run in place of `chant build --env <env>`. */
|
|
343
355
|
prBuildCommand?: string;
|
|
344
356
|
/**
|
|
@@ -467,8 +479,10 @@ export interface ParsedArgs {
|
|
|
467
479
|
label?: string;
|
|
468
480
|
/** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. */
|
|
469
481
|
branch?: string;
|
|
470
|
-
/** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. */
|
|
482
|
+
/** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. Also `chant ci init --workflow <name>` (#4168 step 6), the prescribed workflow to set up. */
|
|
471
483
|
ciWorkflows?: string[];
|
|
484
|
+
/** `chant ci init --security <group>`, repeatable (#4168 step 6): a security group to turn on. */
|
|
485
|
+
securityGroups?: string[];
|
|
472
486
|
/** `chant ci run plan|apply --commit <sha>` (#4094): the commit the job runs at. Default `HEAD`. */
|
|
473
487
|
commit?: string;
|
|
474
488
|
/** `chant ci run --pass-env <NAME>`, repeatable (#4094): a variable of the runner's environment the job gets, beside the few it always gets. */
|
|
@@ -699,6 +713,8 @@ export interface ParsedArgs {
|
|
|
699
713
|
holder?: string;
|
|
700
714
|
/** `chant run <op> --work <id>` (#2748): the work item an Op with a work lease runs under. `chant workspace check --changes --work <id>` (#2773): the work item in hand. */
|
|
701
715
|
work?: string;
|
|
716
|
+
/** Every `--work <id>`, in order: `chant release compose` (#4168 4.3) takes several. */
|
|
717
|
+
works?: string[];
|
|
702
718
|
/** `chant workspace work claim|renew <id> --ttl <seconds|duration>` (#2732): how long the lease lasts unless renewed. */
|
|
703
719
|
ttl?: string;
|
|
704
720
|
/** `chant workspace work renew|release <id> --token <token>` (#2732): the fencing token the caller holds. */
|