@tallyrelease/cli 0.1.1 → 0.2.0-beta.9
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/README.md +25 -1
- package/dist/affected-schema.d.ts +2 -0
- package/dist/affected-schema.js +2 -1
- package/dist/affected.schema.json +5 -1
- package/dist/ci/base.d.ts +5 -0
- package/dist/ci/base.js +21 -0
- package/dist/ci/outputs.d.ts +2 -0
- package/dist/ci/outputs.js +2 -0
- package/dist/ci/summary.d.ts +2 -0
- package/dist/ci/summary.js +10 -0
- package/dist/commands/affected.js +4 -20
- package/dist/commands/check.d.ts +4 -1
- package/dist/commands/check.js +9 -1
- package/dist/commands/explain.d.ts +5 -0
- package/dist/commands/explain.js +79 -0
- package/dist/commands/plan.d.ts +29 -0
- package/dist/commands/plan.js +161 -0
- package/dist/commands/shadow.d.ts +37 -0
- package/dist/commands/shadow.js +212 -0
- package/dist/config/schema.d.ts +84 -0
- package/dist/config/schema.js +87 -0
- package/dist/fs-ctx.d.ts +7 -0
- package/dist/fs-ctx.js +64 -0
- package/dist/git/reader.js +6 -1
- package/dist/main.js +52 -0
- package/dist/plan-schema.d.ts +127 -0
- package/dist/plan-schema.js +81 -0
- package/dist/plan.schema.json +515 -0
- package/dist/session.d.ts +15 -1
- package/dist/session.js +33 -16
- package/dist/shadow/compare.d.ts +50 -0
- package/dist/shadow/compare.js +112 -0
- package/dist/tallyrelease.schema.json +160 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -39,9 +39,13 @@ units:
|
|
|
39
39
|
|
|
40
40
|
Global flags: `--cwd <dir>` (repository root), `--config <file>` (default `tallyrelease.yaml`), `--token <token>` (forge token, default `GITHUB_TOKEN`), `--verbose`.
|
|
41
41
|
|
|
42
|
-
- `tallyrelease check` validates config, plugins, units and
|
|
42
|
+
- `tallyrelease check` validates config, plugins, units, graph and release config; with released units it also checks the repository's tags.
|
|
43
43
|
- `tallyrelease graph [--format text|json|mermaid] [--json]` prints units and edges.
|
|
44
44
|
- `tallyrelease affected [--base <ref>] [--format text|json|tsv] [--json] [--out <file>] [--github-output] [--tag <tag>]` computes the build set. `--out` writes the affected document as JSON, `--github-output` appends outputs to `$GITHUB_OUTPUT`. `--tag` (repeatable) keeps units with any of the given tags.
|
|
45
|
+
- `tallyrelease plan [--branch <name>] [--base <ref>] [--head <ref>] [--all] [--format text|json] [--json] [--out <file>] [--github-output] [--summary]` computes the release plan without changing anything: kind, base version, bump, next version and prerelease version per unit. `--head` plans another commit with the current config, `--all` also lists released units with a pending release outside the build set. `--github-output` writes `plan`, the path of the written plan (`--out`, else `$RUNNER_TEMP/tallyrelease-plan.json`). `--summary` appends a markdown summary to `$GITHUB_STEP_SUMMARY`, or prints it instead of the table when unset (exit 2 with `--json` then). The JSON schema is exported as `@tallyrelease/cli/plan.schema.json`.
|
|
46
|
+
- `tallyrelease explain <unit> [--branch <name>] [--base <ref>]` explains a unit: why it is affected, which commits and files bump it (including changes absorbed from unreleased dependencies), propagation and Release-As footers.
|
|
47
|
+
|
|
48
|
+
- `tallyrelease shadow [--manifest <path>] [--rp-config <path>] [--head <ref>] [--branch <name>] [--pin <spec>] [--out <file>] [--summary] [--strict]` compares tallyrelease with a release-please release: on a push that changes the release-please manifest, it plans the parent commit and compares each unit's next version with the new manifest value. See [Shadow mode](#shadow-mode).
|
|
45
49
|
|
|
46
50
|
Exit codes: `0` ok, `1` error, `2` usage. For scripts use `--format tsv` or `--json`; the text layout may change between versions.
|
|
47
51
|
|
|
@@ -92,6 +96,26 @@ jobs:
|
|
|
92
96
|
- run: echo "build ${{ matrix.unit.id }} from ${{ matrix.unit.path }}"
|
|
93
97
|
```
|
|
94
98
|
|
|
99
|
+
## Shadow mode
|
|
100
|
+
|
|
101
|
+
While release-please owns versions, `shadow` checks that tallyrelease would have released the same versions. Run it on pushes to the default branch:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
shadow:
|
|
105
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
106
|
+
runs-on: ubuntu-latest
|
|
107
|
+
permissions:
|
|
108
|
+
contents: read
|
|
109
|
+
steps:
|
|
110
|
+
- uses: actions/checkout@v6
|
|
111
|
+
with:
|
|
112
|
+
fetch-depth: 0
|
|
113
|
+
persist-credentials: false
|
|
114
|
+
- run: npx @tallyrelease/cli@<version> shadow --pin <version> --out shadow-report.json --summary
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Pushes that do not change `.release-please-manifest.json` are skipped (`W_SHADOW_SKIPPED`). Each row is `match`, `mismatch`, `tallyrelease-only` or `rp-only`; known differences are labelled (`release-config`, `dependent-patch`, `absorbed-internal`, `group-vs-rp`, `separate-pr`, `squash-split`). `--strict` exits 1 on unlabelled rows. Replay an earlier release merge locally with `shadow --head <sha> --branch main`.
|
|
118
|
+
|
|
95
119
|
## Editor support
|
|
96
120
|
|
|
97
121
|
`tallyrelease.yaml` has a JSON schema for completion and validation in editors using the YAML language server. Add one of these modelines as the first line.
|
|
@@ -29,6 +29,7 @@ export declare const affectedDocSchema: z.ZodObject<{
|
|
|
29
29
|
dependent: "dependent";
|
|
30
30
|
global: "global";
|
|
31
31
|
all: "all";
|
|
32
|
+
group: "group";
|
|
32
33
|
}>;
|
|
33
34
|
via: z.ZodArray<z.ZodString>;
|
|
34
35
|
released: z.ZodBoolean;
|
|
@@ -39,5 +40,6 @@ export declare const affectedDocSchema: z.ZodObject<{
|
|
|
39
40
|
code: z.ZodString;
|
|
40
41
|
message: z.ZodString;
|
|
41
42
|
plugin: z.ZodExactOptional<z.ZodString>;
|
|
43
|
+
unit: z.ZodExactOptional<z.ZodString>;
|
|
42
44
|
}, z.core.$strict>>;
|
|
43
45
|
}, z.core.$strict>;
|
package/dist/affected-schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
const reason = z.enum(["direct", "dependent", "global", "all"]);
|
|
2
|
+
const reason = z.enum(["direct", "dependent", "global", "all", "group"]);
|
|
3
3
|
/** Schema of `tallyrelease affected --json`, published as `affected.schema.json`. */
|
|
4
4
|
export const affectedDocSchema = z.strictObject({
|
|
5
5
|
schema: z.literal(1),
|
|
@@ -35,5 +35,6 @@ export const affectedDocSchema = z.strictObject({
|
|
|
35
35
|
code: z.string(),
|
|
36
36
|
message: z.string(),
|
|
37
37
|
plugin: z.string().exactOptional(),
|
|
38
|
+
unit: z.string().exactOptional(),
|
|
38
39
|
})),
|
|
39
40
|
});
|
|
@@ -77,7 +77,8 @@
|
|
|
77
77
|
"direct",
|
|
78
78
|
"dependent",
|
|
79
79
|
"global",
|
|
80
|
-
"all"
|
|
80
|
+
"all",
|
|
81
|
+
"group"
|
|
81
82
|
]
|
|
82
83
|
},
|
|
83
84
|
"via": {
|
|
@@ -127,6 +128,9 @@
|
|
|
127
128
|
},
|
|
128
129
|
"plugin": {
|
|
129
130
|
"type": "string"
|
|
131
|
+
},
|
|
132
|
+
"unit": {
|
|
133
|
+
"type": "string"
|
|
130
134
|
}
|
|
131
135
|
},
|
|
132
136
|
"required": [
|
package/dist/ci/base.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Env, GithubContext } from "./github-env.js";
|
|
2
2
|
import type { Forge } from "../forge/forge.js";
|
|
3
3
|
import type { RepoGit } from "../git/reader.js";
|
|
4
|
+
import type { Paint } from "../output/paint.js";
|
|
4
5
|
import type { BaseSource, Warning } from "@tallyrelease/core";
|
|
5
6
|
export interface BuildBase {
|
|
6
7
|
base: string | null;
|
|
@@ -22,3 +23,7 @@ export interface BuildBaseInput {
|
|
|
22
23
|
}
|
|
23
24
|
/** Resolves the build base from `--base`, the CI event, the forge, then the local default branch. */
|
|
24
25
|
export declare const resolveBuildBase: (input: BuildBaseInput) => Promise<BuildBase>;
|
|
26
|
+
/** Short description of where the build base came from. */
|
|
27
|
+
export declare const baseLabel: (base: BuildBase) => string;
|
|
28
|
+
/** `base` line of the `plan` and `explain` text. */
|
|
29
|
+
export declare const baseLine: (paint: Paint, base: BuildBase) => string;
|
package/dist/ci/base.js
CHANGED
|
@@ -167,3 +167,24 @@ export const resolveBuildBase = async (input) => {
|
|
|
167
167
|
return result(null, "none");
|
|
168
168
|
}
|
|
169
169
|
};
|
|
170
|
+
/** Short description of where the build base came from. */
|
|
171
|
+
export const baseLabel = (base) => {
|
|
172
|
+
switch (base.source) {
|
|
173
|
+
case "cli":
|
|
174
|
+
return `--base ${base.ref ?? ""}`;
|
|
175
|
+
case "push-before":
|
|
176
|
+
return "push before";
|
|
177
|
+
case "pr-merge-base":
|
|
178
|
+
return "merge base with the PR target";
|
|
179
|
+
case "merge-group":
|
|
180
|
+
return "merge group base";
|
|
181
|
+
case "forge-last-success":
|
|
182
|
+
return "last successful run";
|
|
183
|
+
case "local-merge-base":
|
|
184
|
+
return `merge base with ${base.ref ?? ""}`;
|
|
185
|
+
case "none":
|
|
186
|
+
return "everything is affected";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/** `base` line of the `plan` and `explain` text. */
|
|
190
|
+
export const baseLine = (paint, base) => `${paint("bold", "base")} ${paint("yellow", base.base?.slice(0, 7) ?? "none")} ${paint("dim", `(${baseLabel(base)})`)}`;
|
package/dist/ci/outputs.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { AffectedDoc } from "@tallyrelease/core";
|
|
2
2
|
/** `$GITHUB_OUTPUT` entries of `affected`; units are ordered by id. */
|
|
3
3
|
export declare const affectedGithubOutput: (doc: AffectedDoc) => string;
|
|
4
|
+
/** `$GITHUB_OUTPUT` entry of `plan`: the path of the written plan document. */
|
|
5
|
+
export declare const planGithubOutput: (path: string) => string;
|
package/dist/ci/outputs.js
CHANGED
|
@@ -11,3 +11,5 @@ export const affectedGithubOutput = (doc) => [
|
|
|
11
11
|
`base=${doc.build.base ?? ""}\n`,
|
|
12
12
|
`base-source=${doc.build.baseSource}\n`,
|
|
13
13
|
].join("");
|
|
14
|
+
/** `$GITHUB_OUTPUT` entry of `plan`: the path of the written plan document. */
|
|
15
|
+
export const planGithubOutput = (path) => `plan=${path}\n`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
/** Appends markdown to `$GITHUB_STEP_SUMMARY`; false when it is not set. */
|
|
3
|
+
export const appendStepSummary = (env, markdown) => {
|
|
4
|
+
const path = env["GITHUB_STEP_SUMMARY"];
|
|
5
|
+
if (!path) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
appendFileSync(path, markdown);
|
|
9
|
+
return true;
|
|
10
|
+
};
|
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
import { computeAffected, TallyError } from "@tallyrelease/core";
|
|
2
2
|
import { appendFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { resolveBuildBase } from "../ci/base.js";
|
|
4
|
+
import { baseLabel, resolveBuildBase } from "../ci/base.js";
|
|
5
5
|
import { affectedGithubOutput } from "../ci/outputs.js";
|
|
6
6
|
import { alignColumns } from "../output/columns.js";
|
|
7
7
|
import { count } from "../output/count.js";
|
|
8
8
|
import { painter } from "../output/paint.js";
|
|
9
|
+
import { branchName } from "../session.js";
|
|
9
10
|
import { version } from "../version.js";
|
|
10
11
|
const GROUPS = [
|
|
11
12
|
"all",
|
|
12
13
|
"global",
|
|
13
14
|
"direct",
|
|
14
15
|
"dependent",
|
|
16
|
+
"group",
|
|
15
17
|
];
|
|
16
|
-
const baseLabel = (base) => {
|
|
17
|
-
switch (base.source) {
|
|
18
|
-
case "cli":
|
|
19
|
-
return `--base ${base.ref ?? ""}`;
|
|
20
|
-
case "push-before":
|
|
21
|
-
return "push before";
|
|
22
|
-
case "pr-merge-base":
|
|
23
|
-
return "merge base with the PR target";
|
|
24
|
-
case "merge-group":
|
|
25
|
-
return "merge group base";
|
|
26
|
-
case "forge-last-success":
|
|
27
|
-
return "last successful run";
|
|
28
|
-
case "local-merge-base":
|
|
29
|
-
return `merge base with ${base.ref ?? ""}`;
|
|
30
|
-
case "none":
|
|
31
|
-
return "everything is affected";
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
18
|
const tsv = (doc) => [
|
|
35
19
|
"id\tpath\treason\tvia\treleased\ttags",
|
|
36
20
|
...doc.units.map((u) => [
|
|
@@ -62,7 +46,7 @@ export const affected = async (session, flags) => {
|
|
|
62
46
|
});
|
|
63
47
|
const computed = await computeAffected(workspace, git, {
|
|
64
48
|
tool: `tallyrelease@${version}`,
|
|
65
|
-
branch:
|
|
49
|
+
branch: await branchName(session),
|
|
66
50
|
head,
|
|
67
51
|
base: base.base,
|
|
68
52
|
baseSource: base.source,
|
package/dist/commands/check.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { Session } from "../session.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Validates config, plugins, units, graph and release config; with released
|
|
4
|
+
* units also the repository's tags.
|
|
5
|
+
*/
|
|
3
6
|
export declare const check: (session: Session) => Promise<void>;
|
package/dist/commands/check.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { validateRelease, validateTags } from "@tallyrelease/core";
|
|
1
2
|
import { count } from "../output/count.js";
|
|
2
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Validates config, plugins, units, graph and release config; with released
|
|
5
|
+
* units also the repository's tags.
|
|
6
|
+
*/
|
|
3
7
|
export const check = async (session) => {
|
|
4
8
|
const workspace = await session.workspace();
|
|
5
9
|
session.warn(workspace.warnings);
|
|
10
|
+
validateRelease(workspace);
|
|
6
11
|
const released = workspace.units
|
|
7
12
|
.filter((u) => u.version !== "none")
|
|
8
13
|
.map((u) => u.id);
|
|
14
|
+
if (released.length > 0) {
|
|
15
|
+
session.warn(validateTags(workspace, await session.git.tags()));
|
|
16
|
+
}
|
|
9
17
|
const list = released.length > 0 ? `: ${released.join(", ")}` : "";
|
|
10
18
|
session.stdout.write(`ok: ${count(workspace.units.length, "unit")} (${String(released.length)} released${list}), ${count(workspace.graph.edges.length, "edge")}\n`);
|
|
11
19
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PlanFlags } from "./plan.js";
|
|
2
|
+
import type { Session } from "../session.js";
|
|
3
|
+
export type ExplainFlags = Pick<PlanFlags, "branch" | "base">;
|
|
4
|
+
/** Explains why a unit is affected and how its version is computed. */
|
|
5
|
+
export declare const explain: (session: Session, id: string, flags: ExplainFlags) => Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { explainUnit } from "@tallyrelease/core";
|
|
2
|
+
import { planOptions } from "./plan.js";
|
|
3
|
+
import { baseLine } from "../ci/base.js";
|
|
4
|
+
import { alignColumns } from "../output/columns.js";
|
|
5
|
+
import { painter } from "../output/paint.js";
|
|
6
|
+
const short = (sha) => sha?.slice(0, 7) ?? "-";
|
|
7
|
+
const releaseLine = (u, released, { group, nextSource }) => {
|
|
8
|
+
if (!released) {
|
|
9
|
+
return "not released";
|
|
10
|
+
}
|
|
11
|
+
if (u === null || u.next === null) {
|
|
12
|
+
return "nothing pending";
|
|
13
|
+
}
|
|
14
|
+
const why = group !== null && nextSource !== "bump"
|
|
15
|
+
? `group ${group.name}, ${nextSource ?? ""}`
|
|
16
|
+
: group === null && u.releaseAs !== null
|
|
17
|
+
? "release-as"
|
|
18
|
+
: (u.bump ?? "");
|
|
19
|
+
return `${u.from ?? "first release"} -> ${u.next} (${why})${u.version === null ? "" : `, version ${u.version}`}`;
|
|
20
|
+
};
|
|
21
|
+
/** Explains why a unit is affected and how its version is computed. */
|
|
22
|
+
export const explain = async (session, id, flags) => {
|
|
23
|
+
const { opts, ref, base, warnings } = await planOptions(session, flags);
|
|
24
|
+
const { explanation, unit } = await session.workspaceAt(ref, async (workspace, fs) => ({
|
|
25
|
+
explanation: await explainUnit(workspace, session.git, {
|
|
26
|
+
...opts,
|
|
27
|
+
fs,
|
|
28
|
+
unit: id,
|
|
29
|
+
}),
|
|
30
|
+
unit: workspace.units.find((u) => u.id === id),
|
|
31
|
+
}));
|
|
32
|
+
session.warn([...explanation.warnings, ...warnings]);
|
|
33
|
+
const paint = painter(session.stdout);
|
|
34
|
+
const plan = explanation.unit;
|
|
35
|
+
const affected = plan?.affected;
|
|
36
|
+
const { group } = explanation;
|
|
37
|
+
const lines = [
|
|
38
|
+
`${paint("bold", id)} ${paint("dim", `${unit?.root ?? ""} ${unit?.plugin ?? ""} ${unit?.scheme ?? ""}`)}`,
|
|
39
|
+
baseLine(paint, base),
|
|
40
|
+
"",
|
|
41
|
+
`${paint("bold", "affected")} ${affected
|
|
42
|
+
? `${affected.reason === "group" ? `pulled by group ${group?.name ?? ""}` : affected.reason}${affected.via.length > 0 ? ` via ${affected.via.join(", ")}` : ""}`
|
|
43
|
+
: "no"}`,
|
|
44
|
+
...explanation.files.map((f) => ` ${f}`),
|
|
45
|
+
...(group
|
|
46
|
+
? [
|
|
47
|
+
`${paint("bold", "group")} ${group.name} ${paint("dim", `(${group.members.join(", ")})`)}`,
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
50
|
+
`${paint("bold", "release")} ${releaseLine(explanation.unit, unit?.version !== "none", explanation)}`,
|
|
51
|
+
];
|
|
52
|
+
if (plan?.baseTag) {
|
|
53
|
+
lines.push(` base tag ${plan.baseTag}${plan.lineBaseTag !== null && plan.lineBaseTag !== plan.baseTag ? `, line base tag ${plan.lineBaseTag}` : ""}`);
|
|
54
|
+
}
|
|
55
|
+
if (explanation.changes.length > 0) {
|
|
56
|
+
lines.push("", paint("bold", `changes (${String(explanation.changes.length)})`));
|
|
57
|
+
const rows = alignColumns(explanation.changes.map((c) => [
|
|
58
|
+
short(c.sha),
|
|
59
|
+
`${c.type || "-"}${c.breaking ? "!" : ""}`,
|
|
60
|
+
c.subject,
|
|
61
|
+
c.via === undefined ? "" : `via ${c.via}`,
|
|
62
|
+
c.releaseAs === undefined ? "" : `release-as ${c.releaseAs}`,
|
|
63
|
+
]), { indent: 2, styles: ["yellow", undefined, undefined, "dim", "cyan"] }, paint);
|
|
64
|
+
explanation.changes.forEach((c, i) => {
|
|
65
|
+
lines.push(rows[i] ?? "", ...c.files.map((f) => ` ${paint("dim", f)}`));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (explanation.propagatedFrom.length > 0) {
|
|
69
|
+
lines.push("", paint("bold", "propagated from"), ...explanation.propagatedFrom.map((p) => ` ${p.id} ${paint("dim", p.pinned === null ? "(through unreleased units)" : p.pinned ? "(pinned)" : "(unpinned)")}`));
|
|
70
|
+
}
|
|
71
|
+
if (explanation.releaseAs.length > 0) {
|
|
72
|
+
lines.push("", paint("bold", "release-as footers"), ...alignColumns(explanation.releaseAs.map((r) => [
|
|
73
|
+
short(r.sha),
|
|
74
|
+
r.qualified ? `${id}@${r.version}` : r.version,
|
|
75
|
+
r.applied ? "applied" : "ignored",
|
|
76
|
+
]), { indent: 2, styles: ["yellow", undefined, "dim"] }, paint));
|
|
77
|
+
}
|
|
78
|
+
session.stdout.write(`${lines.join("\n")}\n`);
|
|
79
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BuildBase } from "../ci/base.js";
|
|
2
|
+
import type { Session } from "../session.js";
|
|
3
|
+
import type { PlanOptions, Warning } from "@tallyrelease/core";
|
|
4
|
+
export interface PlanFlags {
|
|
5
|
+
branch?: string;
|
|
6
|
+
base?: string;
|
|
7
|
+
head?: string;
|
|
8
|
+
all?: boolean;
|
|
9
|
+
format?: "text" | "json";
|
|
10
|
+
json?: boolean;
|
|
11
|
+
out?: string;
|
|
12
|
+
githubOutput?: boolean;
|
|
13
|
+
summary?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface PlanSetup {
|
|
16
|
+
opts: Omit<PlanOptions, "fs">;
|
|
17
|
+
/** `--head` sha, null to plan the working tree at HEAD. */
|
|
18
|
+
ref: string | null;
|
|
19
|
+
base: BuildBase;
|
|
20
|
+
/** Base warnings and `W_HEAD_BRANCH`. */
|
|
21
|
+
warnings: Warning[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Plan options for `--branch`, `--base` and `--head`. With `--head` only
|
|
25
|
+
* `--base` and the local merge base apply, and files come from the commit.
|
|
26
|
+
*/
|
|
27
|
+
export declare const planOptions: (session: Session, flags: Pick<PlanFlags, "branch" | "base" | "head">) => Promise<PlanSetup>;
|
|
28
|
+
/** Computes the release plan and writes the requested outputs. */
|
|
29
|
+
export declare const plan: (session: Session, flags: PlanFlags) => Promise<void>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { TallyError } from "@tallyrelease/core";
|
|
2
|
+
import { appendFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { baseLabel, baseLine, resolveBuildBase } from "../ci/base.js";
|
|
5
|
+
import { planGithubOutput } from "../ci/outputs.js";
|
|
6
|
+
import { appendStepSummary } from "../ci/summary.js";
|
|
7
|
+
import { alignColumns } from "../output/columns.js";
|
|
8
|
+
import { count } from "../output/count.js";
|
|
9
|
+
import { painter } from "../output/paint.js";
|
|
10
|
+
import { branchName } from "../session.js";
|
|
11
|
+
import { version } from "../version.js";
|
|
12
|
+
const lineLabel = (line, branch) => line === null
|
|
13
|
+
? `${branch} (no branch rule)`
|
|
14
|
+
: `${branch} (rule ${line.rule}, ${line.kind} line${line.channel === null ? "" : `, channel ${line.channel}`}${line.maxBump === null ? "" : `, max bump ${line.maxBump}`}${line.absolute === null ? "" : `, base ${String(line.absolute.major)}.${line.absolute.minor === undefined ? "x" : String(line.absolute.minor)} of ${line.absolute.unit ?? line.absolute.group ?? ""}`})`;
|
|
15
|
+
const text = (session, doc, base) => {
|
|
16
|
+
const paint = painter(session.stdout);
|
|
17
|
+
const lines = [
|
|
18
|
+
`${paint("bold", "branch")} ${lineLabel(doc.line, doc.branch)}`,
|
|
19
|
+
baseLine(paint, base),
|
|
20
|
+
"",
|
|
21
|
+
];
|
|
22
|
+
if (doc.units.length === 0) {
|
|
23
|
+
lines.push("no units planned");
|
|
24
|
+
return `${lines.join("\n")}\n`;
|
|
25
|
+
}
|
|
26
|
+
const grouped = doc.units.some((u) => u.group !== null);
|
|
27
|
+
const table = [
|
|
28
|
+
[
|
|
29
|
+
"UNIT",
|
|
30
|
+
...(grouped ? ["GROUP"] : []),
|
|
31
|
+
"KIND",
|
|
32
|
+
"FROM",
|
|
33
|
+
"BUMP",
|
|
34
|
+
"NEXT",
|
|
35
|
+
"VERSION",
|
|
36
|
+
],
|
|
37
|
+
...doc.units.map((u) => [
|
|
38
|
+
u.id,
|
|
39
|
+
...(grouped ? [u.group ?? "-"] : []),
|
|
40
|
+
u.kind,
|
|
41
|
+
u.from ?? "-",
|
|
42
|
+
u.bump ?? "-",
|
|
43
|
+
u.next ?? "-",
|
|
44
|
+
u.version ?? "-",
|
|
45
|
+
]),
|
|
46
|
+
];
|
|
47
|
+
// Header is aligned unstyled so column styles do not apply to it.
|
|
48
|
+
const [header = ""] = alignColumns(table);
|
|
49
|
+
const [, ...rows] = alignColumns(table, { styles: grouped ? [undefined, "dim", "cyan"] : [undefined, "cyan"] }, paint);
|
|
50
|
+
const kinds = ["prerelease", "rebuild", "none"]
|
|
51
|
+
.map((kind) => [kind, doc.units.filter((u) => u.kind === kind).length])
|
|
52
|
+
.filter(([, n]) => n !== 0)
|
|
53
|
+
.map(([kind, n]) => `${String(n)} ${String(kind)}`);
|
|
54
|
+
lines.push(paint("bold", header), ...rows, "", `${count(doc.units.length, "unit")} planned: ${kinds.join(", ")}`);
|
|
55
|
+
return `${lines.join("\n")}\n`;
|
|
56
|
+
};
|
|
57
|
+
const markdown = (doc, base) => {
|
|
58
|
+
const lines = [
|
|
59
|
+
"### tallyrelease plan",
|
|
60
|
+
"",
|
|
61
|
+
`Branch ${lineLabel(doc.line, doc.branch)}.`,
|
|
62
|
+
"",
|
|
63
|
+
`Base ${base.base?.slice(0, 7) ?? "none"} (${baseLabel(base)}).`,
|
|
64
|
+
"",
|
|
65
|
+
];
|
|
66
|
+
if (doc.units.length === 0) {
|
|
67
|
+
lines.push("No units planned.");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const grouped = doc.units.some((u) => u.group !== null);
|
|
71
|
+
lines.push(`| Unit |${grouped ? " Group |" : ""} Kind | From | Bump | Next | Version |`, `| :-- |${grouped ? " :-- |" : ""} :-- | :-- | :-- | :-- | :-- |`, ...doc.units.map((u) => `| ${u.id} |${grouped ? ` ${u.group ?? "-"} |` : ""} ${u.kind} | ${u.from ?? "-"} | ${u.bump ?? "-"} | ${u.next ?? "-"} | ${u.version ?? "-"} |`));
|
|
72
|
+
}
|
|
73
|
+
if (doc.warnings.length > 0) {
|
|
74
|
+
lines.push("", "Warnings:", "", ...doc.warnings.map((w) => `- ${w.code}: ${w.message}`));
|
|
75
|
+
}
|
|
76
|
+
return `${lines.join("\n")}\n`;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Plan options for `--branch`, `--base` and `--head`. With `--head` only
|
|
80
|
+
* `--base` and the local merge base apply, and files come from the commit.
|
|
81
|
+
*/
|
|
82
|
+
export const planOptions = async (session, flags) => {
|
|
83
|
+
const { git } = session;
|
|
84
|
+
const current = await git.resolve("HEAD");
|
|
85
|
+
if (current === undefined) {
|
|
86
|
+
// Surfaces E_NOT_A_REPO and E_NOT_REPO_ROOT first.
|
|
87
|
+
await session.workspace();
|
|
88
|
+
throw new TallyError("E_NO_HEAD", "HEAD does not resolve to a commit; the repository has no commits");
|
|
89
|
+
}
|
|
90
|
+
const head = flags.head === undefined ? current : await git.resolve(flags.head);
|
|
91
|
+
if (head === undefined) {
|
|
92
|
+
throw new TallyError("E_HEAD_UNRESOLVED", `--head "${flags.head ?? ""}" does not resolve to a commit`, { ref: flags.head });
|
|
93
|
+
}
|
|
94
|
+
const replay = flags.head !== undefined;
|
|
95
|
+
const base = await resolveBuildBase({
|
|
96
|
+
git,
|
|
97
|
+
head,
|
|
98
|
+
env: replay ? {} : session.env,
|
|
99
|
+
cliBase: flags.base,
|
|
100
|
+
github: replay ? undefined : session.github,
|
|
101
|
+
forge: replay ? undefined : session.forge,
|
|
102
|
+
});
|
|
103
|
+
const branch = flags.branch ?? (await branchName(session));
|
|
104
|
+
const headBranch = replay && flags.branch === undefined
|
|
105
|
+
? [
|
|
106
|
+
{
|
|
107
|
+
code: "W_HEAD_BRANCH",
|
|
108
|
+
message: `planning --head under the branch rule for "${branch}"; pass --branch to choose`,
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
: [];
|
|
112
|
+
return {
|
|
113
|
+
opts: {
|
|
114
|
+
tool: `tallyrelease@${version}`,
|
|
115
|
+
branch,
|
|
116
|
+
head,
|
|
117
|
+
base: base.base,
|
|
118
|
+
baseSource: base.source,
|
|
119
|
+
},
|
|
120
|
+
ref: replay ? head : null,
|
|
121
|
+
base,
|
|
122
|
+
warnings: [...base.warnings, ...headBranch],
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
/** Computes the release plan and writes the requested outputs. */
|
|
126
|
+
export const plan = async (session, flags) => {
|
|
127
|
+
const { opts, ref, base, warnings } = await planOptions(session, flags);
|
|
128
|
+
const planned = await session.planAt(ref, {
|
|
129
|
+
...opts,
|
|
130
|
+
all: flags.all ?? false,
|
|
131
|
+
});
|
|
132
|
+
const doc = { ...planned, warnings: [...planned.warnings, ...warnings] };
|
|
133
|
+
session.warn(doc.warnings);
|
|
134
|
+
const json = `${JSON.stringify(doc, null, 2)}\n`;
|
|
135
|
+
const out = flags.out === undefined ? undefined : resolve(session.cwd, flags.out);
|
|
136
|
+
if (out !== undefined) {
|
|
137
|
+
writeFileSync(out, json);
|
|
138
|
+
}
|
|
139
|
+
if (flags.githubOutput) {
|
|
140
|
+
const outputPath = session.env["GITHUB_OUTPUT"];
|
|
141
|
+
if (!outputPath) {
|
|
142
|
+
throw new TallyError("E_GITHUB_OUTPUT", "--github-output requires GITHUB_OUTPUT to be set");
|
|
143
|
+
}
|
|
144
|
+
const temp = session.env["RUNNER_TEMP"];
|
|
145
|
+
if (out === undefined && !temp) {
|
|
146
|
+
throw new TallyError("E_GITHUB_OUTPUT", "--github-output requires --out or RUNNER_TEMP to be set");
|
|
147
|
+
}
|
|
148
|
+
const path = out ?? join(temp ?? "", "tallyrelease-plan.json");
|
|
149
|
+
if (out === undefined) {
|
|
150
|
+
writeFileSync(path, json);
|
|
151
|
+
}
|
|
152
|
+
appendFileSync(outputPath, planGithubOutput(path));
|
|
153
|
+
}
|
|
154
|
+
const appended = flags.summary && appendStepSummary(session.env, markdown(doc, base));
|
|
155
|
+
const format = flags.format ?? (flags.json ? "json" : "text");
|
|
156
|
+
session.stdout.write(format === "json"
|
|
157
|
+
? json
|
|
158
|
+
: flags.summary && !appended
|
|
159
|
+
? markdown(doc, base)
|
|
160
|
+
: text(session, doc, base));
|
|
161
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Session } from "../session.js";
|
|
2
|
+
import type { ShadowRow } from "../shadow/compare.js";
|
|
3
|
+
import type { Warning } from "@tallyrelease/core";
|
|
4
|
+
export interface ShadowFlags {
|
|
5
|
+
manifest?: string;
|
|
6
|
+
rpConfig?: string;
|
|
7
|
+
head?: string;
|
|
8
|
+
branch?: string;
|
|
9
|
+
pin?: string;
|
|
10
|
+
out?: string;
|
|
11
|
+
summary?: boolean;
|
|
12
|
+
strict?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** `tallyrelease shadow --out`, schema 1. */
|
|
15
|
+
export interface ShadowReport {
|
|
16
|
+
schema: 1;
|
|
17
|
+
tool: string;
|
|
18
|
+
/** `--pin`. */
|
|
19
|
+
pin: string | null;
|
|
20
|
+
/** `GITHUB_SHA`. */
|
|
21
|
+
sha: string | null;
|
|
22
|
+
/** True with `--head`, or when `head` is not `GITHUB_SHA`. */
|
|
23
|
+
replay: boolean;
|
|
24
|
+
branch: string;
|
|
25
|
+
head: string;
|
|
26
|
+
/** `HEAD^1`, the planned commit; null for a root commit. */
|
|
27
|
+
planned: string | null;
|
|
28
|
+
manifest: string;
|
|
29
|
+
skipped: {
|
|
30
|
+
reason: "manifest" | "cap";
|
|
31
|
+
message: string;
|
|
32
|
+
} | null;
|
|
33
|
+
rows: ShadowRow[];
|
|
34
|
+
warnings: Warning[];
|
|
35
|
+
}
|
|
36
|
+
/** Compares the plan before a release-please merge with its manifest change. */
|
|
37
|
+
export declare const shadow: (session: Session, flags: ShadowFlags) => Promise<void>;
|