@intentius/chant 0.124.34 → 0.124.36
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 +23 -8
- 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 +19 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +17 -0
- package/dist/cli/registry.d.ts +14 -0
- 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.d.ts +4 -1
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +6 -3
- 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 +3 -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 +5 -1
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +7 -2
- 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 +5 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
- package/dist/lifecycle/__fixtures__/publish-fakes.js +20 -2
- 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 +6 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
- package/dist/lifecycle/publish-declaration.js +21 -0
- package/dist/lifecycle/publish-npm.d.ts +11 -4
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +104 -14
- 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 +6 -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 +225 -223
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/checks/ci-workflow.js +8 -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-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 +4 -4
- package/dist/workspace/ci-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-workflow.js +2 -2
- 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/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/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/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 +22 -8
- package/src/cli/handlers/release-compose.e2e.test.ts +133 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +4 -1
- package/src/cli/handlers/release.ts +112 -10
- package/src/cli/handlers/run.ts +12 -1
- package/src/cli/main.ts +19 -1
- package/src/cli/parse-args.ts +13 -0
- package/src/cli/registry.ts +14 -0
- 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.test.ts +7 -0
- package/src/components/promote.ts +9 -4
- 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.test.ts +18 -0
- package/src/graph-declared.ts +8 -1
- 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 +20 -2
- 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 +21 -0
- package/src/lifecycle/publish-npm.ts +100 -14
- package/src/lifecycle/publish-targets.test.ts +108 -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 +8 -1
- package/src/lifecycle/release-store.ts +16 -1
- package/src/workspace/checks/ci-workflow.test.ts +12 -2
- package/src/workspace/checks/ci-workflow.ts +7 -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-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 +4 -4
- package/src/workspace/delivery-metrics.ts +12 -3
- package/src/workspace/graph-contract.test.ts +3 -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/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/releases.schema.json +30 -0
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* ran from it, through the release gate `promote --release` binds (#4045).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { ENV_VAR } from "../../env";
|
|
24
25
|
import { dirname, join, resolve } from "node:path";
|
|
25
26
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
26
27
|
import { loadChantConfig, type ChantConfig } from "../../config";
|
|
@@ -116,9 +117,16 @@ function resolveActor(ctx: CommandContext): string | undefined {
|
|
|
116
117
|
return ctx.args.actor ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Config, params and the declared components, or the exit code of a refusal
|
|
122
|
+
* already printed. `env` is the environment being deployed: it is set as
|
|
123
|
+
* `CHANT_ENV` before the components load, as `--env` sets it for `chant run`
|
|
124
|
+
* (../main.ts), so a component that reads it (a stack name per environment)
|
|
125
|
+
* deploys to that environment and not to the one it was promoted from (#4222).
|
|
126
|
+
*/
|
|
127
|
+
async function loadProject(ctx: CommandContext, selector: string, env: string) {
|
|
121
128
|
const { args } = ctx;
|
|
129
|
+
process.env[ENV_VAR] = env;
|
|
122
130
|
const projectPath = resolve(".");
|
|
123
131
|
const { config } = await loadChantConfig(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
124
132
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
@@ -397,17 +405,22 @@ async function deployPlan(
|
|
|
397
405
|
const prepared: DriverComponent[] = [];
|
|
398
406
|
const removed = new Map<string, string[]>();
|
|
399
407
|
const refusals: string[] = [];
|
|
408
|
+
// #4222: a component with no artifact, deployed from the templates the release records, and its record's digest.
|
|
409
|
+
const templateDigests = new Map<string, string>();
|
|
410
|
+
const { releaseTemplatesDigest } = await import("../../components/release-promote");
|
|
400
411
|
for (const item of plan.items) {
|
|
412
|
+
const templates = under ? releaseTemplatesDigest(under.target, item.component) : null;
|
|
401
413
|
// A rollback and a redeploy deploy a release this environment already
|
|
402
414
|
// received, so neither runs the publish step.
|
|
403
415
|
const result = withoutBuildSteps(
|
|
404
416
|
byName.get(item.component)!,
|
|
405
|
-
verb === "promote" ? {} : { pinDigest: item.digest, verb: `a ${verb}` },
|
|
417
|
+
{ ...(verb === "promote" ? {} : { pinDigest: item.digest, verb: `a ${verb}` }), templatesPinned: templates !== null },
|
|
406
418
|
);
|
|
407
419
|
if ("error" in result) refusals.push(result.error);
|
|
408
420
|
else {
|
|
409
421
|
prepared.push(result.component);
|
|
410
422
|
removed.set(item.component, result.removed);
|
|
423
|
+
if (result.templateOnly && templates !== null) templateDigests.set(item.component, templates);
|
|
411
424
|
}
|
|
412
425
|
}
|
|
413
426
|
if (refusals.length > 0) {
|
|
@@ -647,7 +660,8 @@ async function deployPlan(
|
|
|
647
660
|
const approver = gateApprover(result);
|
|
648
661
|
const admission = gateAdmission(result);
|
|
649
662
|
const input = record(item, plan.to, { runId, ...(runOrigin ? { runOrigin } : {}), actor: actor!, timestamp, ...(approver ? { approver } : {}), ...(admission ? { admission } : {}) });
|
|
650
|
-
const
|
|
663
|
+
const templates = templateDigests.get(item.component);
|
|
664
|
+
const { record: written } = await appendReleaseRecord(under ? { ...input, ...(templates ? { digest: templates } : {}), releaseDigest: under.release.releaseDigest } : input);
|
|
651
665
|
recorded.push(written);
|
|
652
666
|
}
|
|
653
667
|
if (outcome) await appendDeployOutcome(outcome);
|
|
@@ -872,7 +886,7 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
|
|
|
872
886
|
const who = actorOrRefuse(ctx);
|
|
873
887
|
if (typeof who === "number") return who;
|
|
874
888
|
|
|
875
|
-
const project = await loadProject(ctx, args.component ?? "all");
|
|
889
|
+
const project = await loadProject(ctx, args.component ?? "all", to);
|
|
876
890
|
if (typeof project === "number") return project;
|
|
877
891
|
|
|
878
892
|
// The member's `environments` array is its promotion order (#4030). It
|
|
@@ -958,7 +972,7 @@ export async function runComponentsRollback(ctx: CommandContext): Promise<number
|
|
|
958
972
|
if (typeof who === "number") return who;
|
|
959
973
|
if (args.release) return runReleaseRollback(ctx, env, args.release, who.actor);
|
|
960
974
|
|
|
961
|
-
const project = await loadProject(ctx, args.component
|
|
975
|
+
const project = await loadProject(ctx, args.component!, env);
|
|
962
976
|
if (typeof project === "number") return project;
|
|
963
977
|
|
|
964
978
|
await fetchLifecycle().catch(() => false);
|
|
@@ -988,7 +1002,7 @@ export async function runComponentsRollback(ctx: CommandContext): Promise<number
|
|
|
988
1002
|
* runs now, as `promote --release` binds them (#4045).
|
|
989
1003
|
*/
|
|
990
1004
|
async function runReleaseRollback(ctx: CommandContext, env: string, ref: string, actor: string | undefined): Promise<number> {
|
|
991
|
-
const project = await loadProject(ctx, "all");
|
|
1005
|
+
const project = await loadProject(ctx, "all", env);
|
|
992
1006
|
if (typeof project === "number") return project;
|
|
993
1007
|
await fetchLifecycle().catch(() => false);
|
|
994
1008
|
const records = await readLedger(env);
|
|
@@ -1025,7 +1039,7 @@ export async function runComponentsRedeploy(ctx: CommandContext): Promise<number
|
|
|
1025
1039
|
const who = actorOrRefuse(ctx);
|
|
1026
1040
|
if (typeof who === "number") return who;
|
|
1027
1041
|
|
|
1028
|
-
const project = await loadProject(ctx, args.component);
|
|
1042
|
+
const project = await loadProject(ctx, args.component, env);
|
|
1029
1043
|
if (typeof project === "number") return project;
|
|
1030
1044
|
|
|
1031
1045
|
await fetchLifecycle().catch(() => false);
|
|
@@ -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
|
+
});
|
|
@@ -160,6 +160,9 @@ describe("chant release publish with targets", () => {
|
|
|
160
160
|
["publish", join(dir, "packages/cli"), "--tag", "next", "--access", "public", "--provenance"],
|
|
161
161
|
["dist-tag", "add", "@acme/core@1.0.0", "latest"],
|
|
162
162
|
["dist-tag", "add", "@acme/cli@1.0.0", "latest"],
|
|
163
|
+
// #4244: the reveal waits until the registry serves each version it published.
|
|
164
|
+
["view", "@acme/core@1.0.0", "version"],
|
|
165
|
+
["view", "@acme/cli@1.0.0", "version"],
|
|
163
166
|
// #4049: in CI, what was published is installed in an empty directory and its signatures and attestations audited.
|
|
164
167
|
["install", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
165
168
|
["audit", "signatures"],
|
|
@@ -212,7 +215,7 @@ describe("chant release publish with targets", () => {
|
|
|
212
215
|
const moved = await chant(dir, ["release", "publish", "v1", "--yes"], { ...path, ...secrets, ...oidc });
|
|
213
216
|
expect(moved.code).toBe(1);
|
|
214
217
|
expect(moved.stderr).toMatch(/\[release-head-mismatch\]/);
|
|
215
|
-
expect(calls()).toHaveLength(
|
|
218
|
+
expect(calls()).toHaveLength(10);
|
|
216
219
|
},
|
|
217
220
|
240_000,
|
|
218
221
|
);
|
|
@@ -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
|
@@ -368,7 +368,7 @@ CI:
|
|
|
368
368
|
Releases:
|
|
369
369
|
release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param <k=v>]
|
|
370
370
|
[--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>]
|
|
371
|
-
[--actor <name>] [--sign [<key file>]] [--json]
|
|
371
|
+
[--actor <name>] [--sign [<key file>]] [--channel <tag>] [--json]
|
|
372
372
|
A dry run of a cut of this member from HEAD, which
|
|
373
373
|
must be clean, on trunk and pushed, and carry a
|
|
374
374
|
ci/green tag: compile every environment in the
|
|
@@ -389,6 +389,18 @@ Releases:
|
|
|
389
389
|
trunk from a temporary worktree instead of HEAD, so
|
|
390
390
|
the checkout need not be clean. The label reaches
|
|
391
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.
|
|
392
404
|
release candidates [--env <env>] [--json]
|
|
393
405
|
The green commits of trunk a release can be cut from,
|
|
394
406
|
newest first, each with the commits, work items, pull
|
|
@@ -1010,6 +1022,10 @@ Component release ledger + status:
|
|
|
1010
1022
|
planned after the one before applied, a moved
|
|
1011
1023
|
wave waiting at <name>-wave-<k>, with --wave <k>
|
|
1012
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>;
|
|
1013
1029
|
--events-json, both commands: the run-event
|
|
1014
1030
|
stream on stdout; --report-url <s3 or file URL>,
|
|
1015
1031
|
both commands: copy the report, the change set
|
|
@@ -1467,6 +1483,8 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1467
1483
|
// #4049: check a release's signature offline.
|
|
1468
1484
|
{ name: "release verify", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseVerify(ctx) },
|
|
1469
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) },
|
|
1470
1488
|
{ name: "release candidates", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCandidates(ctx) },
|
|
1471
1489
|
{ name: "release list", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseList(ctx) },
|
|
1472
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>'");
|
|
@@ -822,6 +834,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
822
834
|
const value = args[++i];
|
|
823
835
|
if (!value || value.startsWith("-")) throw new Error("--work needs a work item id: --work <id>");
|
|
824
836
|
result.work = value;
|
|
837
|
+
(result.works ??= []).push(value);
|
|
825
838
|
} else if (arg === "--holder" || arg === "--ttl" || arg === "--token" || arg === "--outcome") {
|
|
826
839
|
// `chant workspace work claim|renew|release <id>` (#2732)
|
|
827
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
|
/**
|
|
@@ -701,6 +713,8 @@ export interface ParsedArgs {
|
|
|
701
713
|
holder?: string;
|
|
702
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. */
|
|
703
715
|
work?: string;
|
|
716
|
+
/** Every `--work <id>`, in order: `chant release compose` (#4168 4.3) takes several. */
|
|
717
|
+
works?: string[];
|
|
704
718
|
/** `chant workspace work claim|renew <id> --ttl <seconds|duration>` (#2732): how long the lease lasts unless renewed. */
|
|
705
719
|
ttl?: string;
|
|
706
720
|
/** `chant workspace work renew|release <id> --token <token>` (#2732): the fencing token the caller holds. */
|