@pob/version 5.2.0 → 6.0.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/LICENSE +21 -15
- package/README.md +147 -10
- package/dist/index-node.mjs +1482 -0
- package/dist/index-node.mjs.map +1 -0
- package/package.json +78 -19
- package/src/commands/version.test.ts +120 -0
- package/src/commands/version.ts +956 -0
- package/src/index.ts +13 -0
- package/src/missing-types.d.ts +10 -0
- package/src/pm/BunPackageManager.ts +29 -0
- package/src/pm/PackageManager.ts +90 -0
- package/src/pm/PnpmPackageManager.ts +23 -0
- package/src/pm/YarnPackageManager.ts +44 -0
- package/src/utils/UsageError.ts +6 -0
- package/src/utils/bumpTypeUtils.test.ts +65 -0
- package/src/utils/bumpTypeUtils.ts +111 -0
- package/src/utils/conventionalChangelogUtils.test.ts +762 -0
- package/src/utils/conventionalChangelogUtils.ts +104 -0
- package/src/utils/conventionalCommitConfigUtils.ts +19 -0
- package/src/utils/execCommand.ts +214 -0
- package/src/utils/generatorUtils.ts +11 -0
- package/src/utils/gitUtils.ts +193 -0
- package/src/utils/githubUtils.ts +120 -0
- package/src/utils/mapUtils.ts +7 -0
- package/src/utils/mapWorkspaces.test.ts +89 -0
- package/src/utils/packageDependenciesUtils.test.ts +25 -0
- package/src/utils/packageDependenciesUtils.ts +80 -0
- package/src/utils/packageUtils.test.ts +32 -0
- package/src/utils/packageUtils.ts +129 -0
- package/src/utils/updateChangelog.ts +39 -0
- package/src/utils/workspaceUtils.ts +142 -0
- package/.editorconfig +0 -13
- package/.eslintrc.json +0 -7
- package/CHANGELOG.md +0 -125
- package/generate-index.js +0 -13
- package/lib/index.d.ts +0 -8
- package/lib/index.js +0 -5
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { writeChangelogString } from "conventional-changelog-writer";
|
|
2
|
+
import { parseCommits as createConventionalCommitsParser } from "conventional-commits-parser";
|
|
3
|
+
import type { Commit, ParserStreamOptions } from "conventional-commits-parser";
|
|
4
|
+
import type { PackageJson } from "type-fest";
|
|
5
|
+
import type { ConventionalChangelogConfig } from "./conventionalCommitConfigUtils.ts";
|
|
6
|
+
import type { Workspace } from "./packageUtils.ts";
|
|
7
|
+
|
|
8
|
+
export interface WhatBump {
|
|
9
|
+
level: 0 | 1 | 2;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
export type WhatBumpResult = WhatBump | null | undefined;
|
|
13
|
+
export interface BumperRecommendation extends WhatBump {
|
|
14
|
+
releaseType: "major" | "minor" | "patch";
|
|
15
|
+
commits: Commit[];
|
|
16
|
+
}
|
|
17
|
+
export interface EmptyBumperRecommendation {
|
|
18
|
+
commits: Commit[];
|
|
19
|
+
}
|
|
20
|
+
export type BumperRecommendationResult =
|
|
21
|
+
| BumperRecommendation
|
|
22
|
+
| EmptyBumperRecommendation;
|
|
23
|
+
|
|
24
|
+
export interface Preset {
|
|
25
|
+
whatBump: (commits: Commit[]) => Promise<WhatBumpResult> | WhatBumpResult;
|
|
26
|
+
tags?: unknown;
|
|
27
|
+
commits?: unknown;
|
|
28
|
+
parser?: ParserStreamOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const versions: BumperRecommendation["releaseType"][] = [
|
|
32
|
+
"major",
|
|
33
|
+
"minor",
|
|
34
|
+
"patch",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const recommendBump = async (
|
|
38
|
+
commits: Parameters<ConventionalChangelogConfig["whatBump"]>[0],
|
|
39
|
+
config: ConventionalChangelogConfig,
|
|
40
|
+
): Promise<Partial<BumperRecommendation>> => {
|
|
41
|
+
const whatBump = config.whatBump;
|
|
42
|
+
if (!whatBump) {
|
|
43
|
+
throw new Error("whatBump method is missing in config");
|
|
44
|
+
}
|
|
45
|
+
const result = { ...(await whatBump(commits)) };
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...result,
|
|
49
|
+
releaseType: result.level != null ? versions[result.level] : undefined,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type Commits = Parameters<ConventionalChangelogConfig["whatBump"]>[0];
|
|
54
|
+
|
|
55
|
+
export const generateChangelog = (
|
|
56
|
+
workspace: Workspace,
|
|
57
|
+
pkg: PackageJson,
|
|
58
|
+
config: ConventionalChangelogConfig,
|
|
59
|
+
previousTag: string | null,
|
|
60
|
+
newTag: string | null,
|
|
61
|
+
commits: Commits | undefined,
|
|
62
|
+
date: string,
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
64
|
+
): Promise<string> => {
|
|
65
|
+
if (!newTag) {
|
|
66
|
+
throw new Error(`Missing new tag for package "${pkg.name ?? ""}"`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const originUrl =
|
|
70
|
+
typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url;
|
|
71
|
+
const match =
|
|
72
|
+
originUrl &&
|
|
73
|
+
typeof originUrl === "string" &&
|
|
74
|
+
/^(?:git@|https?:\/\/)(?:([^./:]+(?:\.com)?)[/:])?([^/:]+)\/([^./:]+)(?:.git)?/.exec(
|
|
75
|
+
originUrl,
|
|
76
|
+
);
|
|
77
|
+
const [, gitHost, gitAccount, repoName] = match || [
|
|
78
|
+
undefined,
|
|
79
|
+
undefined,
|
|
80
|
+
undefined,
|
|
81
|
+
undefined,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return writeChangelogString(
|
|
85
|
+
commits ?? [],
|
|
86
|
+
{
|
|
87
|
+
// @ts-expect-error - missing types
|
|
88
|
+
previousTag,
|
|
89
|
+
currentTag: newTag,
|
|
90
|
+
linkCompare: previousTag != null,
|
|
91
|
+
version: pkg.version,
|
|
92
|
+
host: gitHost ? `https://${gitHost}` : undefined,
|
|
93
|
+
owner: gitAccount,
|
|
94
|
+
repository: repoName,
|
|
95
|
+
date,
|
|
96
|
+
},
|
|
97
|
+
// @ts-expect-error - missing types
|
|
98
|
+
config.writer,
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const createCommitsParser = (config: ConventionalChangelogConfig) => {
|
|
103
|
+
return createConventionalCommitsParser(config.parser);
|
|
104
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadPreset } from "conventional-changelog-preset-loader";
|
|
2
|
+
import { UsageError } from "./UsageError.ts";
|
|
3
|
+
import type { Preset } from "./conventionalChangelogUtils.ts";
|
|
4
|
+
import type { Workspace } from "./packageUtils.ts";
|
|
5
|
+
|
|
6
|
+
export type ConventionalChangelogConfig = Preset;
|
|
7
|
+
|
|
8
|
+
export const loadConventionalCommitConfig = async (
|
|
9
|
+
rootWorkspace: Workspace,
|
|
10
|
+
preset: string,
|
|
11
|
+
): Promise<ConventionalChangelogConfig> => {
|
|
12
|
+
try {
|
|
13
|
+
return await loadPreset<ConventionalChangelogConfig>(preset);
|
|
14
|
+
} catch (error: any) {
|
|
15
|
+
throw new UsageError(
|
|
16
|
+
`Failed to require preset "${preset}": ${error.message as string}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import type { Workspace } from "./packageUtils.ts";
|
|
3
|
+
|
|
4
|
+
interface ExecResultIfFailed {
|
|
5
|
+
code: number | null;
|
|
6
|
+
signal: string | null;
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ExecResultIfSuccess {
|
|
12
|
+
code: null;
|
|
13
|
+
signal: null;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ExecResult<Strict extends boolean> = Strict extends true
|
|
19
|
+
? ExecResultIfSuccess
|
|
20
|
+
: ExecResultIfFailed;
|
|
21
|
+
|
|
22
|
+
async function execvp<const Strict extends boolean>(
|
|
23
|
+
command: string,
|
|
24
|
+
args: string[],
|
|
25
|
+
{
|
|
26
|
+
cwd = process.cwd(),
|
|
27
|
+
env = process.env,
|
|
28
|
+
encoding,
|
|
29
|
+
strict,
|
|
30
|
+
stdo = "pipe",
|
|
31
|
+
input,
|
|
32
|
+
}: {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
env?: typeof process.env;
|
|
35
|
+
encoding?: BufferEncoding;
|
|
36
|
+
strict?: Strict;
|
|
37
|
+
stdo?: "inherit" | "pipe";
|
|
38
|
+
input?: string;
|
|
39
|
+
},
|
|
40
|
+
): Promise<ExecResult<Strict>> {
|
|
41
|
+
const stdoutChunks: Uint8Array[] = [];
|
|
42
|
+
const stderrChunks: Uint8Array[] = [];
|
|
43
|
+
if (env.PWD !== undefined) {
|
|
44
|
+
env = { ...env, PWD: cwd };
|
|
45
|
+
}
|
|
46
|
+
const subprocess = childProcess.spawn(command, args, {
|
|
47
|
+
cwd,
|
|
48
|
+
env,
|
|
49
|
+
stdio: [input ? "pipe" : "ignore", stdo, stdo],
|
|
50
|
+
});
|
|
51
|
+
if (input && subprocess.stdin) {
|
|
52
|
+
// write input to stdin and close it
|
|
53
|
+
subprocess.stdin.write(input);
|
|
54
|
+
subprocess.stdin.end();
|
|
55
|
+
}
|
|
56
|
+
subprocess.stdout?.on("data", (chunk) => {
|
|
57
|
+
stdoutChunks.push(chunk);
|
|
58
|
+
});
|
|
59
|
+
subprocess.stderr?.on("data", (chunk) => {
|
|
60
|
+
stderrChunks.push(chunk);
|
|
61
|
+
});
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
subprocess.on("error", (err) => {
|
|
64
|
+
reject(new Error(`Process ${command} failed to spawn`));
|
|
65
|
+
});
|
|
66
|
+
subprocess.on("close", (code, signal) => {
|
|
67
|
+
const chunksToString = (chunks: Uint8Array[]): string =>
|
|
68
|
+
stdo === "inherit"
|
|
69
|
+
? ""
|
|
70
|
+
: Buffer.concat(chunks).toString(encoding ?? "utf8");
|
|
71
|
+
const stdout = chunksToString(stdoutChunks);
|
|
72
|
+
const stderr = chunksToString(stderrChunks);
|
|
73
|
+
if (code === 0 || !strict) {
|
|
74
|
+
resolve({
|
|
75
|
+
code,
|
|
76
|
+
signal,
|
|
77
|
+
stdout,
|
|
78
|
+
stderr,
|
|
79
|
+
} as ExecResult<Strict>);
|
|
80
|
+
} else {
|
|
81
|
+
reject(
|
|
82
|
+
new Error(
|
|
83
|
+
`Process ${[command, ...args].join(" ")} exited ${code !== null ? `with code ${code}` : `with signal ${signal || ""}`}:\nstdout: ${stdout}\nstderr: ${stderr}`,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function* spawnStreamStdout<const Strict extends boolean>(
|
|
92
|
+
command: string,
|
|
93
|
+
args: string[],
|
|
94
|
+
{
|
|
95
|
+
cwd = process.cwd(),
|
|
96
|
+
env = process.env,
|
|
97
|
+
encoding,
|
|
98
|
+
strict,
|
|
99
|
+
separator,
|
|
100
|
+
}: {
|
|
101
|
+
cwd?: string;
|
|
102
|
+
env?: typeof process.env;
|
|
103
|
+
encoding?: BufferEncoding;
|
|
104
|
+
strict?: Strict;
|
|
105
|
+
separator: string;
|
|
106
|
+
},
|
|
107
|
+
): AsyncGenerator<string, ExecResult<Strict>, unknown> {
|
|
108
|
+
const stderrChunks: Uint8Array[] = [];
|
|
109
|
+
if (env.PWD !== undefined) {
|
|
110
|
+
env = { ...env, PWD: cwd };
|
|
111
|
+
}
|
|
112
|
+
const subprocess = childProcess.spawn(command, args, {
|
|
113
|
+
cwd,
|
|
114
|
+
env,
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
subprocess.stderr.on("data", (chunk) => {
|
|
119
|
+
stderrChunks.push(chunk);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
let chunk: Buffer;
|
|
123
|
+
let payload: string[];
|
|
124
|
+
let buffer = "";
|
|
125
|
+
|
|
126
|
+
for await (chunk of subprocess.stdout) {
|
|
127
|
+
buffer += chunk.toString();
|
|
128
|
+
|
|
129
|
+
if (buffer.includes(separator)) {
|
|
130
|
+
payload = buffer.split(separator);
|
|
131
|
+
buffer = payload.pop() || "";
|
|
132
|
+
|
|
133
|
+
yield* payload;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (buffer) {
|
|
138
|
+
yield buffer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
subprocess.on("error", (err) => {
|
|
143
|
+
reject(new Error(`Process ${command} failed to spawn`));
|
|
144
|
+
});
|
|
145
|
+
subprocess.on("close", (code, signal) => {
|
|
146
|
+
const chunksToString = (chunks: Uint8Array[]): string =>
|
|
147
|
+
Buffer.concat(chunks).toString(encoding ?? "utf8");
|
|
148
|
+
const stderr = chunksToString(stderrChunks);
|
|
149
|
+
if (code === 0 || !strict) {
|
|
150
|
+
resolve({
|
|
151
|
+
code,
|
|
152
|
+
signal,
|
|
153
|
+
stderr,
|
|
154
|
+
} as ExecResult<Strict>);
|
|
155
|
+
} else {
|
|
156
|
+
reject(
|
|
157
|
+
new Error(
|
|
158
|
+
`Process ${[command, ...args].join(" ")} exited ${code !== null ? `with code ${code}` : `with signal ${signal || ""}`}:\nstderr: ${stderr}`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const execCommand = (
|
|
167
|
+
workspace: Workspace,
|
|
168
|
+
commandAndArgs: string[],
|
|
169
|
+
stdo: "inherit" | "pipe" = "pipe",
|
|
170
|
+
): ReturnType<typeof execvp> => {
|
|
171
|
+
const [command, ...args] = commandAndArgs;
|
|
172
|
+
if (command === undefined) {
|
|
173
|
+
throw new Error("Command is required");
|
|
174
|
+
}
|
|
175
|
+
return execvp(command, args, {
|
|
176
|
+
cwd: workspace.cwd,
|
|
177
|
+
strict: true,
|
|
178
|
+
stdo,
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const execCommandWithInput = (
|
|
183
|
+
workspace: Workspace,
|
|
184
|
+
commandAndArgs: string[],
|
|
185
|
+
input: string,
|
|
186
|
+
stdo: "inherit" | "pipe" = "pipe",
|
|
187
|
+
): ReturnType<typeof execvp> => {
|
|
188
|
+
const [command, ...args] = commandAndArgs;
|
|
189
|
+
if (command === undefined) {
|
|
190
|
+
throw new Error("Command is required");
|
|
191
|
+
}
|
|
192
|
+
return execvp(command, args, {
|
|
193
|
+
cwd: workspace.cwd,
|
|
194
|
+
strict: true,
|
|
195
|
+
stdo,
|
|
196
|
+
input,
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const execCommandStreamStdout = (
|
|
201
|
+
workspace: Workspace,
|
|
202
|
+
commandAndArgs: string[],
|
|
203
|
+
separator = "\n",
|
|
204
|
+
): AsyncGenerator<string, ExecResult<true>, unknown> => {
|
|
205
|
+
const [command, ...args] = commandAndArgs;
|
|
206
|
+
if (command === undefined) {
|
|
207
|
+
throw new Error("Command is required");
|
|
208
|
+
}
|
|
209
|
+
return spawnStreamStdout(command, args, {
|
|
210
|
+
cwd: workspace.cwd,
|
|
211
|
+
strict: true,
|
|
212
|
+
separator,
|
|
213
|
+
});
|
|
214
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
import { UsageError } from "./UsageError.ts";
|
|
3
|
+
import { execCommand, execCommandStreamStdout } from "./execCommand.ts";
|
|
4
|
+
import type { Workspace } from "./packageUtils.ts";
|
|
5
|
+
|
|
6
|
+
export const getGitCurrentBranch = async (
|
|
7
|
+
workspace: Workspace,
|
|
8
|
+
): Promise<string> => {
|
|
9
|
+
const { stdout } = await execCommand(workspace, [
|
|
10
|
+
"git",
|
|
11
|
+
"rev-parse",
|
|
12
|
+
"--abbrev-ref",
|
|
13
|
+
"HEAD",
|
|
14
|
+
]);
|
|
15
|
+
const currentBranch = stdout.trim();
|
|
16
|
+
|
|
17
|
+
if (currentBranch === "HEAD") {
|
|
18
|
+
throw new UsageError("HEAD is detached. Please checkout a branch.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return currentBranch;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const createGitCommit = async (
|
|
25
|
+
workspace: Workspace,
|
|
26
|
+
commitMessage: string,
|
|
27
|
+
): Promise<void> => {
|
|
28
|
+
await execCommand(workspace, ["git", "add", "-A"]);
|
|
29
|
+
|
|
30
|
+
await execCommand(workspace, [
|
|
31
|
+
"git",
|
|
32
|
+
"commit",
|
|
33
|
+
"--no-verify",
|
|
34
|
+
"-m",
|
|
35
|
+
commitMessage,
|
|
36
|
+
]);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const createGitTag = async (
|
|
40
|
+
workspace: Workspace,
|
|
41
|
+
newTag: string,
|
|
42
|
+
): Promise<void> => {
|
|
43
|
+
await execCommand(workspace, ["git", "tag", newTag, "-m", newTag]);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const pushCommitsAndTags = async (
|
|
47
|
+
workspace: Workspace,
|
|
48
|
+
gitRemote: string,
|
|
49
|
+
currentBranch: string,
|
|
50
|
+
): Promise<void> => {
|
|
51
|
+
await execCommand(workspace, [
|
|
52
|
+
"git",
|
|
53
|
+
"push",
|
|
54
|
+
"--follow-tags",
|
|
55
|
+
"--no-verify",
|
|
56
|
+
"--atomic",
|
|
57
|
+
gitRemote,
|
|
58
|
+
currentBranch,
|
|
59
|
+
]);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const isBehindRemote = async (
|
|
63
|
+
workspace: Workspace,
|
|
64
|
+
gitRemote: string,
|
|
65
|
+
currentBranch: string,
|
|
66
|
+
): Promise<boolean> => {
|
|
67
|
+
await execCommand(workspace, ["git", "remote", "update", gitRemote]);
|
|
68
|
+
const { stdout } = await execCommand(workspace, [
|
|
69
|
+
"git",
|
|
70
|
+
"rev-list",
|
|
71
|
+
"--left-right",
|
|
72
|
+
"--count",
|
|
73
|
+
`${gitRemote}/${currentBranch}..${currentBranch}`,
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const [behind] = stdout.split("\t").map((val) => parseInt(val, 10));
|
|
77
|
+
|
|
78
|
+
return behind ? behind > 0 : false;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const getDirtyFiles = async (workspace: Workspace): Promise<string> => {
|
|
82
|
+
const { stdout: dirtyFiles } = await execCommand(workspace, [
|
|
83
|
+
"git",
|
|
84
|
+
"status",
|
|
85
|
+
"--porcelain",
|
|
86
|
+
]);
|
|
87
|
+
return dirtyFiles;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const toArray = <T>(input: T | T[]): T[] =>
|
|
91
|
+
Array.isArray(input) ? input : [input];
|
|
92
|
+
|
|
93
|
+
export interface GitTagVersion {
|
|
94
|
+
tag: string;
|
|
95
|
+
version: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const getGitLatestTagVersion = async (
|
|
99
|
+
workspace: Workspace,
|
|
100
|
+
{
|
|
101
|
+
prefix,
|
|
102
|
+
skipUnstable,
|
|
103
|
+
since,
|
|
104
|
+
from = "",
|
|
105
|
+
to = "HEAD",
|
|
106
|
+
path,
|
|
107
|
+
}: {
|
|
108
|
+
prefix?: string;
|
|
109
|
+
skipUnstable?: boolean;
|
|
110
|
+
since?: Date | string;
|
|
111
|
+
from?: string;
|
|
112
|
+
to?: string;
|
|
113
|
+
path?: string[] | string;
|
|
114
|
+
} = {},
|
|
115
|
+
): Promise<GitTagVersion | null> => {
|
|
116
|
+
// eslint-disable-next-line regexp/no-super-linear-backtracking
|
|
117
|
+
const tagRegex = /tag:\s*(.+?)[,)]/gi;
|
|
118
|
+
for await (const chunk of execCommandStreamStdout(workspace, [
|
|
119
|
+
"git",
|
|
120
|
+
"log",
|
|
121
|
+
"--no-color",
|
|
122
|
+
"--date-order",
|
|
123
|
+
"--format=%d",
|
|
124
|
+
...(since
|
|
125
|
+
? [`--since=${since instanceof Date ? since.toISOString() : since}`]
|
|
126
|
+
: []),
|
|
127
|
+
[from, to].filter(Boolean).join(".."),
|
|
128
|
+
...(path ? ["--", ...toArray(path)] : []),
|
|
129
|
+
])) {
|
|
130
|
+
const trimmed = chunk.trim();
|
|
131
|
+
if (!trimmed) continue;
|
|
132
|
+
const matches = trimmed.matchAll(tagRegex);
|
|
133
|
+
for (const match of matches) {
|
|
134
|
+
const tag = match[1];
|
|
135
|
+
if (!tag) continue;
|
|
136
|
+
if (prefix && !tag.startsWith(prefix)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const version = prefix ? tag.slice(prefix.length) : tag;
|
|
140
|
+
if (!semver.valid(version)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (skipUnstable && semver.prerelease(version)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
return { tag, version };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const GIT_COMMIT_SEPARATOR =
|
|
154
|
+
"------------------------ >8 ------------------------";
|
|
155
|
+
|
|
156
|
+
export const getGitCommits = (
|
|
157
|
+
workspace: Workspace,
|
|
158
|
+
{
|
|
159
|
+
path,
|
|
160
|
+
from = "",
|
|
161
|
+
to = "HEAD",
|
|
162
|
+
format = "%B",
|
|
163
|
+
reverse,
|
|
164
|
+
merges,
|
|
165
|
+
since,
|
|
166
|
+
}: {
|
|
167
|
+
format?: string;
|
|
168
|
+
path?: string[] | string;
|
|
169
|
+
from?: string;
|
|
170
|
+
to?: string;
|
|
171
|
+
reverse?: boolean;
|
|
172
|
+
merges?: boolean;
|
|
173
|
+
since?: Date | string;
|
|
174
|
+
} = {},
|
|
175
|
+
) => {
|
|
176
|
+
return execCommandStreamStdout(
|
|
177
|
+
workspace,
|
|
178
|
+
[
|
|
179
|
+
"git",
|
|
180
|
+
"log",
|
|
181
|
+
`--format=${format}%n${GIT_COMMIT_SEPARATOR}`,
|
|
182
|
+
...(since
|
|
183
|
+
? [`--since=${since instanceof Date ? since.toISOString() : since}`]
|
|
184
|
+
: []),
|
|
185
|
+
...(reverse ? ["--reverse"] : []),
|
|
186
|
+
...(merges ? ["--merges"] : []),
|
|
187
|
+
...(merges === false ? ["--no-merges"] : []),
|
|
188
|
+
[from, to].filter(Boolean).join(".."),
|
|
189
|
+
...(path ? ["--", ...toArray(path)] : []),
|
|
190
|
+
],
|
|
191
|
+
GIT_COMMIT_SEPARATOR,
|
|
192
|
+
);
|
|
193
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { UsageError } from "./UsageError.ts";
|
|
2
|
+
import { execCommandWithInput } from "./execCommand.ts";
|
|
3
|
+
import type { Workspace } from "./packageUtils.ts";
|
|
4
|
+
|
|
5
|
+
export async function ensureGhCliAvailable(
|
|
6
|
+
workspace: Workspace,
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const { GH_TOKEN } = process.env;
|
|
9
|
+
|
|
10
|
+
if (!GH_TOKEN) {
|
|
11
|
+
throw new UsageError('"GH_TOKEN" environment variable required');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Use existing execCommand helper pattern by spawning gh --version
|
|
16
|
+
// We import execCommand lazily to avoid cycles when utils are used elsewhere
|
|
17
|
+
const { execCommand } = await import("./execCommand.ts");
|
|
18
|
+
await execCommand(workspace, ["gh", "--version"]);
|
|
19
|
+
} catch {
|
|
20
|
+
throw new UsageError(
|
|
21
|
+
'The "gh" CLI is required. Please install GitHub CLI: https://cli.github.com/',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ParsedGithubUrl {
|
|
27
|
+
username: string;
|
|
28
|
+
reponame: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const githubRegex = /^https?:\/\/github.com\/([^#/]+)\/([^#/]+?)(?:\.git)?$/;
|
|
32
|
+
export const parseGithubRepoUrl = (workspace: Workspace): ParsedGithubUrl => {
|
|
33
|
+
const repository = workspace.pkg.repository;
|
|
34
|
+
|
|
35
|
+
const url: string | undefined =
|
|
36
|
+
typeof repository === "string" ? repository : repository?.url;
|
|
37
|
+
|
|
38
|
+
if (!url) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"No repository URL found in manifest. Please add one and try again. https://docs.npmjs.com/cli/v9/configuring-npm/package-json#repository",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const match = githubRegex.exec(url);
|
|
44
|
+
|
|
45
|
+
if (!match) {
|
|
46
|
+
throw new Error(`Invalid GitHub repository URL: "${url}"`);
|
|
47
|
+
}
|
|
48
|
+
const [, username, reponame] = match;
|
|
49
|
+
if (!username || !reponame) {
|
|
50
|
+
throw new Error(`Invalid GitHub repository URL: "${url}"`);
|
|
51
|
+
}
|
|
52
|
+
return { username, reponame };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const extractHostnameFromGheApiUrl = (gheApiUrl: string): string | null => {
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(gheApiUrl);
|
|
58
|
+
return url.hostname || null;
|
|
59
|
+
} catch {
|
|
60
|
+
// fallback: try to strip protocol and path
|
|
61
|
+
const m = /^https?:\/\/([^/]+)/.exec(gheApiUrl);
|
|
62
|
+
return m?.[1] || null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const createGhRelease = async (
|
|
67
|
+
workspace: Workspace,
|
|
68
|
+
opts: {
|
|
69
|
+
parsedRepoUrl: ParsedGithubUrl;
|
|
70
|
+
tag: string;
|
|
71
|
+
title: string; // title is not necessary but slack release bot shows "undefined" if not provided
|
|
72
|
+
body: string;
|
|
73
|
+
prerelease: boolean;
|
|
74
|
+
},
|
|
75
|
+
): Promise<void> => {
|
|
76
|
+
const { parsedRepoUrl, tag, title, body, prerelease } = opts;
|
|
77
|
+
|
|
78
|
+
const { GH_TOKEN, GHE_API_URL } = process.env;
|
|
79
|
+
|
|
80
|
+
if (!GH_TOKEN) {
|
|
81
|
+
await ensureGhCliAvailable(workspace);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Determine hostname for GHE if provided
|
|
86
|
+
const args: string[] = [
|
|
87
|
+
"release",
|
|
88
|
+
"create",
|
|
89
|
+
tag,
|
|
90
|
+
"--title",
|
|
91
|
+
title,
|
|
92
|
+
"--repo",
|
|
93
|
+
`${parsedRepoUrl.username}/${parsedRepoUrl.reponame}`,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
if (prerelease) {
|
|
97
|
+
args.push("--prerelease");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (GHE_API_URL) {
|
|
101
|
+
const hostname = extractHostnameFromGheApiUrl(GHE_API_URL);
|
|
102
|
+
if (hostname) {
|
|
103
|
+
args.push("--hostname", hostname);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await execCommandWithInput(
|
|
109
|
+
workspace,
|
|
110
|
+
["gh", ...args, "--notes-file", "-"],
|
|
111
|
+
body,
|
|
112
|
+
);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// execCommandWithInput will include stdout/stderr in its error message when strict
|
|
115
|
+
throw new Error(
|
|
116
|
+
`gh release create failed: ${String(error instanceof Error ? error.message : error)}`,
|
|
117
|
+
{ cause: error },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
};
|