@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.
- 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 +81 -4
- package/README.md +0 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface WorkflowRunQuery {
|
|
2
|
+
owner: string;
|
|
3
|
+
repo: string;
|
|
4
|
+
/** Workflow file name, for example `ci.yaml`. */
|
|
5
|
+
workflow: string;
|
|
6
|
+
branch: string;
|
|
7
|
+
}
|
|
8
|
+
/** Code host API used by the cli. */
|
|
9
|
+
export interface Forge {
|
|
10
|
+
/** Head shas of successful push runs of a workflow on a branch, newest first. */
|
|
11
|
+
lastSuccessfulPushRuns: (query: WorkflowRunQuery) => Promise<string[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Forge } from "./forge.js";
|
|
2
|
+
export interface GithubForgeOptions {
|
|
3
|
+
token: string;
|
|
4
|
+
/** `GITHUB_API_URL`, defaults to the public API. */
|
|
5
|
+
baseUrl?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** `Forge` backed by the GitHub REST API. */
|
|
8
|
+
export declare const githubForge: (opts: GithubForgeOptions) => Forge;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Octokit } from "@octokit/rest";
|
|
2
|
+
/** `Forge` backed by the GitHub REST API. */
|
|
3
|
+
export const githubForge = (opts) => {
|
|
4
|
+
const octokit = new Octokit({
|
|
5
|
+
auth: opts.token,
|
|
6
|
+
baseUrl: opts.baseUrl ?? "https://api.github.com",
|
|
7
|
+
});
|
|
8
|
+
return {
|
|
9
|
+
lastSuccessfulPushRuns: async ({ owner, repo, workflow, branch }) => {
|
|
10
|
+
try {
|
|
11
|
+
const { data } = await octokit.rest.actions.listWorkflowRuns({
|
|
12
|
+
owner,
|
|
13
|
+
repo,
|
|
14
|
+
workflow_id: workflow,
|
|
15
|
+
branch,
|
|
16
|
+
status: "success",
|
|
17
|
+
event: "push",
|
|
18
|
+
per_page: 100,
|
|
19
|
+
});
|
|
20
|
+
return data.workflow_runs.map((run) => run.head_sha);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// A renamed or deleted workflow has no runs to build on.
|
|
24
|
+
if (error.status === 404) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
package/dist/fs-ctx.d.ts
ADDED
package/dist/fs-ctx.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { globSync } from "tinyglobby";
|
|
4
|
+
const IGNORED = ["**/.git/**", "**/node_modules/**", "vendor/**"];
|
|
5
|
+
/** `ReadonlyFs` over the directory `root`. */
|
|
6
|
+
export const nodeFs = (root) => ({
|
|
7
|
+
read: (path) => {
|
|
8
|
+
try {
|
|
9
|
+
return readFileSync(join(root, path), "utf8");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
exists: (path) => existsSync(join(root, path)),
|
|
16
|
+
glob: (patterns, opts) => globSync(patterns, {
|
|
17
|
+
cwd: root,
|
|
18
|
+
dot: true,
|
|
19
|
+
ignore: IGNORED,
|
|
20
|
+
expandDirectories: false,
|
|
21
|
+
onlyDirectories: opts?.dirs ?? false,
|
|
22
|
+
})
|
|
23
|
+
.map((p) => p.replace(/\/$/, ""))
|
|
24
|
+
.sort(),
|
|
25
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface GitResult {
|
|
2
|
+
code: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
/** Runs `git` in `cwd`; resolves with the exit code instead of rejecting. */
|
|
7
|
+
export declare const git: (cwd: string, args: readonly string[], debug?: (message: string) => void) => Promise<GitResult>;
|
|
8
|
+
/** Runs `git` and returns stdout; throws on a non-zero exit code. */
|
|
9
|
+
export declare const gitOk: (cwd: string, args: readonly string[], debug?: (message: string) => void) => Promise<string>;
|
package/dist/git/exec.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
/** Runs `git` in `cwd`; resolves with the exit code instead of rejecting. */
|
|
3
|
+
export const git = (cwd, args, debug) => {
|
|
4
|
+
debug?.(`git ${args.join(" ")}`);
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
execFile("git", ["-c", "core.quotePath=false", ...args], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
7
|
+
if (error === null) {
|
|
8
|
+
resolve({ code: 0, stdout, stderr });
|
|
9
|
+
}
|
|
10
|
+
else if (typeof error.code === "number") {
|
|
11
|
+
resolve({ code: error.code, stdout, stderr });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
reject(new Error(error.message, { cause: error }));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
/** Runs `git` and returns stdout; throws on a non-zero exit code. */
|
|
20
|
+
export const gitOk = async (cwd, args, debug) => {
|
|
21
|
+
const result = await git(cwd, args, debug);
|
|
22
|
+
if (result.code !== 0) {
|
|
23
|
+
throw new Error(`git ${args.join(" ")} failed (${String(result.code)}): ${result.stderr.trim()}`);
|
|
24
|
+
}
|
|
25
|
+
return result.stdout;
|
|
26
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GitReader, MaybePromise } from "@tallyrelease/core";
|
|
2
|
+
/** `GitReader` plus the queries the cli needs to resolve a build base. */
|
|
3
|
+
export interface RepoGit extends GitReader {
|
|
4
|
+
/** Full commit sha of `ref`, undefined if it does not resolve locally. */
|
|
5
|
+
resolve: (ref: string) => MaybePromise<string | undefined>;
|
|
6
|
+
/** Best common ancestor, undefined if none. */
|
|
7
|
+
mergeBase: (a: string, b: string) => MaybePromise<string | undefined>;
|
|
8
|
+
isShallow: () => MaybePromise<boolean>;
|
|
9
|
+
/** Current branch, undefined when detached. */
|
|
10
|
+
branch: () => MaybePromise<string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/** `RepoGit` backed by the `git` executable in `cwd`. */
|
|
13
|
+
export declare const gitReader: (cwd: string, debug?: (message: string) => void) => RepoGit;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { gitOk, git as run } from "./exec.js";
|
|
2
|
+
const byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
3
|
+
const lines = (out) => out.split("\n").filter((l) => l !== "");
|
|
4
|
+
const nulFields = (out) => out.split("\0").filter((l) => l !== "");
|
|
5
|
+
/** `RepoGit` backed by the `git` executable in `cwd`. */
|
|
6
|
+
export const gitReader = (cwd, debug) => {
|
|
7
|
+
const ok = (args) => gitOk(cwd, args, debug);
|
|
8
|
+
const resolve = async (ref) => {
|
|
9
|
+
const result = await run(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], debug);
|
|
10
|
+
return result.code === 0 ? result.stdout.trim() : undefined;
|
|
11
|
+
};
|
|
12
|
+
const tagRefs = async (extra) => {
|
|
13
|
+
const refs = lines(await ok([
|
|
14
|
+
"for-each-ref",
|
|
15
|
+
...extra,
|
|
16
|
+
"--format=%(refname:strip=2)%00%(objecttype)%00%(objectname)%00%(*objecttype)%00%(*objectname)",
|
|
17
|
+
"refs/tags",
|
|
18
|
+
])).map(async (line) => {
|
|
19
|
+
const [name = "", type = "", sha = "", peeledType = "", peeled = ""] = line.split("\0");
|
|
20
|
+
if (peeledType === "commit") {
|
|
21
|
+
return { name, sha: peeled };
|
|
22
|
+
}
|
|
23
|
+
if (type === "commit") {
|
|
24
|
+
return { name, sha };
|
|
25
|
+
}
|
|
26
|
+
// Older git peels one level only; tags of tags need a full peel.
|
|
27
|
+
const commit = await resolve(sha);
|
|
28
|
+
return commit === undefined ? undefined : { name, sha: commit };
|
|
29
|
+
});
|
|
30
|
+
return (await Promise.all(refs))
|
|
31
|
+
.filter((t) => t !== undefined)
|
|
32
|
+
.sort(byName);
|
|
33
|
+
};
|
|
34
|
+
const firstParents = new Map();
|
|
35
|
+
const firstParentSet = (ref) => {
|
|
36
|
+
let set = firstParents.get(ref);
|
|
37
|
+
if (set === undefined) {
|
|
38
|
+
set = ok(["rev-list", "--first-parent", ref]).then((out) => new Set(lines(out)));
|
|
39
|
+
firstParents.set(ref, set);
|
|
40
|
+
}
|
|
41
|
+
return set;
|
|
42
|
+
};
|
|
43
|
+
const range = (from, to) => from === null ? [to] : [to, `^${from}`];
|
|
44
|
+
return {
|
|
45
|
+
tags: () => tagRefs([]),
|
|
46
|
+
tagsMerged: (ref) => tagRefs([`--merged=${ref}`]),
|
|
47
|
+
isAncestor: async (a, b) => {
|
|
48
|
+
const result = await run(cwd, ["merge-base", "--is-ancestor", a, b], debug);
|
|
49
|
+
if (result.code > 1) {
|
|
50
|
+
throw new Error(`git merge-base failed: ${result.stderr.trim()}`);
|
|
51
|
+
}
|
|
52
|
+
return result.code === 0;
|
|
53
|
+
},
|
|
54
|
+
isFirstParentAncestor: async (a, b) => {
|
|
55
|
+
const sha = await resolve(a);
|
|
56
|
+
return sha !== undefined && (await firstParentSet(b)).has(sha);
|
|
57
|
+
},
|
|
58
|
+
log: async (from, to) => {
|
|
59
|
+
const out = await ok([
|
|
60
|
+
"log",
|
|
61
|
+
"-z",
|
|
62
|
+
"--no-show-signature",
|
|
63
|
+
"--no-color",
|
|
64
|
+
"--no-merges",
|
|
65
|
+
"--no-renames",
|
|
66
|
+
"--name-only",
|
|
67
|
+
"--format=%x1e%H%x00%P%x00%cI%x00%B%x00",
|
|
68
|
+
...range(from, to),
|
|
69
|
+
"--",
|
|
70
|
+
]);
|
|
71
|
+
return out
|
|
72
|
+
.split("\x1e")
|
|
73
|
+
.slice(1)
|
|
74
|
+
.map((record) => {
|
|
75
|
+
const [sha = "", parents = "", date = "", message = "", ...rest] = record.split("\0");
|
|
76
|
+
// Files follow the header as "\0\n" plus NUL-terminated paths.
|
|
77
|
+
const files = nulFields(rest.join("\0").replace(/^\0\n?/, ""));
|
|
78
|
+
return {
|
|
79
|
+
sha,
|
|
80
|
+
parents: parents.split(" ").filter((p) => p !== ""),
|
|
81
|
+
message: message.trimEnd(),
|
|
82
|
+
files: files.sort(),
|
|
83
|
+
date,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
countFirstParent: async (from, to) => Number((await ok([
|
|
88
|
+
"rev-list",
|
|
89
|
+
"--count",
|
|
90
|
+
"--first-parent",
|
|
91
|
+
...range(from, to),
|
|
92
|
+
])).trim()),
|
|
93
|
+
diffFiles: async (from, to) => nulFields(await ok([
|
|
94
|
+
"diff",
|
|
95
|
+
"-z",
|
|
96
|
+
"--no-color",
|
|
97
|
+
"--name-only",
|
|
98
|
+
"--no-renames",
|
|
99
|
+
from,
|
|
100
|
+
to,
|
|
101
|
+
"--",
|
|
102
|
+
])).sort(),
|
|
103
|
+
resolve,
|
|
104
|
+
mergeBase: async (a, b) => {
|
|
105
|
+
const result = await run(cwd, ["merge-base", a, b], debug);
|
|
106
|
+
return result.code === 0 ? result.stdout.trim() : undefined;
|
|
107
|
+
},
|
|
108
|
+
isShallow: async () => (await ok(["rev-parse", "--is-shallow-repository"])).trim() === "true",
|
|
109
|
+
branch: async () => {
|
|
110
|
+
const result = await run(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], debug);
|
|
111
|
+
return result.code === 0 ? result.stdout.trim() : undefined;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CONFIG_FILE, TallyError } from "@tallyrelease/core";
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
import { inspect } from "node:util";
|
|
4
|
+
import { affected } from "./commands/affected.js";
|
|
5
|
+
import { check } from "./commands/check.js";
|
|
6
|
+
import { graph } from "./commands/graph.js";
|
|
7
|
+
import { createSession } from "./session.js";
|
|
8
|
+
import { version } from "./version.js";
|
|
9
|
+
/** Runs the cli; returns the exit code (0 ok, 1 error, 2 usage). */
|
|
10
|
+
export const main = async (argv, deps) => {
|
|
11
|
+
const program = new Command("tallyrelease")
|
|
12
|
+
.description("Affected build units for monorepos and single repos")
|
|
13
|
+
.version(version)
|
|
14
|
+
.option("--cwd <dir>", "repository root")
|
|
15
|
+
.option("--config <file>", "config file", CONFIG_FILE)
|
|
16
|
+
.option("--token <token>", "forge token (default: GITHUB_TOKEN)")
|
|
17
|
+
.option("--verbose", "print debug output")
|
|
18
|
+
.exitOverride()
|
|
19
|
+
.configureOutput({
|
|
20
|
+
writeOut: (s) => deps.stdout.write(s),
|
|
21
|
+
writeErr: (s) => deps.stderr.write(s),
|
|
22
|
+
});
|
|
23
|
+
const session = () => createSession(deps, program.opts());
|
|
24
|
+
program
|
|
25
|
+
.command("check")
|
|
26
|
+
.description("validate config, plugins, units and graph")
|
|
27
|
+
.action(async () => {
|
|
28
|
+
await check(session());
|
|
29
|
+
});
|
|
30
|
+
program
|
|
31
|
+
.command("graph")
|
|
32
|
+
.description("print units and edges")
|
|
33
|
+
.option("--json", "print JSON")
|
|
34
|
+
.action(async (flags) => {
|
|
35
|
+
await graph(session(), flags);
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command("affected")
|
|
39
|
+
.description("compute the build set")
|
|
40
|
+
.option("--base <ref>", "diff against this ref")
|
|
41
|
+
.option("--json", "print the affected document")
|
|
42
|
+
.option("--out <file>", "write the affected document to a file")
|
|
43
|
+
.option("--github-output", "append outputs to $GITHUB_OUTPUT")
|
|
44
|
+
.action(async (flags) => {
|
|
45
|
+
await affected(session(), flags);
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
await program.parseAsync(argv, { from: "user" });
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error instanceof CommanderError) {
|
|
53
|
+
return error.exitCode === 0 ? 0 : 2;
|
|
54
|
+
}
|
|
55
|
+
if (error instanceof TallyError) {
|
|
56
|
+
deps.stderr.write(`${error.code}: ${error.message}\n`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
deps.stderr.write(`E_INTERNAL: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
60
|
+
if (program.opts().verbose) {
|
|
61
|
+
deps.stderr.write(`${inspect(error)}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const nodePlugin: import("@tallyrelease/core").Plugin<unknown>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { defaultUnitId, definePlugin } from "@tallyrelease/core";
|
|
2
|
+
import { parse as parseYaml } from "yaml";
|
|
3
|
+
const PINNED_FIELDS = ["dependencies", "optionalDependencies"];
|
|
4
|
+
const UNPINNED_FIELDS = ["peerDependencies", "devDependencies"];
|
|
5
|
+
const EXACT = /^(workspace:)?(\*|=?v?\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$/;
|
|
6
|
+
const manifestPath = (root) => root === "." ? "package.json" : `${root}/package.json`;
|
|
7
|
+
const readManifest = (ctx, root) => {
|
|
8
|
+
const raw = ctx.fs.read(manifestPath(root));
|
|
9
|
+
return raw === undefined ? undefined : JSON.parse(raw);
|
|
10
|
+
};
|
|
11
|
+
/** Workspace globs from `pnpm-workspace.yaml` or `package.json`, if any. */
|
|
12
|
+
const workspacePatterns = (ctx) => {
|
|
13
|
+
const pnpm = ctx.fs.read("pnpm-workspace.yaml");
|
|
14
|
+
if (pnpm !== undefined) {
|
|
15
|
+
const doc = parseYaml(pnpm);
|
|
16
|
+
return doc?.packages ?? [];
|
|
17
|
+
}
|
|
18
|
+
const { workspaces } = readManifest(ctx, ".") ?? {};
|
|
19
|
+
if (workspaces === undefined) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return Array.isArray(workspaces) ? workspaces : (workspaces.packages ?? []);
|
|
23
|
+
};
|
|
24
|
+
const packageRoots = (ctx, patterns) => {
|
|
25
|
+
const manifests = (list) => ctx.fs.glob(list.map((p) => `${p.replace(/\/+$/, "")}/package.json`));
|
|
26
|
+
const excluded = new Set(manifests(patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1))));
|
|
27
|
+
return manifests(patterns.filter((p) => !p.startsWith("!")))
|
|
28
|
+
.filter((p) => !excluded.has(p))
|
|
29
|
+
.map((p) => p.slice(0, -"/package.json".length));
|
|
30
|
+
};
|
|
31
|
+
export const nodePlugin = definePlugin({
|
|
32
|
+
apiVersion: 1,
|
|
33
|
+
name: "node",
|
|
34
|
+
discover: (ctx) => {
|
|
35
|
+
const patterns = workspacePatterns(ctx);
|
|
36
|
+
const roots = patterns === undefined
|
|
37
|
+
? ctx.fs.exists("package.json")
|
|
38
|
+
? ["."]
|
|
39
|
+
: []
|
|
40
|
+
: packageRoots(ctx, patterns);
|
|
41
|
+
return roots.map((root) => {
|
|
42
|
+
const packageName = readManifest(ctx, root)?.name;
|
|
43
|
+
return {
|
|
44
|
+
id: defaultUnitId(root, ctx.repo.name),
|
|
45
|
+
root,
|
|
46
|
+
version: "file",
|
|
47
|
+
meta: packageName === undefined ? {} : { packageName },
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
edges: (ctx, units) => {
|
|
52
|
+
const byName = new Map();
|
|
53
|
+
for (const unit of units) {
|
|
54
|
+
const name = unit.meta["node"]?.["packageName"];
|
|
55
|
+
if (unit.plugin === "node" && typeof name === "string") {
|
|
56
|
+
const prior = byName.get(name);
|
|
57
|
+
if (prior) {
|
|
58
|
+
ctx.log.warn(`package "${name}" is declared by "${prior.root}" and "${unit.root}"; using "${unit.root}"`);
|
|
59
|
+
}
|
|
60
|
+
byName.set(name, unit);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const edges = [];
|
|
64
|
+
for (const to of byName.values()) {
|
|
65
|
+
const manifest = readManifest(ctx, to.root) ?? {};
|
|
66
|
+
const fields = [
|
|
67
|
+
...PINNED_FIELDS.map((field) => [field, true]),
|
|
68
|
+
...UNPINNED_FIELDS.map((field) => [field, false]),
|
|
69
|
+
];
|
|
70
|
+
for (const [field, pinnable] of fields) {
|
|
71
|
+
const deps = (manifest[field] ?? {});
|
|
72
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
73
|
+
const from = byName.get(name);
|
|
74
|
+
if (from && from.id !== to.id) {
|
|
75
|
+
edges.push({
|
|
76
|
+
from: from.id,
|
|
77
|
+
to: to.id,
|
|
78
|
+
reason: `${field}: ${name}`,
|
|
79
|
+
pinned: pinnable && EXACT.test(range),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return edges;
|
|
86
|
+
},
|
|
87
|
+
inputs: (ctx) => ({
|
|
88
|
+
global: [
|
|
89
|
+
"yarn.lock",
|
|
90
|
+
"package-lock.json",
|
|
91
|
+
"pnpm-lock.yaml",
|
|
92
|
+
"pnpm-workspace.yaml",
|
|
93
|
+
".yarnrc.yml",
|
|
94
|
+
".yarn/patches/**",
|
|
95
|
+
...(workspacePatterns(ctx) === undefined ? [] : ["package.json"]),
|
|
96
|
+
"tsconfig*.json",
|
|
97
|
+
"turbo.json",
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const phpPlugin: import("@tallyrelease/core").Plugin<unknown>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defaultUnitId, definePlugin } from "@tallyrelease/core";
|
|
2
|
+
const EXACT = /^(self\.version|=?v?\d+\.\d+\.\d+(\.\d+)?(-[0-9A-Za-z.-]+)?)$/;
|
|
3
|
+
const composerPath = (root) => root === "." ? "composer.json" : `${root}/composer.json`;
|
|
4
|
+
const readComposer = (ctx, root) => {
|
|
5
|
+
const raw = ctx.fs.read(composerPath(root));
|
|
6
|
+
return raw === undefined ? undefined : JSON.parse(raw);
|
|
7
|
+
};
|
|
8
|
+
/** Local `repositories[type=path]` url globs, repo-relative. */
|
|
9
|
+
const pathRepoGlobs = (composer) => {
|
|
10
|
+
const repos = Object.values((composer.repositories ?? {}));
|
|
11
|
+
return repos
|
|
12
|
+
.filter((r) => r.type === "path" && typeof r.url === "string")
|
|
13
|
+
.map((r) => (r.url ?? "").replace(/^\.\//, "").replace(/\/+$/, ""))
|
|
14
|
+
.filter((url) => url !== "" && !url.startsWith("../"));
|
|
15
|
+
};
|
|
16
|
+
export const phpPlugin = definePlugin({
|
|
17
|
+
apiVersion: 1,
|
|
18
|
+
name: "php",
|
|
19
|
+
discover: (ctx) => {
|
|
20
|
+
const rootComposer = readComposer(ctx, ".");
|
|
21
|
+
if (!rootComposer) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const roots = [
|
|
25
|
+
".",
|
|
26
|
+
...ctx.fs
|
|
27
|
+
.glob(pathRepoGlobs(rootComposer).map((u) => `${u}/composer.json`))
|
|
28
|
+
.map((p) => p.slice(0, -"/composer.json".length))
|
|
29
|
+
.filter((root) => root !== ""),
|
|
30
|
+
];
|
|
31
|
+
return roots.map((root) => {
|
|
32
|
+
const packageName = readComposer(ctx, root)?.name;
|
|
33
|
+
return {
|
|
34
|
+
id: defaultUnitId(root, ctx.repo.name),
|
|
35
|
+
root,
|
|
36
|
+
version: "tag",
|
|
37
|
+
meta: packageName === undefined ? {} : { packageName },
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
edges: (ctx, units) => {
|
|
42
|
+
const byName = new Map();
|
|
43
|
+
for (const unit of units) {
|
|
44
|
+
const name = unit.meta["php"]?.["packageName"];
|
|
45
|
+
if (unit.plugin === "php" && typeof name === "string") {
|
|
46
|
+
byName.set(name, unit);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const edges = [];
|
|
50
|
+
for (const to of byName.values()) {
|
|
51
|
+
const composer = readComposer(ctx, to.root) ?? {};
|
|
52
|
+
for (const field of ["require", "require-dev"]) {
|
|
53
|
+
const deps = (composer[field] ?? {});
|
|
54
|
+
for (const [name, constraint] of Object.entries(deps)) {
|
|
55
|
+
const from = byName.get(name);
|
|
56
|
+
if (from && from.id !== to.id) {
|
|
57
|
+
edges.push({
|
|
58
|
+
from: from.id,
|
|
59
|
+
to: to.id,
|
|
60
|
+
reason: `${field}: ${name}`,
|
|
61
|
+
pinned: field === "require" && EXACT.test(constraint),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return edges;
|
|
68
|
+
},
|
|
69
|
+
inputs: () => ({ global: ["composer.lock"] }),
|
|
70
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Env, GithubContext } from "./ci/github-env.js";
|
|
2
|
+
import type { Forge } from "./forge/forge.js";
|
|
3
|
+
import type { RepoGit } from "./git/reader.js";
|
|
4
|
+
import type { ReadonlyFs, Warning, Workspace } from "@tallyrelease/core";
|
|
5
|
+
export interface Writer {
|
|
6
|
+
write: (chunk: string) => unknown;
|
|
7
|
+
}
|
|
8
|
+
/** Injected environment of a cli run; `bin.ts` wires the real one. */
|
|
9
|
+
export interface Deps {
|
|
10
|
+
env: Env;
|
|
11
|
+
cwd: string;
|
|
12
|
+
stdout: Writer;
|
|
13
|
+
stderr: Writer;
|
|
14
|
+
/** Defaults to the `git` executable at the repo root. */
|
|
15
|
+
git?: RepoGit;
|
|
16
|
+
/** Defaults to `node:fs` at the repo root. */
|
|
17
|
+
fs?: ReadonlyFs;
|
|
18
|
+
/** Defaults to GitHub when a token is available; used only with a token. */
|
|
19
|
+
forge?: Forge;
|
|
20
|
+
}
|
|
21
|
+
export interface GlobalOptions {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
config: string;
|
|
24
|
+
token?: string;
|
|
25
|
+
verbose?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Resolved dependencies shared by all commands. */
|
|
28
|
+
export interface Session {
|
|
29
|
+
readonly cwd: string;
|
|
30
|
+
readonly root: string;
|
|
31
|
+
readonly env: Env;
|
|
32
|
+
readonly stdout: Writer;
|
|
33
|
+
readonly fs: ReadonlyFs;
|
|
34
|
+
readonly git: RepoGit;
|
|
35
|
+
readonly github: GithubContext | undefined;
|
|
36
|
+
/** Undefined without a token. */
|
|
37
|
+
readonly forge: Forge | undefined;
|
|
38
|
+
readonly workspace: () => Promise<Workspace>;
|
|
39
|
+
readonly warn: (warnings: readonly Warning[]) => void;
|
|
40
|
+
}
|
|
41
|
+
export declare const createSession: (deps: Deps, opts: GlobalOptions) => Session;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { loadWorkspace, TallyError } from "@tallyrelease/core";
|
|
2
|
+
import { basename, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { githubContext } from "./ci/github-env.js";
|
|
4
|
+
import { loadConfig } from "./config/load.js";
|
|
5
|
+
import { githubForge } from "./forge/github.js";
|
|
6
|
+
import { nodeFs } from "./fs-ctx.js";
|
|
7
|
+
import { git as runGit } from "./git/exec.js";
|
|
8
|
+
import { gitReader } from "./git/reader.js";
|
|
9
|
+
import { builtinPlugins } from "./plugins/index.js";
|
|
10
|
+
export const createSession = (deps, opts) => {
|
|
11
|
+
const root = resolve(deps.cwd, opts.cwd ?? ".");
|
|
12
|
+
const debug = opts.verbose
|
|
13
|
+
? (message) => deps.stderr.write(`debug: ${message}\n`)
|
|
14
|
+
: undefined;
|
|
15
|
+
const fs = deps.fs ?? nodeFs(root);
|
|
16
|
+
const github = githubContext(deps.env);
|
|
17
|
+
const token = opts.token ?? deps.env["GITHUB_TOKEN"];
|
|
18
|
+
const configPath = relative(root, resolve(root, opts.config))
|
|
19
|
+
.split(sep)
|
|
20
|
+
.join("/");
|
|
21
|
+
// Injected git or fs means there is no repository on disk to query.
|
|
22
|
+
const onDisk = deps.git === undefined && deps.fs === undefined;
|
|
23
|
+
const assertRepoRoot = async () => {
|
|
24
|
+
const result = await runGit(root, ["rev-parse", "--show-toplevel", "--show-prefix"], debug);
|
|
25
|
+
const [toplevel = "", prefix = ""] = result.stdout.split("\n");
|
|
26
|
+
if (result.code === 0 && prefix !== "") {
|
|
27
|
+
throw new TallyError("E_NOT_REPO_ROOT", `"${root}" is below the repository root "${toplevel}"; run from the root or pass --cwd`, { cwd: root, toplevel });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// GITHUB_REPOSITORY, else the origin url, else the directory name.
|
|
31
|
+
const repoName = async () => {
|
|
32
|
+
const fromEnv = deps.env["GITHUB_REPOSITORY"]?.split("/")[1];
|
|
33
|
+
if (fromEnv) {
|
|
34
|
+
return fromEnv;
|
|
35
|
+
}
|
|
36
|
+
if (onDisk) {
|
|
37
|
+
const origin = await runGit(root, ["remote", "get-url", "origin"], debug);
|
|
38
|
+
const name = origin.code === 0
|
|
39
|
+
? /([^/:]+?)(?:\.git)?\/*$/.exec(origin.stdout.trim())?.[1]
|
|
40
|
+
: undefined;
|
|
41
|
+
if (name) {
|
|
42
|
+
return name;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return basename(root);
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
cwd: deps.cwd,
|
|
49
|
+
root,
|
|
50
|
+
env: deps.env,
|
|
51
|
+
stdout: deps.stdout,
|
|
52
|
+
fs,
|
|
53
|
+
git: deps.git ?? gitReader(root, debug),
|
|
54
|
+
github,
|
|
55
|
+
forge: token
|
|
56
|
+
? (deps.forge ?? githubForge({ token, baseUrl: github?.apiUrl }))
|
|
57
|
+
: undefined,
|
|
58
|
+
workspace: async () => {
|
|
59
|
+
if (onDisk) {
|
|
60
|
+
await assertRepoRoot();
|
|
61
|
+
}
|
|
62
|
+
return await loadWorkspace({
|
|
63
|
+
config: loadConfig(fs, configPath),
|
|
64
|
+
configFile: configPath,
|
|
65
|
+
plugins: Object.values(builtinPlugins),
|
|
66
|
+
fs,
|
|
67
|
+
repo: { name: await repoName() },
|
|
68
|
+
...(debug && { debug }),
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
warn: (warnings) => {
|
|
72
|
+
for (const w of warnings) {
|
|
73
|
+
deps.stderr.write(`${w.code}: ${w.plugin ? `${w.plugin}: ` : ""}${w.message}\n`);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const version = "0.0.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const version = "0.0.0";
|