@tallyrelease/cli 0.0.0-stage → 0.0.0
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/affected-schema.d.ts +40 -0
- package/dist/affected-schema.js +36 -0
- package/dist/affected.schema.json +140 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/ci/base.d.ts +21 -0
- package/dist/ci/base.js +130 -0
- package/dist/ci/github-env.d.ts +19 -0
- package/dist/ci/github-env.js +32 -0
- package/dist/ci/outputs.d.ts +3 -0
- package/dist/ci/outputs.js +13 -0
- package/dist/commands/affected.d.ts +9 -0
- package/dist/commands/affected.js +54 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.js +6 -0
- package/dist/commands/graph.d.ts +5 -0
- package/dist/commands/graph.js +27 -0
- package/dist/config/load.d.ts +3 -0
- package/dist/config/load.js +28 -0
- package/dist/config/schema.d.ts +38 -0
- package/dist/config/schema.js +35 -0
- package/dist/forge/forge.d.ts +12 -0
- package/dist/forge/forge.js +1 -0
- package/dist/forge/github.d.ts +8 -0
- package/dist/forge/github.js +31 -0
- package/dist/fs-ctx.d.ts +3 -0
- package/dist/fs-ctx.js +25 -0
- package/dist/git/exec.d.ts +9 -0
- package/dist/git/exec.js +26 -0
- package/dist/git/reader.d.ts +13 -0
- package/dist/git/reader.js +114 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +66 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +6 -0
- package/dist/plugins/node/index.d.ts +1 -0
- package/dist/plugins/node/index.js +100 -0
- package/dist/plugins/php/index.d.ts +1 -0
- package/dist/plugins/php/index.js +70 -0
- package/dist/session.d.ts +41 -0
- package/dist/session.js +77 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +80 -3
- 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
package/dist/bin.js
ADDED
|
@@ -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>;
|
package/dist/ci/base.js
ADDED
|
@@ -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,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,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,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,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
|
+
});
|