@tallyrelease/cli 0.0.0-stage → 0.1.0-beta.1

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.
Files changed (47) hide show
  1. package/dist/affected-schema.d.ts +40 -0
  2. package/dist/affected-schema.js +36 -0
  3. package/dist/affected.schema.json +140 -0
  4. package/dist/bin.d.ts +2 -0
  5. package/dist/bin.js +8 -0
  6. package/dist/ci/base.d.ts +21 -0
  7. package/dist/ci/base.js +130 -0
  8. package/dist/ci/github-env.d.ts +19 -0
  9. package/dist/ci/github-env.js +32 -0
  10. package/dist/ci/outputs.d.ts +3 -0
  11. package/dist/ci/outputs.js +13 -0
  12. package/dist/commands/affected.d.ts +9 -0
  13. package/dist/commands/affected.js +54 -0
  14. package/dist/commands/check.d.ts +3 -0
  15. package/dist/commands/check.js +6 -0
  16. package/dist/commands/graph.d.ts +5 -0
  17. package/dist/commands/graph.js +27 -0
  18. package/dist/config/load.d.ts +3 -0
  19. package/dist/config/load.js +28 -0
  20. package/dist/config/schema.d.ts +38 -0
  21. package/dist/config/schema.js +35 -0
  22. package/dist/forge/forge.d.ts +12 -0
  23. package/dist/forge/forge.js +1 -0
  24. package/dist/forge/github.d.ts +8 -0
  25. package/dist/forge/github.js +31 -0
  26. package/dist/fs-ctx.d.ts +3 -0
  27. package/dist/fs-ctx.js +25 -0
  28. package/dist/git/exec.d.ts +9 -0
  29. package/dist/git/exec.js +26 -0
  30. package/dist/git/reader.d.ts +13 -0
  31. package/dist/git/reader.js +114 -0
  32. package/dist/index.d.ts +5 -0
  33. package/dist/index.js +2 -0
  34. package/dist/main.d.ts +3 -0
  35. package/dist/main.js +66 -0
  36. package/dist/plugins/index.d.ts +2 -0
  37. package/dist/plugins/index.js +6 -0
  38. package/dist/plugins/node/index.d.ts +1 -0
  39. package/dist/plugins/node/index.js +100 -0
  40. package/dist/plugins/php/index.d.ts +1 -0
  41. package/dist/plugins/php/index.js +70 -0
  42. package/dist/session.d.ts +41 -0
  43. package/dist/session.js +77 -0
  44. package/dist/version.d.ts +1 -0
  45. package/dist/version.js +1 -0
  46. package/package.json +81 -4
  47. package/README.md +0 -3
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ /** Schema of `tallyrelease affected --json`, published as `affected.schema.json`. */
3
+ export declare const affectedDocSchema: z.ZodObject<{
4
+ schema: z.ZodLiteral<1>;
5
+ tool: z.ZodString;
6
+ repo: z.ZodString;
7
+ branch: z.ZodString;
8
+ head: z.ZodString;
9
+ build: z.ZodObject<{
10
+ base: z.ZodNullable<z.ZodString>;
11
+ baseSource: z.ZodEnum<{
12
+ cli: "cli";
13
+ "push-before": "push-before";
14
+ "pr-merge-base": "pr-merge-base";
15
+ "merge-group": "merge-group";
16
+ "forge-last-success": "forge-last-success";
17
+ none: "none";
18
+ }>;
19
+ all: z.ZodBoolean;
20
+ globalInputs: z.ZodArray<z.ZodString>;
21
+ }, z.core.$strict>;
22
+ units: z.ZodArray<z.ZodObject<{
23
+ id: z.ZodString;
24
+ path: z.ZodString;
25
+ plugin: z.ZodString;
26
+ reason: z.ZodEnum<{
27
+ direct: "direct";
28
+ dependent: "dependent";
29
+ global: "global";
30
+ all: "all";
31
+ }>;
32
+ via: z.ZodArray<z.ZodString>;
33
+ }, z.core.$strict>>;
34
+ unattributed: z.ZodArray<z.ZodString>;
35
+ warnings: z.ZodArray<z.ZodObject<{
36
+ code: z.ZodString;
37
+ message: z.ZodString;
38
+ plugin: z.ZodExactOptional<z.ZodString>;
39
+ }, z.core.$strict>>;
40
+ }, z.core.$strict>;
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ const reason = z.enum(["direct", "dependent", "global", "all"]);
3
+ /** Schema of `tallyrelease affected --json`, published as `affected.schema.json`. */
4
+ export const affectedDocSchema = z.strictObject({
5
+ schema: z.literal(1),
6
+ tool: z.string(),
7
+ repo: z.string(),
8
+ branch: z.string(),
9
+ head: z.string(),
10
+ build: z.strictObject({
11
+ base: z.string().nullable(),
12
+ baseSource: z.enum([
13
+ "cli",
14
+ "push-before",
15
+ "pr-merge-base",
16
+ "merge-group",
17
+ "forge-last-success",
18
+ "none",
19
+ ]),
20
+ all: z.boolean(),
21
+ globalInputs: z.array(z.string()),
22
+ }),
23
+ units: z.array(z.strictObject({
24
+ id: z.string(),
25
+ path: z.string(),
26
+ plugin: z.string(),
27
+ reason,
28
+ via: z.array(z.string()),
29
+ })),
30
+ unattributed: z.array(z.string()),
31
+ warnings: z.array(z.strictObject({
32
+ code: z.string(),
33
+ message: z.string(),
34
+ plugin: z.string().exactOptional(),
35
+ })),
36
+ });
@@ -0,0 +1,140 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "schema": {
6
+ "type": "number",
7
+ "const": 1
8
+ },
9
+ "tool": {
10
+ "type": "string"
11
+ },
12
+ "repo": {
13
+ "type": "string"
14
+ },
15
+ "branch": {
16
+ "type": "string"
17
+ },
18
+ "head": {
19
+ "type": "string"
20
+ },
21
+ "build": {
22
+ "type": "object",
23
+ "properties": {
24
+ "base": {
25
+ "type": [
26
+ "string",
27
+ "null"
28
+ ]
29
+ },
30
+ "baseSource": {
31
+ "type": "string",
32
+ "enum": [
33
+ "cli",
34
+ "push-before",
35
+ "pr-merge-base",
36
+ "merge-group",
37
+ "forge-last-success",
38
+ "none"
39
+ ]
40
+ },
41
+ "all": {
42
+ "type": "boolean"
43
+ },
44
+ "globalInputs": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "string"
48
+ }
49
+ }
50
+ },
51
+ "required": [
52
+ "base",
53
+ "baseSource",
54
+ "all",
55
+ "globalInputs"
56
+ ],
57
+ "additionalProperties": false
58
+ },
59
+ "units": {
60
+ "type": "array",
61
+ "items": {
62
+ "type": "object",
63
+ "properties": {
64
+ "id": {
65
+ "type": "string"
66
+ },
67
+ "path": {
68
+ "type": "string"
69
+ },
70
+ "plugin": {
71
+ "type": "string"
72
+ },
73
+ "reason": {
74
+ "type": "string",
75
+ "enum": [
76
+ "direct",
77
+ "dependent",
78
+ "global",
79
+ "all"
80
+ ]
81
+ },
82
+ "via": {
83
+ "type": "array",
84
+ "items": {
85
+ "type": "string"
86
+ }
87
+ }
88
+ },
89
+ "required": [
90
+ "id",
91
+ "path",
92
+ "plugin",
93
+ "reason",
94
+ "via"
95
+ ],
96
+ "additionalProperties": false
97
+ }
98
+ },
99
+ "unattributed": {
100
+ "type": "array",
101
+ "items": {
102
+ "type": "string"
103
+ }
104
+ },
105
+ "warnings": {
106
+ "type": "array",
107
+ "items": {
108
+ "type": "object",
109
+ "properties": {
110
+ "code": {
111
+ "type": "string"
112
+ },
113
+ "message": {
114
+ "type": "string"
115
+ },
116
+ "plugin": {
117
+ "type": "string"
118
+ }
119
+ },
120
+ "required": [
121
+ "code",
122
+ "message"
123
+ ],
124
+ "additionalProperties": false
125
+ }
126
+ }
127
+ },
128
+ "required": [
129
+ "schema",
130
+ "tool",
131
+ "repo",
132
+ "branch",
133
+ "head",
134
+ "build",
135
+ "units",
136
+ "unattributed",
137
+ "warnings"
138
+ ],
139
+ "additionalProperties": false
140
+ }
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./main.js";
3
+ process.exitCode = await main(process.argv.slice(2), {
4
+ env: process.env,
5
+ cwd: process.cwd(),
6
+ stdout: process.stdout,
7
+ stderr: process.stderr,
8
+ });
@@ -0,0 +1,21 @@
1
+ import type { GithubContext } from "./github-env.js";
2
+ import type { Forge } from "../forge/forge.js";
3
+ import type { RepoGit } from "../git/reader.js";
4
+ import type { BaseSource, Warning } from "@tallyrelease/core";
5
+ export interface BuildBase {
6
+ base: string | null;
7
+ source: BaseSource;
8
+ warnings: Warning[];
9
+ }
10
+ export interface BuildBaseInput {
11
+ git: RepoGit;
12
+ /** Resolved HEAD sha. */
13
+ head: string;
14
+ /** `--base`. */
15
+ cliBase?: string | undefined;
16
+ github?: GithubContext | undefined;
17
+ /** Present only when a token is available. */
18
+ forge?: Forge | undefined;
19
+ }
20
+ /** Resolves the build base from `--base`, the CI event, then the forge. */
21
+ export declare const resolveBuildBase: (input: BuildBaseInput) => Promise<BuildBase>;
@@ -0,0 +1,130 @@
1
+ import { TallyError } from "@tallyrelease/core";
2
+ const ZERO_SHA = /^0+$/;
3
+ const str = (value) => typeof value === "string" && value !== "" ? value : undefined;
4
+ const field = (value, key) => typeof value === "object" && value !== null
5
+ ? value[key]
6
+ : undefined;
7
+ const noForge = {
8
+ code: "W_NO_FORGE",
9
+ message: "no token for the forge; the last successful run is unknown",
10
+ };
11
+ const noWorkflowContext = {
12
+ code: "W_NO_WORKFLOW_CONTEXT",
13
+ message: "GITHUB_REPOSITORY, GITHUB_WORKFLOW_REF or GITHUB_REF_NAME is missing; the last successful run is unknown",
14
+ };
15
+ const forgeError = (error) => {
16
+ const status = error?.status;
17
+ const reason = error instanceof Error ? error.message : String(error);
18
+ return {
19
+ code: "W_FORGE_ERROR",
20
+ message: `forge request failed${typeof status === "number" ? ` (${String(status)})` : ""}: ${reason}; the last successful run is unknown`,
21
+ };
22
+ };
23
+ const noLastSuccess = {
24
+ code: "W_NO_LAST_SUCCESS",
25
+ message: "no successful push run of this workflow on this branch",
26
+ };
27
+ /** Resolves the build base from `--base`, the CI event, then the forge. */
28
+ export const resolveBuildBase = async (input) => {
29
+ const { git, head, github, forge } = input;
30
+ // A named sha missing from a shallow clone must not silently become "everything".
31
+ const local = async (ref, what) => {
32
+ const sha = await git.resolve(ref);
33
+ if (sha === undefined && (await git.isShallow())) {
34
+ throw new TallyError("E_SHALLOW_BASE", `${what} "${ref}" is missing in this shallow clone; fetch with fetch-depth: 0`, { ref });
35
+ }
36
+ return sha;
37
+ };
38
+ const required = async (ref, what) => {
39
+ const sha = ref === undefined ? undefined : await local(ref, what);
40
+ if (sha === undefined) {
41
+ throw new TallyError("E_BASE_UNRESOLVED", `${what} "${ref ?? ""}" does not resolve to a commit`, { ref });
42
+ }
43
+ return sha;
44
+ };
45
+ // A named sha that is not an ancestor may only be cut off by a shallow clone.
46
+ const shallowBase = async (ref, what) => {
47
+ if (await git.isShallow()) {
48
+ throw new TallyError("E_SHALLOW_BASE", `${what} "${ref}" has no history in common with HEAD in this shallow clone; fetch with fetch-depth: 0`, { ref });
49
+ }
50
+ };
51
+ /** The newest usable successful run, or the warning why there is none. */
52
+ const lastSuccess = async (from) => {
53
+ if (!github?.repository || !github.workflow || !github.refName) {
54
+ return noWorkflowContext;
55
+ }
56
+ let shas;
57
+ try {
58
+ shas = await from.lastSuccessfulPushRuns({
59
+ ...github.repository,
60
+ workflow: github.workflow,
61
+ branch: github.refName,
62
+ });
63
+ }
64
+ catch (error) {
65
+ return forgeError(error);
66
+ }
67
+ for (const candidate of shas) {
68
+ // eslint-disable-next-line no-await-in-loop -- newest usable run wins
69
+ const sha = await local(candidate, "last successful run");
70
+ // eslint-disable-next-line no-await-in-loop -- newest usable run wins
71
+ if (sha !== undefined && (await git.isAncestor(sha, head))) {
72
+ return sha;
73
+ }
74
+ }
75
+ return noLastSuccess;
76
+ };
77
+ const result = (base, source, ...warnings) => ({ base, source, warnings });
78
+ const fromLastSuccess = async () => {
79
+ if (!forge) {
80
+ return result(null, "none", noForge);
81
+ }
82
+ const last = await lastSuccess(forge);
83
+ return typeof last === "string"
84
+ ? result(last, "forge-last-success")
85
+ : result(null, "none", last);
86
+ };
87
+ if (input.cliBase !== undefined) {
88
+ return result(await required(input.cliBase, "--base"), "cli");
89
+ }
90
+ switch (github?.eventName) {
91
+ case "pull_request": {
92
+ const target = await required(str(field(field(github.payload()["pull_request"], "base"), "sha")), "pull_request.base.sha");
93
+ const base = await git.mergeBase(head, target);
94
+ if (base === undefined) {
95
+ await shallowBase(target, "pull_request.base.sha");
96
+ throw new TallyError("E_BASE_UNRESOLVED", `no merge base of HEAD and "${target}"`, { ref: target });
97
+ }
98
+ return result(base, "pr-merge-base");
99
+ }
100
+ case "merge_group":
101
+ return result(await required(str(field(github.payload()["merge_group"], "base_sha")), "merge_group.base_sha"), "merge-group");
102
+ case "push": {
103
+ const ref = str(github.payload()["before"]);
104
+ const before = ref === undefined || ZERO_SHA.test(ref)
105
+ ? undefined
106
+ : await local(ref, "push before");
107
+ if (before !== undefined && (await git.isAncestor(before, head))) {
108
+ if (!forge) {
109
+ return result(before, "push-before", noForge);
110
+ }
111
+ const last = await lastSuccess(forge);
112
+ if (typeof last !== "string") {
113
+ return result(before, "push-before", last);
114
+ }
115
+ return (await git.isAncestor(last, before))
116
+ ? result(last, "forge-last-success")
117
+ : result(before, "push-before");
118
+ }
119
+ if (before !== undefined) {
120
+ await shallowBase(before, "push before");
121
+ }
122
+ return await fromLastSuccess();
123
+ }
124
+ case "workflow_dispatch":
125
+ case "schedule":
126
+ return await fromLastSuccess();
127
+ default:
128
+ return result(null, "none");
129
+ }
130
+ };
@@ -0,0 +1,19 @@
1
+ export type Env = Readonly<Record<string, string | undefined>>;
2
+ /** GitHub Actions run context from `GITHUB_*` variables. */
3
+ export interface GithubContext {
4
+ eventName: string;
5
+ /** Parsed `GITHUB_EVENT_PATH` (read on first call), empty when unset. */
6
+ payload: () => Record<string, unknown>;
7
+ refName: string | undefined;
8
+ /** Head branch of a pull request run. */
9
+ headRef: string | undefined;
10
+ /** Workflow file name from `GITHUB_WORKFLOW_REF`. */
11
+ workflow: string | undefined;
12
+ repository: {
13
+ owner: string;
14
+ repo: string;
15
+ } | undefined;
16
+ apiUrl: string | undefined;
17
+ }
18
+ /** Undefined outside GitHub Actions (no `GITHUB_EVENT_NAME`). */
19
+ export declare const githubContext: (env: Env) => GithubContext | undefined;
@@ -0,0 +1,32 @@
1
+ import { TallyError } from "@tallyrelease/core";
2
+ import { readFileSync } from "node:fs";
3
+ const nonEmpty = (value) => value === "" ? undefined : value;
4
+ const parseEvent = (path) => {
5
+ try {
6
+ return JSON.parse(readFileSync(path, "utf8"));
7
+ }
8
+ catch (error) {
9
+ throw new TallyError("E_GITHUB_EVENT", `cannot read GITHUB_EVENT_PATH "${path}": ${error instanceof Error ? error.message : String(error)}`, { path });
10
+ }
11
+ };
12
+ /** Undefined outside GitHub Actions (no `GITHUB_EVENT_NAME`). */
13
+ export const githubContext = (env) => {
14
+ const eventName = env["GITHUB_EVENT_NAME"];
15
+ if (!eventName) {
16
+ return undefined;
17
+ }
18
+ const eventPath = env["GITHUB_EVENT_PATH"];
19
+ let payload;
20
+ // owner/repo/.github/workflows/ci.yaml@refs/heads/main
21
+ const workflowPath = env["GITHUB_WORKFLOW_REF"]?.split("@")[0];
22
+ const [owner, repo] = env["GITHUB_REPOSITORY"]?.split("/") ?? [];
23
+ return {
24
+ eventName,
25
+ payload: () => (payload ??= eventPath ? parseEvent(eventPath) : {}),
26
+ refName: nonEmpty(env["GITHUB_REF_NAME"]),
27
+ headRef: nonEmpty(env["GITHUB_HEAD_REF"]),
28
+ workflow: nonEmpty(workflowPath?.split("/").at(-1)),
29
+ repository: owner && repo ? { owner, repo } : undefined,
30
+ apiUrl: nonEmpty(env["GITHUB_API_URL"]),
31
+ };
32
+ };
@@ -0,0 +1,3 @@
1
+ import type { AffectedDoc } from "@tallyrelease/core";
2
+ /** `$GITHUB_OUTPUT` entries of `affected`; units are ordered by id. */
3
+ export declare const affectedGithubOutput: (doc: AffectedDoc) => string;
@@ -0,0 +1,13 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const multiline = (key, value) => {
3
+ const delimiter = `ghadelimiter_${randomUUID()}`;
4
+ return `${key}<<${delimiter}\n${value}\n${delimiter}\n`;
5
+ };
6
+ /** `$GITHUB_OUTPUT` entries of `affected`; units are ordered by id. */
7
+ export const affectedGithubOutput = (doc) => [
8
+ `affected=${JSON.stringify(doc.units.map(({ id, path, reason }) => ({ id, path, reason })))}\n`,
9
+ multiline("affected-paths", doc.units.map((u) => u.path).join("\n")),
10
+ `all=${String(doc.build.all)}\n`,
11
+ `base=${doc.build.base ?? ""}\n`,
12
+ `base-source=${doc.build.baseSource}\n`,
13
+ ].join("");
@@ -0,0 +1,9 @@
1
+ import type { Session } from "../session.js";
2
+ export interface AffectedFlags {
3
+ base?: string;
4
+ json?: boolean;
5
+ out?: string;
6
+ githubOutput?: boolean;
7
+ }
8
+ /** Computes the build set and writes the requested outputs. */
9
+ export declare const affected: (session: Session, flags: AffectedFlags) => Promise<void>;
@@ -0,0 +1,54 @@
1
+ import { computeAffected, TallyError } from "@tallyrelease/core";
2
+ import { appendFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { resolveBuildBase } from "../ci/base.js";
5
+ import { affectedGithubOutput } from "../ci/outputs.js";
6
+ import { version } from "../version.js";
7
+ /** Computes the build set and writes the requested outputs. */
8
+ export const affected = async (session, flags) => {
9
+ const { git, github } = session;
10
+ const workspace = await session.workspace();
11
+ const head = await git.resolve("HEAD");
12
+ if (head === undefined) {
13
+ throw new TallyError("E_NO_HEAD", "HEAD does not resolve to a commit; the repository has no commits");
14
+ }
15
+ const base = await resolveBuildBase({
16
+ git,
17
+ head,
18
+ cliBase: flags.base,
19
+ github,
20
+ forge: session.forge,
21
+ });
22
+ const computed = await computeAffected(workspace, git, {
23
+ tool: `tallyrelease@${version}`,
24
+ branch: github?.headRef ?? github?.refName ?? (await git.branch()) ?? "HEAD",
25
+ head,
26
+ base: base.base,
27
+ baseSource: base.source,
28
+ });
29
+ const doc = {
30
+ ...computed,
31
+ warnings: [...computed.warnings, ...base.warnings],
32
+ };
33
+ session.warn(doc.warnings);
34
+ const json = `${JSON.stringify(doc, null, 2)}\n`;
35
+ if (flags.out !== undefined) {
36
+ writeFileSync(resolve(session.cwd, flags.out), json);
37
+ }
38
+ if (flags.githubOutput) {
39
+ const path = session.env["GITHUB_OUTPUT"];
40
+ if (!path) {
41
+ throw new TallyError("E_GITHUB_OUTPUT", "--github-output requires GITHUB_OUTPUT to be set");
42
+ }
43
+ appendFileSync(path, affectedGithubOutput(doc));
44
+ }
45
+ if (flags.json) {
46
+ session.stdout.write(json);
47
+ return;
48
+ }
49
+ session.stdout.write(`base: ${doc.build.base ?? "none"} (${doc.build.baseSource})\n`);
50
+ for (const u of doc.units) {
51
+ const via = u.via.length > 0 ? ` via ${u.via.join(", ")}` : "";
52
+ session.stdout.write(`${u.id}\t${u.path}\t${u.reason}${via}\n`);
53
+ }
54
+ };
@@ -0,0 +1,3 @@
1
+ import type { Session } from "../session.js";
2
+ /** Validates config, plugins, units and graph. */
3
+ export declare const check: (session: Session) => Promise<void>;
@@ -0,0 +1,6 @@
1
+ /** Validates config, plugins, units and graph. */
2
+ export const check = async (session) => {
3
+ const workspace = await session.workspace();
4
+ session.warn(workspace.warnings);
5
+ session.stdout.write(`ok: ${String(workspace.units.length)} units, ${String(workspace.graph.edges.length)} edges\n`);
6
+ };
@@ -0,0 +1,5 @@
1
+ import type { Session } from "../session.js";
2
+ /** Prints units and edges. */
3
+ export declare const graph: (session: Session, flags: {
4
+ json?: boolean;
5
+ }) => Promise<void>;
@@ -0,0 +1,27 @@
1
+ /** Prints units and edges. */
2
+ export const graph = async (session, flags) => {
3
+ const workspace = await session.workspace();
4
+ session.warn(workspace.warnings);
5
+ const units = workspace.units.map((u) => ({
6
+ id: u.id,
7
+ path: u.root,
8
+ plugin: u.plugin,
9
+ version: u.version,
10
+ }));
11
+ const edges = workspace.graph.edges.map((e) => ({
12
+ from: e.from,
13
+ to: e.to,
14
+ pinned: e.pinned,
15
+ reasons: e.reasons,
16
+ }));
17
+ if (flags.json) {
18
+ session.stdout.write(`${JSON.stringify({ repo: workspace.repo, units, edges }, null, 2)}\n`);
19
+ return;
20
+ }
21
+ for (const u of units) {
22
+ session.stdout.write(`${u.id}\t${u.path}\t${u.plugin}\n`);
23
+ }
24
+ for (const e of edges) {
25
+ session.stdout.write(`${e.from} -> ${e.to}${e.pinned ? " (pinned)" : ""}\n`);
26
+ }
27
+ };
@@ -0,0 +1,3 @@
1
+ import type { ReadonlyFs, TallyConfig } from "@tallyrelease/core";
2
+ /** Reads and validates the config file at the repo-relative `path`. */
3
+ export declare const loadConfig: (fs: ReadonlyFs, path: string) => TallyConfig;
@@ -0,0 +1,28 @@
1
+ import { TallyError } from "@tallyrelease/core";
2
+ import { parse } from "yaml";
3
+ import { configSchema } from "./schema.js";
4
+ /** Reads and validates the config file at the repo-relative `path`. */
5
+ export const loadConfig = (fs, path) => {
6
+ const text = fs.read(path);
7
+ if (text === undefined) {
8
+ throw new TallyError("E_CONFIG_MISSING", `config file "${path}" not found`, {
9
+ path,
10
+ });
11
+ }
12
+ let raw;
13
+ try {
14
+ raw = parse(text) ?? {};
15
+ }
16
+ catch (error) {
17
+ throw new TallyError("E_CONFIG", `${path}: ${error instanceof Error ? error.message : String(error)}`, { path });
18
+ }
19
+ const result = configSchema.safeParse(raw);
20
+ if (!result.success) {
21
+ const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
22
+ throw new TallyError("E_CONFIG", `${path}: ${issues.join("; ")}`, {
23
+ path,
24
+ issues: result.error.issues,
25
+ });
26
+ }
27
+ return result.data;
28
+ };
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ /** Schema of `tallyrelease.yaml`. */
3
+ export declare const configSchema: z.ZodObject<{
4
+ plugins: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>>;
5
+ defaults: z.ZodExactOptional<z.ZodObject<{
6
+ tagFormat: z.ZodExactOptional<z.ZodString>;
7
+ propagate: z.ZodExactOptional<z.ZodEnum<{
8
+ auto: "auto";
9
+ patch: "patch";
10
+ rebuild: "rebuild";
11
+ }>>;
12
+ }, z.core.$strict>>;
13
+ units: z.ZodExactOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
14
+ id: z.ZodExactOptional<z.ZodString>;
15
+ include: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
16
+ exclude: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
17
+ version: z.ZodExactOptional<z.ZodEnum<{
18
+ file: "file";
19
+ none: "none";
20
+ tag: "tag";
21
+ }>>;
22
+ scheme: z.ZodExactOptional<z.ZodString>;
23
+ tagFormat: z.ZodExactOptional<z.ZodString>;
24
+ propagate: z.ZodExactOptional<z.ZodEnum<{
25
+ auto: "auto";
26
+ patch: "patch";
27
+ rebuild: "rebuild";
28
+ }>>;
29
+ }, z.core.$strict>>>;
30
+ deps: z.ZodExactOptional<z.ZodArray<z.ZodObject<{
31
+ from: z.ZodString;
32
+ to: z.ZodString;
33
+ pinned: z.ZodExactOptional<z.ZodBoolean>;
34
+ reason: z.ZodExactOptional<z.ZodString>;
35
+ }, z.core.$strict>>>;
36
+ ignore: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
37
+ globalInputs: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
38
+ }, z.core.$strict>;
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ const globs = z.array(z.string()).exactOptional();
3
+ const propagate = z.enum(["auto", "patch", "rebuild"]);
4
+ const unitOverlay = z.strictObject({
5
+ id: z.string().exactOptional(),
6
+ include: globs,
7
+ exclude: globs,
8
+ version: z.enum(["file", "tag", "none"]).exactOptional(),
9
+ scheme: z.string().exactOptional(),
10
+ tagFormat: z.string().exactOptional(),
11
+ propagate: propagate.exactOptional(),
12
+ });
13
+ /** Schema of `tallyrelease.yaml`. */
14
+ export const configSchema = z.strictObject({
15
+ plugins: z
16
+ .record(z.string(), z.record(z.string(), z.unknown()).nullable())
17
+ .default({}),
18
+ defaults: z
19
+ .strictObject({
20
+ tagFormat: z.string().exactOptional(),
21
+ propagate: propagate.exactOptional(),
22
+ })
23
+ .exactOptional(),
24
+ units: z.record(z.string(), unitOverlay).exactOptional(),
25
+ deps: z
26
+ .array(z.strictObject({
27
+ from: z.string(),
28
+ to: z.string(),
29
+ pinned: z.boolean().exactOptional(),
30
+ reason: z.string().exactOptional(),
31
+ }))
32
+ .exactOptional(),
33
+ ignore: globs,
34
+ globalInputs: globs,
35
+ });