@dbx-tools/projen 0.9.47 → 0.9.51

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.
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env -S bun
2
- /** Validate one stable release version against the canonical workspace policy. */
3
- import { resolve } from "node:path";
4
- import { parseArgs } from "node:util";
5
- import * as projectUtils from "@dbx-tools/core/project-utils";
6
- import { captureTaskCommand } from "../../src/_task-command.ts";
7
- import { compareSemver, parseSemver, type Semver } from "../../src/workspace-version.ts";
8
-
9
- function publishedVersion(root: string, prefixes: readonly string[]): string | undefined {
10
- let best: Semver | undefined;
11
- for (const prefix of prefixes) {
12
- const tags = captureTaskCommand(root, "git", [
13
- "-c",
14
- "versionsort.suffix=-",
15
- "tag",
16
- "--sort=-version:refname",
17
- "--list",
18
- `${prefix}*`,
19
- ]);
20
- for (const tag of tags.split("\n")) {
21
- if (!tag.startsWith(prefix)) continue;
22
- const version = parseSemver(tag.slice(prefix.length));
23
- if (version && (!best || compareSemver(version, best) > 0)) best = version;
24
- }
25
- }
26
- return best?.join(".");
27
- }
28
-
29
- export function assertReleaseVersion(
30
- version: string,
31
- options: { root?: string; prefixes?: readonly string[]; assertNext?: boolean } = {},
32
- ): void {
33
- const parsed = parseSemver(version);
34
- if (!parsed) {
35
- throw new Error(
36
- `release version must be an exact stable x.y.z, got ${JSON.stringify(version)}`,
37
- );
38
- }
39
- if (!options.assertNext) return;
40
- const root = resolve(options.root ?? projectUtils.root() ?? process.cwd());
41
- const prefixes = options.prefixes?.length ? options.prefixes : ["v"];
42
- const current = publishedVersion(root, prefixes);
43
- const currentParsed = current ? parseSemver(current) : undefined;
44
- if (currentParsed && compareSemver(parsed, currentParsed) <= 0) {
45
- throw new Error(`release version ${version} must be greater than published ${current}`);
46
- }
47
- }
48
-
49
- export function main(args: string[] = process.argv.slice(2)): void {
50
- const { values } = parseArgs({
51
- args,
52
- options: {
53
- version: { type: "string" },
54
- root: { type: "string" },
55
- prefix: { type: "string", multiple: true },
56
- "assert-next": { type: "boolean", default: false },
57
- },
58
- strict: true,
59
- });
60
- if (!values.version) throw new Error("--version is required");
61
- assertReleaseVersion(values.version, {
62
- ...(values.root ? { root: values.root } : {}),
63
- ...(values.prefix ? { prefixes: values.prefix } : {}),
64
- assertNext: values["assert-next"],
65
- });
66
- }
@@ -1,278 +0,0 @@
1
- #!/usr/bin/env -S bun
2
- /** Run the configured release transaction. */
3
- import * as projectUtils from "@dbx-tools/core/project-utils";
4
- import { log } from "@dbx-tools/shared-core";
5
- import { Command, Option } from "commander";
6
- import { publishLocalRelease } from "./local-publish.ts";
7
- import { assertReleaseVersion } from "./release-version.ts";
8
- import {
9
- captureTaskCommand,
10
- runTaskCommand,
11
- taskCommandSucceeds,
12
- } from "../../src/_task-command.ts";
13
- import {
14
- RELEASE_INSTALL_MODES,
15
- RELEASE_PUBLISH_TARGETS,
16
- releasePublishesLocally,
17
- releaseStepSelection,
18
- releaseTagAnnotation,
19
- type ReleaseInstallMode,
20
- type ReleaseSelectionOptions,
21
- } from "../../src/release-options.ts";
22
- import { readWorkspaceVersion } from "../../src/workspace-version.ts";
23
-
24
- const logger = log.logger("projen:release");
25
-
26
- function gitHead(root: string, ref: string): string | undefined {
27
- if (!taskCommandSucceeds(root, "git", ["rev-parse", "--verify", ref])) return undefined;
28
- return captureTaskCommand(root, "git", ["rev-parse", ref], { check: true });
29
- }
30
-
31
- function assertAncestor(root: string, ancestor: string, descendant: string, message: string): void {
32
- if (!taskCommandSucceeds(root, "git", ["merge-base", "--is-ancestor", ancestor, descendant])) {
33
- throw new Error(message);
34
- }
35
- }
36
-
37
- function prepareReleaseBranch(options: {
38
- readonly branch: string;
39
- readonly remote: string;
40
- readonly root: string;
41
- }): void {
42
- const { branch, remote, root } = options;
43
- const sourceBranch = captureTaskCommand(root, "git", ["branch", "--show-current"], {
44
- check: true,
45
- });
46
- if (!sourceBranch) throw new Error("release cannot run from a detached HEAD");
47
-
48
- const status = captureTaskCommand(
49
- root,
50
- "git",
51
- ["status", "--porcelain=v1", "--untracked-files=all"],
52
- { check: true },
53
- );
54
- if (status) {
55
- runTaskCommand(root, "git", ["add", "--all"]);
56
- runTaskCommand(root, "git", ["commit", "-m", "chore: prepare release"]);
57
- }
58
-
59
- runTaskCommand(root, "git", ["fetch", "--prune", remote, "--tags"]);
60
- const sourceHead = captureTaskCommand(root, "git", ["rev-parse", "HEAD"], { check: true });
61
- const remoteSourceHead = gitHead(root, `refs/remotes/${remote}/${sourceBranch}`);
62
- if (remoteSourceHead) {
63
- assertAncestor(
64
- root,
65
- remoteSourceHead,
66
- sourceHead,
67
- `cannot fast-forward ${remote}/${sourceBranch} to ${sourceBranch}`,
68
- );
69
- }
70
- runTaskCommand(root, "git", ["push", remote, `HEAD:${sourceBranch}`]);
71
-
72
- if (sourceBranch === branch) return;
73
-
74
- const remoteMain = gitHead(root, `refs/remotes/${remote}/${branch}`);
75
- if (!remoteMain) throw new Error(`release branch ${remote}/${branch} does not exist`);
76
- assertAncestor(
77
- root,
78
- remoteMain,
79
- sourceHead,
80
- `cannot safely merge ${sourceBranch} into ${branch}; ${branch} is not an ancestor`,
81
- );
82
-
83
- const localMain = gitHead(root, `refs/heads/${branch}`);
84
- if (localMain) {
85
- assertAncestor(
86
- root,
87
- localMain,
88
- sourceHead,
89
- `cannot safely fast-forward local ${branch} to ${sourceBranch}`,
90
- );
91
- } else {
92
- runTaskCommand(root, "git", ["branch", branch, `${remote}/${branch}`]);
93
- }
94
- runTaskCommand(root, "git", ["switch", branch]);
95
- runTaskCommand(root, "git", ["merge", "--ff-only", sourceBranch]);
96
- runTaskCommand(root, "git", ["push", remote, `HEAD:${branch}`]);
97
- }
98
-
99
- export async function runRelease(
100
- options: ReleaseSelectionOptions & {
101
- readonly root: string;
102
- readonly branch: string;
103
- readonly bump?: boolean;
104
- readonly prefix: string;
105
- readonly remote: string;
106
- readonly localPublish?: boolean;
107
- readonly localRegistry?: string;
108
- readonly localPypi?: string;
109
- readonly install?: ReleaseInstallMode;
110
- readonly pythonRoot?: string;
111
- readonly validationTasks?: readonly string[];
112
- },
113
- ): Promise<string> {
114
- const { branch, prefix, remote, root } = options;
115
- const selection = releaseStepSelection(options);
116
- const install = options.install ?? "auto";
117
-
118
- if (install === "always") runTaskCommand(root, "bun", ["install"]);
119
-
120
- prepareReleaseBranch({ branch, remote, root });
121
-
122
- runTaskCommand(root, "git", ["fetch", remote, branch, "--tags"]);
123
- if (options.bump ?? true) {
124
- runTaskCommand(root, "bun", ["run", "bump"], {
125
- env: { ...process.env, DBX_TOOLS_RELEASE_INSTALL: install === "auto" ? "auto" : "never" },
126
- });
127
- }
128
-
129
- const version = readWorkspaceVersion(root);
130
- const tag = `${prefix}${version}`;
131
- assertReleaseVersion(version, { root, prefixes: [prefix], assertNext: true });
132
-
133
- if (taskCommandSucceeds(root, "git", ["rev-parse", "--verify", `refs/tags/${tag}`])) {
134
- throw new Error(`release tag ${tag} already exists`);
135
- }
136
-
137
- runTaskCommand(root, "bun", ["run", "version:check"]);
138
- for (const task of selection.validation ? (options.validationTasks ?? []) : []) {
139
- runTaskCommand(root, "bun", ["run", task]);
140
- }
141
- const status = captureTaskCommand(
142
- root,
143
- "git",
144
- ["status", "--porcelain=v1", "--untracked-files=all"],
145
- { check: true },
146
- );
147
- if (status) {
148
- if (!(options.bump ?? true)) {
149
- throw new Error("--no-bump requires an already committed synchronized version");
150
- }
151
- if (taskCommandSucceeds(root, "git", ["diff", "--quiet", "HEAD", "--", "VERSION"])) {
152
- throw new Error("release changes must include the local VERSION bump");
153
- }
154
- runTaskCommand(root, "git", ["add", "--all"]);
155
- runTaskCommand(root, "git", ["commit", "-m", `chore(release): ${version}`]);
156
- }
157
-
158
- const remoteHead = captureTaskCommand(root, "git", ["rev-parse", `${remote}/${branch}`], {
159
- check: true,
160
- });
161
- const head = captureTaskCommand(root, "git", ["rev-parse", "HEAD"], { check: true });
162
- if (!taskCommandSucceeds(root, "git", ["merge-base", "--is-ancestor", remoteHead, head])) {
163
- throw new Error(
164
- `cannot fast-forward ${remote}/${branch} to HEAD; ${branch} has diverged (rebase before release)`,
165
- );
166
- }
167
-
168
- runTaskCommand(root, "git", [
169
- "tag",
170
- "--annotate",
171
- tag,
172
- "--message",
173
- releaseTagAnnotation(tag, selection),
174
- ]);
175
- try {
176
- runTaskCommand(root, "git", ["push", "--atomic", remote, `HEAD:${branch}`, `refs/tags/${tag}`]);
177
- } catch (error) {
178
- runTaskCommand(root, "git", ["tag", "--delete", tag]);
179
- throw error;
180
- }
181
- logger.success(`released ${tag}`, { sha: head });
182
-
183
- // Restore local deploys: when a non-standard (loopback) npm or uv registry is
184
- // configured, publish the freshly tagged version to it directly. Detection and
185
- // publishing are owned by local-publish.ts; this no-ops on standard registries.
186
- if ((options.localPublish ?? true) && releasePublishesLocally(options.publish)) {
187
- await publishLocalRelease({
188
- localPypi: options.pypi === false ? "false" : (options.localPypi ?? "auto"),
189
- localRegistry: options.npm === false ? "false" : (options.localRegistry ?? "auto"),
190
- pythonRoot: options.pythonRoot ?? "packages/py",
191
- root,
192
- version,
193
- });
194
- }
195
-
196
- return tag;
197
- }
198
-
199
- /** Build the release task's native parser without executing git or registry operations. */
200
- export function createReleaseCommand(): Command {
201
- return new Command()
202
- .name("release")
203
- .description("Prepare an annotated release and select its build and publication steps")
204
- .option("--root <path>", "repository root")
205
- .option("--branch <name>", "release branch", "main")
206
- .option("--prefix <prefix>", "release tag prefix", "v")
207
- .option("--remote <name>", "git remote", "origin")
208
- .option("--python-root <path>", "Python package root for local publish", "packages/py")
209
- .option(
210
- "--validate <task>",
211
- "task to run before pushing",
212
- (task, tasks: string[]) => [...tasks, task],
213
- [],
214
- )
215
- .option("--no-bump", "use an existing synchronized local version bump")
216
- .addOption(
217
- new Option("--publish <target>", "publication scope")
218
- .choices([...RELEASE_PUBLISH_TARGETS])
219
- .default("auto"),
220
- )
221
- .addOption(
222
- new Option("--install <mode>", "local workspace dependency installation")
223
- .choices([...RELEASE_INSTALL_MODES])
224
- .default("auto"),
225
- )
226
- .option("--no-npm", "skip npm build and publication, including local npm publication")
227
- .option("--no-pypi", "skip Python build and publication, including local Python publication")
228
- .option("--docs", "build and deploy docs for a selected scope")
229
- .option("--no-docs", "skip documentation build and deployment")
230
- .option(
231
- "--no-validation",
232
- "skip optional release validation tasks; version/source checks remain mandatory",
233
- )
234
- .option("--no-local-publish", "skip publishing to configured local registries")
235
- .option("--local-registry <auto|false|url>", "local npm registry selection", "auto")
236
- .option("--local-pypi <auto|false|url>", "local devpi registry selection", "auto")
237
- .action(
238
- async (
239
- options: ReleaseSelectionOptions & {
240
- root?: string;
241
- branch: string;
242
- bump: boolean;
243
- prefix: string;
244
- remote: string;
245
- pythonRoot: string;
246
- localPublish: boolean;
247
- localRegistry: string;
248
- localPypi: string;
249
- install: ReleaseInstallMode;
250
- validate: string[];
251
- },
252
- ) => {
253
- await runRelease({
254
- root: options.root ?? projectUtils.root() ?? process.cwd(),
255
- branch: options.branch,
256
- bump: options.bump,
257
- prefix: options.prefix,
258
- remote: options.remote,
259
- pythonRoot: options.pythonRoot,
260
- localPublish: options.localPublish,
261
- localRegistry: options.localRegistry,
262
- localPypi: options.localPypi,
263
- install: options.install,
264
- publish: options.publish,
265
- npm: options.npm,
266
- pypi: options.pypi,
267
- docs: options.docs,
268
- validation: options.validation,
269
- validationTasks: options.validate,
270
- });
271
- },
272
- );
273
- }
274
-
275
- /** Execute the native release task parser. */
276
- export async function main(): Promise<void> {
277
- await createReleaseCommand().parseAsync();
278
- }