tegami 0.1.5 → 0.2.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/api-D-jf_8xY.js +116 -0
- package/dist/cli/index.d.ts +4 -4
- package/dist/cli/index.js +124 -133
- package/dist/{checks-Cwz-Ezkq.js → generate-BMlrn-2e.js} +18 -106
- package/dist/generators/simple.d.ts +1 -1
- package/dist/generators/simple.js +3 -3
- package/dist/{graph-DLKPUXSD.js → graph-DrzluXw8.js} +17 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +317 -210
- package/dist/plugins/git.d.ts +1 -1
- package/dist/plugins/git.js +47 -35
- package/dist/plugins/github.d.ts +28 -15
- package/dist/plugins/github.js +158 -142
- package/dist/providers/cargo.d.ts +2 -2
- package/dist/providers/cargo.js +86 -100
- package/dist/providers/npm.d.ts +2 -2
- package/dist/providers/npm.js +132 -129
- package/dist/schemas-B7N6EE2k.js +26 -0
- package/dist/{types-BXk2fMqa.d.ts → types-CurHqnWl.d.ts} +199 -226
- package/package.json +6 -7
- package/dist/schemas-CurBAaW5.js +0 -42
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/plugins/github/api.ts
|
|
2
|
+
function parseGitHubRepo(repo) {
|
|
3
|
+
const [owner, name] = repo.split("/", 2);
|
|
4
|
+
if (!owner || !name) throw new Error(`Invalid GitHub repository: ${repo}`);
|
|
5
|
+
return {
|
|
6
|
+
owner,
|
|
7
|
+
repo: name
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
async function githubRequest(path, token, init = {}) {
|
|
11
|
+
const headers = new Headers(init.headers);
|
|
12
|
+
headers.set("Accept", "application/vnd.github+json");
|
|
13
|
+
headers.set("X-GitHub-Api-Version", "2022-11-28");
|
|
14
|
+
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
15
|
+
return fetch(`https://api.github.com${path}`, {
|
|
16
|
+
...init,
|
|
17
|
+
headers
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function releaseExistsByTag(repo, tag, token) {
|
|
21
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
22
|
+
const response = await githubRequest(`/repos/${owner}/${name}/releases/tags/${encodeURIComponent(tag)}`, token, { method: "HEAD" });
|
|
23
|
+
if (response.status === 404) return false;
|
|
24
|
+
if (!response.ok) throw new Error(`Failed to get GitHub release for ${tag}.`);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
async function createRelease(repo, options) {
|
|
28
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
29
|
+
if (!(await githubRequest(`/repos/${owner}/${name}/releases`, options.token, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
tag_name: options.tag,
|
|
34
|
+
name: options.title,
|
|
35
|
+
body: options.notes,
|
|
36
|
+
prerelease: options.prerelease ?? false
|
|
37
|
+
})
|
|
38
|
+
})).ok) throw new Error(`Failed to create GitHub release for ${options.tag}.`);
|
|
39
|
+
}
|
|
40
|
+
async function findOpenPullRequest(repo, headBranch, token) {
|
|
41
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
42
|
+
const response = await githubRequest(`/repos/${owner}/${name}/pulls?${new URLSearchParams({
|
|
43
|
+
head: `${owner}:${headBranch}`,
|
|
44
|
+
state: "open"
|
|
45
|
+
})}`, token);
|
|
46
|
+
if (!response.ok) throw new Error("Failed to check for an existing version pull request.");
|
|
47
|
+
return (await response.json())[0]?.number;
|
|
48
|
+
}
|
|
49
|
+
async function updatePullRequest(repo, number, options) {
|
|
50
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
51
|
+
if (!(await githubRequest(`/repos/${owner}/${name}/pulls/${number}`, options.token, {
|
|
52
|
+
method: "PATCH",
|
|
53
|
+
headers: { "Content-Type": "application/json" },
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
title: options.title,
|
|
56
|
+
body: options.body
|
|
57
|
+
})
|
|
58
|
+
})).ok) throw new Error("Failed to update the version pull request.");
|
|
59
|
+
}
|
|
60
|
+
async function createPullRequest(repo, options) {
|
|
61
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
62
|
+
if (!(await githubRequest(`/repos/${owner}/${name}/pulls`, options.token, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
title: options.title,
|
|
67
|
+
body: options.body,
|
|
68
|
+
head: options.head,
|
|
69
|
+
base: options.base
|
|
70
|
+
})
|
|
71
|
+
})).ok) throw new Error("Failed to create the version pull request.");
|
|
72
|
+
}
|
|
73
|
+
async function getPullRequest(repo, number, token) {
|
|
74
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
75
|
+
const response = await githubRequest(`/repos/${owner}/${name}/pulls/${number}`, token);
|
|
76
|
+
if (!response.ok) throw new Error(`Failed to resolve pull request #${number}.`);
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
return {
|
|
79
|
+
headRefName: data.head.ref,
|
|
80
|
+
baseRefOid: data.base.sha,
|
|
81
|
+
headRefOid: data.head.sha,
|
|
82
|
+
headRepository: data.head.repo
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function findIssueCommentByPrefix(repo, issueNumber, prefix, token) {
|
|
86
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
87
|
+
let page = 1;
|
|
88
|
+
while (true) {
|
|
89
|
+
const response = await githubRequest(`/repos/${owner}/${name}/issues/${issueNumber}/comments?per_page=100&page=${page}`, token);
|
|
90
|
+
if (!response.ok) throw new Error("Failed to list pull request comments.");
|
|
91
|
+
const batch = await response.json();
|
|
92
|
+
if (batch.length === 0) break;
|
|
93
|
+
const comment = batch.find((comment) => comment.body.startsWith(prefix));
|
|
94
|
+
if (comment) return comment.id;
|
|
95
|
+
if (batch.length < 100) break;
|
|
96
|
+
page += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function updateIssueComment(repo, commentId, body, token) {
|
|
100
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
101
|
+
if (!(await githubRequest(`/repos/${owner}/${name}/issues/comments/${commentId}`, token, {
|
|
102
|
+
method: "PATCH",
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify({ body })
|
|
105
|
+
})).ok) throw new Error("Failed to update pull request comment.");
|
|
106
|
+
}
|
|
107
|
+
async function createIssueComment(repo, issueNumber, body, token) {
|
|
108
|
+
const { owner, repo: name } = parseGitHubRepo(repo);
|
|
109
|
+
if (!(await githubRequest(`/repos/${owner}/${name}/issues/${issueNumber}/comments`, token, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify({ body })
|
|
113
|
+
})).ok) throw new Error("Failed to create pull request comment.");
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { findOpenPullRequest as a, updateIssueComment as c, findIssueCommentByPrefix as i, updatePullRequest as l, createPullRequest as n, getPullRequest as o, createRelease as r, releaseExistsByTag as s, createIssueComment as t };
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as Draft, S as PublishPlan, g as Tegami, t as Awaitable } from "../types-CurHqnWl.js";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
|
|
4
4
|
//#region src/cli/index.d.ts
|
|
5
5
|
interface TegamiCLIOptions {
|
|
6
|
-
/** create a custom draft
|
|
7
|
-
version?: () => Awaitable<
|
|
8
|
-
publish?: () => Awaitable<
|
|
6
|
+
/** create a custom draft, it must not be applied */
|
|
7
|
+
version?: () => Awaitable<Draft>;
|
|
8
|
+
publish?: () => Awaitable<PublishPlan>;
|
|
9
9
|
}
|
|
10
10
|
declare function createCli(tegami: Tegami, options?: TegamiCLIOptions): Command;
|
|
11
11
|
//#endregion
|
package/dist/cli/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { r as formatNpmDistTag } from "../semver-C4vJ4SK8.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as renderChangelog, n as generateFromCommits, r as generateReplays, t as changelogFilename } from "../generate-BMlrn-2e.js";
|
|
3
3
|
import { a as isCI, n as execFailure, r as handlePluginError, t as CancelledError } from "../error-DNy8R5ue.js";
|
|
4
|
-
import {
|
|
4
|
+
import { c as updateIssueComment, i as findIssueCommentByPrefix, o as getPullRequest, t as createIssueComment } from "../api-D-jf_8xY.js";
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
6
|
import path, { basename, isAbsolute, join, normalize, relative } from "node:path";
|
|
6
7
|
import { x } from "tinyexec";
|
|
8
|
+
import z from "zod";
|
|
7
9
|
import { resolveCommand } from "package-manager-detector";
|
|
8
10
|
import { autocompleteMultiselect, confirm, intro, isCancel, multiline, note, outro, select, spinner } from "@clack/prompts";
|
|
9
11
|
import { Command, InvalidArgumentError } from "commander";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
12
|
//#region src/utils/git-changes.ts
|
|
12
13
|
async function getChangedPackages(graph, cwd) {
|
|
13
14
|
return resolveChangedPackages(graph, await getChangedFilePaths(cwd), cwd);
|
|
@@ -47,7 +48,7 @@ async function addGitOutput(files, cwd, args) {
|
|
|
47
48
|
//#region src/cli/changelog.ts
|
|
48
49
|
async function runChangelogTui(tegami) {
|
|
49
50
|
const context = await tegami._internal.context();
|
|
50
|
-
await
|
|
51
|
+
if (await tegami.publishStatus() === "pending") throw new Error(`Publish lock at ${context.lockPath} is still pending. Publish it before applying a new draft.`);
|
|
51
52
|
intro("Create changelogs");
|
|
52
53
|
let selectedPackages = [];
|
|
53
54
|
if (!isCI()) selectedPackages = await promptPackageSelection(context.graph, context.cwd);
|
|
@@ -63,7 +64,7 @@ async function runChangelogTui(tegami) {
|
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
await persistChangelogs(context, (await
|
|
67
|
+
await persistChangelogs(context, (await generateFromCommits(context)).map(({ filename, content, packages }) => ({
|
|
67
68
|
filename,
|
|
68
69
|
content,
|
|
69
70
|
packages
|
|
@@ -122,6 +123,7 @@ async function promptPackageSelection(graph, cwd) {
|
|
|
122
123
|
return useShortname.get(pkg.name) ? pkg.name : pkg.id;
|
|
123
124
|
};
|
|
124
125
|
const changedPackages = new Set(await getChangedPackages(graph, cwd));
|
|
126
|
+
const initialValues = /* @__PURE__ */ new Set();
|
|
125
127
|
const selectOptions = [];
|
|
126
128
|
const groups = [];
|
|
127
129
|
for (const group of graph.getGroups()) {
|
|
@@ -130,23 +132,27 @@ async function promptPackageSelection(graph, cwd) {
|
|
|
130
132
|
}
|
|
131
133
|
groups.sort((a, b) => (a[1] ? 0 : 1) - (b[1] ? 0 : 1));
|
|
132
134
|
for (const [group, changed] of groups) {
|
|
133
|
-
|
|
135
|
+
if (changed) initialValues.add(`group:${group.name}`);
|
|
134
136
|
selectOptions.push({
|
|
135
|
-
label: `(Group) ${group.name}
|
|
137
|
+
label: `(Group) ${group.name}` + (changed ? "*" : ""),
|
|
136
138
|
value: `group:${group.name}`,
|
|
137
|
-
hint:
|
|
139
|
+
hint: group.packages.map(getPackageLabel).join(", ")
|
|
138
140
|
});
|
|
139
141
|
}
|
|
140
142
|
const packages = graph.getPackages().toSorted((a, b) => (changedPackages.has(a) ? 0 : 1) - (changedPackages.has(b) ? 0 : 1));
|
|
141
|
-
for (const pkg of packages)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
for (const pkg of packages) {
|
|
144
|
+
const changed = changedPackages.has(pkg);
|
|
145
|
+
if (changed) initialValues.add(pkg.id);
|
|
146
|
+
selectOptions.push({
|
|
147
|
+
label: getPackageLabel(pkg) + (changed ? "*" : ""),
|
|
148
|
+
value: pkg.id
|
|
149
|
+
});
|
|
150
|
+
}
|
|
146
151
|
const selected = await autocompleteMultiselect({
|
|
147
152
|
message: "Select packages (leave empty to auto-generate from commits)",
|
|
148
153
|
required: false,
|
|
149
|
-
options: selectOptions
|
|
154
|
+
options: selectOptions,
|
|
155
|
+
initialValues: Array.from(initialValues)
|
|
150
156
|
});
|
|
151
157
|
if (isCancel(selected)) throw new CancelledError();
|
|
152
158
|
return selected;
|
|
@@ -221,7 +227,7 @@ async function initAgent(context, options = {}) {
|
|
|
221
227
|
}
|
|
222
228
|
function renderAgentsMd(context) {
|
|
223
229
|
const changelogDir = path.relative(context.cwd, context.changelogDir) || "project root";
|
|
224
|
-
const
|
|
230
|
+
const lockPath = path.relative(context.cwd, context.lockPath) || "project root";
|
|
225
231
|
return [
|
|
226
232
|
"# Release workflow",
|
|
227
233
|
"",
|
|
@@ -259,7 +265,7 @@ function renderAgentsMd(context) {
|
|
|
259
265
|
"- Include YAML frontmatter with `packages`",
|
|
260
266
|
"- Include at least one `#`, `##`, or `###` heading in the body",
|
|
261
267
|
"- Write user-facing release notes under each heading",
|
|
262
|
-
`- Do not edit \`${
|
|
268
|
+
`- Do not edit the publish lock file (\`${lockPath}\`) or package \`CHANGELOG.md\` files directly`,
|
|
263
269
|
""
|
|
264
270
|
].join("\n");
|
|
265
271
|
}
|
|
@@ -284,24 +290,20 @@ async function buildPrPreview(context, draft, options = {}) {
|
|
|
284
290
|
];
|
|
285
291
|
const pendingPackages = [];
|
|
286
292
|
for (const pkg of context.graph.getPackages()) {
|
|
287
|
-
const plan = draft.
|
|
293
|
+
const plan = draft.getPackageDraft(pkg.id);
|
|
288
294
|
if (!plan || plan.bumpVersion(pkg) === pkg.version) continue;
|
|
289
295
|
pendingPackages.push({
|
|
290
296
|
name: pkg.name,
|
|
291
297
|
type: plan.type ?? "—",
|
|
292
298
|
from: pkg.version,
|
|
293
299
|
to: plan.bumpVersion(pkg),
|
|
294
|
-
distTag: plan.npm?.distTag
|
|
295
|
-
publish: plan.publish ?? false
|
|
300
|
+
distTag: plan.npm?.distTag
|
|
296
301
|
});
|
|
297
302
|
}
|
|
298
303
|
const prChangelogs = draft.getChangelogs().filter((entry) => prChangelogFiles.has(entry.filename));
|
|
299
304
|
if (pendingPackages.length > 0) {
|
|
300
305
|
lines.push("#### Release preview", "", "| Package | Bump | Version |", "| --- | --- | --- |");
|
|
301
|
-
for (const { name, type, from, to, distTag
|
|
302
|
-
const publishNote = publish ? "" : " (no publish)";
|
|
303
|
-
lines.push(`| \`${name}\` | ${type} | \`${from}\` → \`${to}\`${formatNpmDistTag(distTag)}${publishNote} |`);
|
|
304
|
-
}
|
|
306
|
+
for (const { name, type, from, to, distTag } of pendingPackages) lines.push(`| \`${name}\` | ${type} | \`${from}\` → \`${to}\`${formatNpmDistTag(distTag)} |`);
|
|
305
307
|
lines.push("");
|
|
306
308
|
}
|
|
307
309
|
if (prChangelogs.length > 0) {
|
|
@@ -318,74 +320,69 @@ async function buildPrPreview(context, draft, options = {}) {
|
|
|
318
320
|
}
|
|
319
321
|
async function postPrComment(body) {
|
|
320
322
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
321
|
-
if (!repo)
|
|
322
|
-
|
|
323
|
+
if (!repo) {
|
|
324
|
+
outro("GITHUB_REPOSITORY is required.");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const pr = await readPullRequestFromWorkflowRunEvent();
|
|
328
|
+
if (!pr.found) {
|
|
329
|
+
outro(pr.reason);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
323
332
|
const markedBody = `${COMMENT_MARKER}\n${body}`;
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
`repos/${repo}/issues/${number}/comments`,
|
|
327
|
-
"--paginate",
|
|
328
|
-
"--jq",
|
|
329
|
-
`[.[] | select(.body | startswith("${COMMENT_MARKER}")) | .id][0] // empty`
|
|
330
|
-
]);
|
|
331
|
-
if (listResult.exitCode !== 0) throw execFailure("Failed to list pull request comments.", listResult);
|
|
332
|
-
const existingId = listResult.stdout.trim();
|
|
333
|
+
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
334
|
+
const existingId = await findIssueCommentByPrefix(repo, pr.number, COMMENT_MARKER, token);
|
|
333
335
|
if (existingId) {
|
|
334
|
-
|
|
335
|
-
const inputPath = join(dir, "body.json");
|
|
336
|
-
try {
|
|
337
|
-
await writeFile(inputPath, JSON.stringify({ body: markedBody }));
|
|
338
|
-
const updateResult = await x("gh", [
|
|
339
|
-
"api",
|
|
340
|
-
"-X",
|
|
341
|
-
"PATCH",
|
|
342
|
-
`repos/${repo}/issues/comments/${existingId}`,
|
|
343
|
-
"--input",
|
|
344
|
-
inputPath
|
|
345
|
-
]);
|
|
346
|
-
if (updateResult.exitCode !== 0) throw execFailure("Failed to update pull request comment.", updateResult);
|
|
347
|
-
} finally {
|
|
348
|
-
await rm(dir, {
|
|
349
|
-
recursive: true,
|
|
350
|
-
force: true
|
|
351
|
-
});
|
|
352
|
-
}
|
|
336
|
+
await updateIssueComment(repo, existingId, markedBody, token);
|
|
353
337
|
return;
|
|
354
338
|
}
|
|
355
|
-
|
|
356
|
-
"pr",
|
|
357
|
-
"comment",
|
|
358
|
-
String(number),
|
|
359
|
-
"--body",
|
|
360
|
-
markedBody,
|
|
361
|
-
"--repo",
|
|
362
|
-
repo
|
|
363
|
-
]);
|
|
364
|
-
if (createResult.exitCode !== 0) throw execFailure("Failed to create pull request comment.", createResult);
|
|
339
|
+
await createIssueComment(repo, pr.number, markedBody, token);
|
|
365
340
|
}
|
|
366
|
-
|
|
341
|
+
const eventSchema = z.looseObject({ workflow_run: z.looseObject({
|
|
342
|
+
event: z.literal("pull_request", { error: "The preview workflow was not triggered by a pull request." }),
|
|
343
|
+
conclusion: z.literal("success", { error: "The preview workflow did not complete successfully." }),
|
|
344
|
+
pull_requests: z.array(z.looseObject({ number: z.int() }), { error: "The preview workflow is not associated with a pull request." }).min(1)
|
|
345
|
+
}, { error: "A workflow_run event is required." }) });
|
|
346
|
+
async function readPullRequestFromWorkflowRunEvent() {
|
|
367
347
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
368
|
-
if (!eventPath)
|
|
369
|
-
|
|
348
|
+
if (!eventPath) return {
|
|
349
|
+
found: false,
|
|
350
|
+
reason: "GITHUB_EVENT_PATH is required."
|
|
351
|
+
};
|
|
352
|
+
let raw;
|
|
370
353
|
try {
|
|
371
|
-
|
|
354
|
+
raw = JSON.parse(await readFile(eventPath, "utf8"));
|
|
372
355
|
} catch {
|
|
373
|
-
|
|
356
|
+
return {
|
|
357
|
+
found: false,
|
|
358
|
+
reason: "Failed to read workflow_run event."
|
|
359
|
+
};
|
|
374
360
|
}
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
361
|
+
const { error, data } = eventSchema.safeParse(raw);
|
|
362
|
+
if (error) return {
|
|
363
|
+
found: false,
|
|
364
|
+
reason: z.prettifyError(error)
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
found: true,
|
|
368
|
+
number: data.workflow_run.pull_requests[0].number
|
|
369
|
+
};
|
|
382
370
|
}
|
|
383
371
|
async function resolvePullRequest(context, options) {
|
|
384
372
|
const repo = context.github?.repo ?? process.env.GITHUB_REPOSITORY;
|
|
385
373
|
if (!repo) throw new Error("GITHUB_REPOSITORY is required.");
|
|
374
|
+
const token = context.github?.token ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
375
|
+
if (!repo) throw new Error("GITHUB_TOKEN is required.");
|
|
386
376
|
if (options.number !== void 0) {
|
|
387
377
|
if (!Number.isInteger(options.number) || options.number <= 0) throw new Error("--number must be a positive integer.");
|
|
388
|
-
|
|
378
|
+
const data = await getPullRequest(repo, options.number, token);
|
|
379
|
+
return {
|
|
380
|
+
repo,
|
|
381
|
+
headRepo: data.headRepository ? `${data.headRepository.owner.login}/${data.headRepository.name}` : repo,
|
|
382
|
+
headRef: data.headRefName,
|
|
383
|
+
baseSha: data.baseRefOid,
|
|
384
|
+
headSha: data.headRefOid
|
|
385
|
+
};
|
|
389
386
|
}
|
|
390
387
|
const event = await readPullRequestEvent();
|
|
391
388
|
if (event) return {
|
|
@@ -394,26 +391,6 @@ async function resolvePullRequest(context, options) {
|
|
|
394
391
|
};
|
|
395
392
|
throw new Error("A pull request event or --number is required.");
|
|
396
393
|
}
|
|
397
|
-
async function readPullRequestFromGh(repo, number) {
|
|
398
|
-
const result = await x("gh", [
|
|
399
|
-
"pr",
|
|
400
|
-
"view",
|
|
401
|
-
String(number),
|
|
402
|
-
"--repo",
|
|
403
|
-
repo,
|
|
404
|
-
"--json",
|
|
405
|
-
"headRefName,baseRefOid,headRefOid,headRepository"
|
|
406
|
-
]);
|
|
407
|
-
if (result.exitCode !== 0) throw execFailure(`Failed to resolve pull request #${number}.`, result);
|
|
408
|
-
const data = JSON.parse(result.stdout);
|
|
409
|
-
return {
|
|
410
|
-
repo,
|
|
411
|
-
headRepo: data.headRepository ? `${data.headRepository.owner.login}/${data.headRepository.name}` : repo,
|
|
412
|
-
headRef: data.headRefName,
|
|
413
|
-
baseSha: data.baseRefOid,
|
|
414
|
-
headSha: data.headRefOid
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
394
|
async function readPullRequestEvent() {
|
|
418
395
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
419
396
|
if (!eventPath) return;
|
|
@@ -459,13 +436,17 @@ function createChangelogUrl(context, repo, branch, filename) {
|
|
|
459
436
|
function createCli(tegami, options = {}) {
|
|
460
437
|
const program = new Command();
|
|
461
438
|
program.name("tegami").description("create changelogs").action(() => runAction(tegami, () => runChangelogTui(tegami)));
|
|
462
|
-
program.command("version").description("draft and
|
|
439
|
+
program.command("version").description("draft version changes and write the publish lock").action(() => runAction(tegami, async () => {
|
|
463
440
|
await versionPackages(tegami, { cli: options });
|
|
464
441
|
}));
|
|
465
442
|
program.command("ci").description("version and publish packages").action(() => runAction(tegami, async () => {
|
|
466
443
|
if (await versionPackages(tegami, { cli: options })) return;
|
|
467
444
|
await publishPackages(tegami, { cli: options });
|
|
468
445
|
}));
|
|
446
|
+
program.command("check-publish").description("exit with code 1 if no publishing needed, otherwise 0").action(() => runAction(tegami, async () => {
|
|
447
|
+
const status = await tegami.publishStatus();
|
|
448
|
+
process.exit(status === "pending" ? 0 : 1);
|
|
449
|
+
}));
|
|
469
450
|
const programPr = program.command("pr");
|
|
470
451
|
programPr.command("preview").description("(should be executed in a GitHub action) show a pull request release preview and changelog guidance").option("--artifact <path>", "write preview markdown to a file").option("--number <number>", "pull request number", (value) => {
|
|
471
452
|
const number = Number(value);
|
|
@@ -500,13 +481,13 @@ function createCli(tegami, options = {}) {
|
|
|
500
481
|
process.exit(1);
|
|
501
482
|
}
|
|
502
483
|
});
|
|
503
|
-
program.command("publish").description("publish packages from the
|
|
484
|
+
program.command("publish").description("publish packages from the publish lock").option("--dry-run", "validate the publish lock without publishing packages").action((commandOptions) => runAction(tegami, async () => {
|
|
504
485
|
await publishPackages(tegami, {
|
|
505
486
|
...commandOptions,
|
|
506
487
|
cli: options
|
|
507
488
|
});
|
|
508
489
|
}));
|
|
509
|
-
program.command("cleanup").description("remove the publish
|
|
490
|
+
program.command("cleanup").description("remove the publish lock after all packages have been published").action(() => runAction(tegami, () => runCleanup(tegami)));
|
|
510
491
|
program.command("init-agent").description("write AGENTS.md with changelog instructions for AI agents").option("-o, --output <path>", "output path", "AGENTS.md").action((commandOptions) => runAction(tegami, () => runInitAgent(tegami, commandOptions)));
|
|
511
492
|
return program;
|
|
512
493
|
}
|
|
@@ -515,59 +496,69 @@ async function versionPackages(tegami, options) {
|
|
|
515
496
|
const { version: customVersion } = options.cli;
|
|
516
497
|
const context = await tegami._internal.context();
|
|
517
498
|
const draft = customVersion ? await customVersion() : await tegami.draft();
|
|
518
|
-
if (!draft.canApply()) throw new Error(`The draft
|
|
519
|
-
for (const plugin of context.plugins) await handlePluginError(plugin, "cli.
|
|
499
|
+
if (!draft.canApply()) throw new Error(`The draft from custom "version" hook must not be applied`);
|
|
500
|
+
for (const plugin of context.plugins) await handlePluginError(plugin, "cli.draftCreated", () => plugin.cli?.draftCreated?.call(context, draft));
|
|
520
501
|
if (!draft.hasPending()) {
|
|
521
502
|
note("No pending version changes matched workspace packages.", "Nothing to version");
|
|
522
503
|
outro("No versions changed.");
|
|
523
504
|
return false;
|
|
524
505
|
}
|
|
525
|
-
|
|
506
|
+
if (await tegami.publishStatus() === "pending") throw new Error(`Publish lock at ${context.lockPath} is still pending. Publish it before applying a new draft.`);
|
|
507
|
+
const lines = [];
|
|
526
508
|
for (const pkg of context.graph.getPackages()) {
|
|
527
|
-
const plan = draft.
|
|
509
|
+
const plan = draft.getPackageDraft(pkg.id);
|
|
528
510
|
if (!plan || plan.bumpVersion(pkg) === pkg.version) continue;
|
|
529
|
-
|
|
530
|
-
if (plan.bumpReasons) for (const reason of plan.bumpReasons)
|
|
511
|
+
lines.push(`${pkg.id}: ${pkg.version} → ${plan.bumpVersion(pkg)} (${plan.changelogs?.length ?? 0} changelogs)`);
|
|
512
|
+
if (plan.bumpReasons) for (const reason of plan.bumpReasons) lines.push(` - ${reason}`);
|
|
531
513
|
}
|
|
532
|
-
note(
|
|
514
|
+
note(lines.join("\n"), "Release plan");
|
|
533
515
|
const s = spinner();
|
|
534
516
|
s.start("Updating package versions");
|
|
535
517
|
try {
|
|
536
|
-
await draft.
|
|
518
|
+
await draft.apply();
|
|
537
519
|
} catch (error) {
|
|
538
|
-
s.stop("Failed to apply
|
|
520
|
+
s.stop("Failed to apply draft");
|
|
539
521
|
throw error;
|
|
540
522
|
}
|
|
541
523
|
s.stop("Package versions updated");
|
|
542
|
-
for (const plugin of context.plugins) await handlePluginError(plugin, "cli.
|
|
543
|
-
outro("Publish
|
|
524
|
+
for (const plugin of context.plugins) await handlePluginError(plugin, "cli.draftApplied", () => plugin.cli?.draftApplied?.call(context, draft));
|
|
525
|
+
outro("Publish lock written.");
|
|
544
526
|
return true;
|
|
545
527
|
}
|
|
546
528
|
async function publishPackages(tegami, options) {
|
|
547
529
|
const dryRun = options.dryRun ?? false;
|
|
530
|
+
const context = await tegami._internal.context();
|
|
548
531
|
const { publish: customPublish } = options.cli;
|
|
549
532
|
intro(dryRun ? "Publish packages (dry run)" : "Publish packages");
|
|
550
533
|
const s = spinner();
|
|
551
|
-
s.start(dryRun ? "Validating publish
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
outro(`No publishable packages were found in ${planPath}.`);
|
|
534
|
+
s.start(dryRun ? "Validating publish lock" : "Publishing packages");
|
|
535
|
+
const plan = customPublish ? await customPublish() : await tegami.publish({ dryRun });
|
|
536
|
+
if (plan === "skipped") {
|
|
537
|
+
s.stop(dryRun ? "No publish lock to validate" : "Nothing to publish");
|
|
538
|
+
outro(`No publishable packages were found in ${context.lockPath}.`);
|
|
557
539
|
return false;
|
|
558
540
|
}
|
|
559
|
-
s.stop(dryRun ? "Publish
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
541
|
+
s.stop(dryRun ? "Publish lock validated" : "Publish complete");
|
|
542
|
+
const lines = [];
|
|
543
|
+
let hasFailed = false;
|
|
544
|
+
for (const [id, packagePlan] of plan.packages) {
|
|
545
|
+
if (!packagePlan.updated) continue;
|
|
546
|
+
const result = packagePlan.publishResult;
|
|
547
|
+
const pkg = context.graph.get(id);
|
|
548
|
+
if (result.type === "failed") hasFailed = true;
|
|
549
|
+
let message = `${result.type} ${pkg.id} - ${pkg.version}`;
|
|
550
|
+
const distTag = packagePlan.npm?.distTag;
|
|
551
|
+
if (distTag) message += ` (npm dist-tag: ${distTag})`;
|
|
552
|
+
if (result.type === "failed" && result.error) message += `: ${result.error}`;
|
|
553
|
+
lines.push(message);
|
|
554
|
+
}
|
|
555
|
+
note(lines.join("\n"), dryRun ? "Publish dry run" : "Publish result");
|
|
556
|
+
if (hasFailed) {
|
|
566
557
|
process.exitCode = 1;
|
|
567
|
-
outro("
|
|
558
|
+
outro("Failed to publish.");
|
|
568
559
|
return false;
|
|
569
560
|
}
|
|
570
|
-
outro(dryRun ? "Publish
|
|
561
|
+
outro(dryRun ? "Publish lock is valid." : "Packages published.");
|
|
571
562
|
return true;
|
|
572
563
|
}
|
|
573
564
|
async function runInitAgent(tegami, options) {
|
|
@@ -581,21 +572,21 @@ async function runInitAgent(tegami, options) {
|
|
|
581
572
|
outro("Agents can follow AGENTS.md to write changelogs.");
|
|
582
573
|
}
|
|
583
574
|
async function runCleanup(tegami) {
|
|
584
|
-
intro("Cleanup publish
|
|
575
|
+
intro("Cleanup publish lock");
|
|
585
576
|
const s = spinner();
|
|
586
|
-
s.start("Checking publish
|
|
577
|
+
s.start("Checking publish lock status");
|
|
587
578
|
const result = await tegami.cleanup();
|
|
588
|
-
const {
|
|
589
|
-
s.stop(result.state === "removed" ? "Publish
|
|
579
|
+
const { lockPath } = await tegami._internal.context();
|
|
580
|
+
s.stop(result.state === "removed" ? "Publish lock removed" : "Publish lock kept");
|
|
590
581
|
if (result.state === "removed") {
|
|
591
|
-
outro(`Removed ${
|
|
582
|
+
outro(`Removed ${lockPath}.`);
|
|
592
583
|
return;
|
|
593
584
|
}
|
|
594
|
-
if (result.reason === "
|
|
595
|
-
outro(`No publish
|
|
585
|
+
if (result.reason === "no-plan") {
|
|
586
|
+
outro(`No publish lock found at ${lockPath}.`);
|
|
596
587
|
return;
|
|
597
588
|
}
|
|
598
|
-
outro(`Publish
|
|
589
|
+
outro(`Publish lock at ${lockPath} is still pending. Publish it before cleanup.`);
|
|
599
590
|
}
|
|
600
591
|
async function runAction(tegami, action) {
|
|
601
592
|
try {
|