@dbx-tools/projen 0.9.5 → 0.9.6

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.
Files changed (47) hide show
  1. package/README.md +64 -16
  2. package/index.ts +7 -5
  3. package/package.json +9 -6
  4. package/src/_rust-project.ts +356 -0
  5. package/src/_rust-release-workflow.ts +901 -0
  6. package/src/_rust-release.ts +605 -0
  7. package/src/_task-command.ts +89 -0
  8. package/src/barrels.ts +9 -7
  9. package/src/engine-root.ts +5 -5
  10. package/src/openapi.ts +1 -1
  11. package/src/packages.ts +9 -9
  12. package/src/pnpm-workspace.ts +1 -1
  13. package/src/project-js.ts +19 -25
  14. package/src/project-py.ts +6 -6
  15. package/src/project-rs.ts +100 -1098
  16. package/src/projen-version.ts +9 -0
  17. package/src/publish.ts +3 -4
  18. package/src/release-catalog.ts +1 -1
  19. package/src/release.ts +11 -12
  20. package/src/scaffold.ts +1 -1
  21. package/src/uniffi.ts +7 -0
  22. package/src/watch.ts +2 -2
  23. package/src/workspace-version.ts +20 -15
  24. package/tasks/barrels.ts +4 -4
  25. package/tasks/build-rust-release.ts +68 -0
  26. package/tasks/bump.ts +3 -2
  27. package/tasks/clean.ts +3 -3
  28. package/tasks/local-publish.ts +21 -3
  29. package/tasks/openapi.ts +2 -2
  30. package/tasks/publish-npm.ts +12 -6
  31. package/tasks/publish-python.ts +2 -2
  32. package/tasks/publish-uniffi-local.ts +11 -15
  33. package/tasks/publish.ts +88 -88
  34. package/tasks/python-registry.ts +1 -1
  35. package/tasks/release-bootstrap.ts +2 -2
  36. package/tasks/release-plan.ts +1 -1
  37. package/tasks/release-please.ts +1 -1
  38. package/tasks/release-pr.ts +60 -115
  39. package/tasks/release-request.ts +8 -29
  40. package/tasks/release-summary-units.ts +2 -9
  41. package/tasks/release-summary.ts +35 -24
  42. package/tasks/release-version.ts +44 -0
  43. package/tasks/rust-release.d.ts +41 -0
  44. package/tasks/rust-release.mjs +1717 -0
  45. package/tasks/uniffi-python.js +1 -1
  46. package/tasks/uniffi.ts +7 -1
  47. package/tasks/version-check.ts +6 -5
@@ -3,10 +3,11 @@
3
3
 
4
4
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { dirname, resolve } from "node:path";
6
- import { exec, project } from "@dbx-tools/core";
7
- import { log, string } from "@dbx-tools/shared-core";
6
+ import * as projectUtils from "@dbx-tools/core/project-utils";
7
+ import { log, stringUtils } from "@dbx-tools/shared-core";
8
8
  import { Command } from "commander";
9
9
  import { selectReleaseSummary } from "./release-summary.ts";
10
+ import { captureTaskCommand, runTaskCommand } from "../src/_task-command.ts";
10
11
 
11
12
  const logger = log.logger("projen:release-request");
12
13
 
@@ -33,13 +34,7 @@ export async function prepareReleaseRequest(
33
34
  }
34
35
  const baseRef = options.push === false ? options.baseBranch : `origin/${options.baseBranch}`;
35
36
  if (options.synthesize !== false) {
36
- exec.spawnSync(process.execPath, ["x", "projen"], {
37
- cwd: root,
38
- stdout: "inherit",
39
- stderr: "inherit",
40
- stdin: "ignore",
41
- check: true,
42
- });
37
+ runTaskCommand(root, process.execPath, ["x", "projen"]);
43
38
  }
44
39
  const dirty = capture(root, ["status", "--porcelain"]);
45
40
  if (dirty) {
@@ -74,7 +69,7 @@ export async function prepareReleaseRequest(
74
69
  throw new Error(`Release requests must run from a branch other than ${options.baseBranch}`);
75
70
  }
76
71
 
77
- const customNotes = string.trimToNull(
72
+ const customNotes = stringUtils.trimToNull(
78
73
  options.notesFile ? readFileSync(resolve(root, options.notesFile), "utf8") : options.notes,
79
74
  );
80
75
  const notes =
@@ -130,27 +125,11 @@ async function generateRequestNotes(root: string, range: string): Promise<string
130
125
  }
131
126
 
132
127
  function capture(root: string, args: string[]): string {
133
- return (
134
- exec
135
- .spawnSync("git", args, {
136
- cwd: root,
137
- stdout: "capture",
138
- stderr: "inherit",
139
- stdin: "ignore",
140
- check: true,
141
- })
142
- .stdout?.trim() ?? ""
143
- );
128
+ return captureTaskCommand(root, "git", args, { check: true, stderr: "inherit" });
144
129
  }
145
130
 
146
131
  function run(root: string, args: string[]): void {
147
- exec.spawnSync("git", args, {
148
- cwd: root,
149
- stdout: "inherit",
150
- stderr: "inherit",
151
- stdin: "ignore",
152
- check: true,
153
- });
132
+ runTaskCommand(root, "git", args);
154
133
  }
155
134
 
156
135
  /** Restore option text after Projen forwards task arguments without shell quoting. */
@@ -175,7 +154,7 @@ if (import.meta.main) {
175
154
  throw new Error("Use --notes or --notes-file, not both");
176
155
  }
177
156
  await prepareReleaseRequest({
178
- root: project.root() ?? process.cwd(),
157
+ root: projectUtils.root() ?? process.cwd(),
179
158
  baseBranch: options.base,
180
159
  message: optionText(options.message),
181
160
  notes: optionText(options.notes),
@@ -3,10 +3,10 @@
3
3
 
4
4
  import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
- import { exec } from "@dbx-tools/core";
7
6
  import { log } from "@dbx-tools/shared-core";
8
7
  import { Command } from "commander";
9
8
  import { generateReleaseSummary } from "./release-summary.ts";
9
+ import { captureTaskCommand } from "../src/_task-command.ts";
10
10
  import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
11
11
  import type { ReleasePlan } from "../src/release-plan.ts";
12
12
 
@@ -71,12 +71,5 @@ function requestNotes(root: string): { summary?: string; paths: string[] } {
71
71
  }
72
72
 
73
73
  function gitCapture(root: string, args: string[]): string {
74
- const result = exec.spawnSync("git", args, {
75
- cwd: root,
76
- stdout: "capture",
77
- stderr: "ignore",
78
- stdin: "ignore",
79
- check: false,
80
- });
81
- return result.exitCode === 0 ? (result.stdout?.trim() ?? "") : "";
74
+ return captureTaskCommand(root, "git", args);
82
75
  }
@@ -6,8 +6,10 @@
6
6
  */
7
7
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
- import { exec } from "@dbx-tools/core";
10
- import { json, log, object, string } from "@dbx-tools/shared-core";
9
+ import * as exec from "@dbx-tools/core/exec";
10
+ import * as projectUtils from "@dbx-tools/core/project-utils";
11
+ import { json, log, object, stringUtils } from "@dbx-tools/shared-core";
12
+ import { captureTaskCommand } from "../src/_task-command.ts";
11
13
  import {
12
14
  RELEASE_SUMMARY_PROVIDER_NAMES,
13
15
  releaseSummaryFile,
@@ -84,30 +86,23 @@ export type ReleaseSummaryRunner = (
84
86
  ) => string | undefined | Promise<string | undefined>;
85
87
 
86
88
  function capture(root: string, command: string, args: string[]): string {
87
- const result = exec.spawnSync(command, args, {
88
- cwd: root,
89
- stdout: "capture",
90
- stderr: "ignore",
91
- stdin: "ignore",
92
- check: false,
93
- });
94
- return result.exitCode === 0 ? (result.stdout?.trim() ?? "") : "";
89
+ return captureTaskCommand(root, command, args);
95
90
  }
96
91
 
97
92
  function contentText(value: unknown): string | undefined {
98
- if (typeof value === "string") return string.trimToNull(value) ?? undefined;
93
+ if (typeof value === "string") return stringUtils.trimToNull(value) ?? undefined;
99
94
  if (!Array.isArray(value)) return undefined;
100
95
  const text = value
101
96
  .flatMap((item) => (object.isRecord(item) && typeof item.text === "string" ? [item.text] : []))
102
97
  .join("");
103
- return string.trimToNull(text) ?? undefined;
98
+ return stringUtils.trimToNull(text) ?? undefined;
104
99
  }
105
100
 
106
101
  function eventText(event: Record<string, unknown>): string | undefined {
107
- if (typeof event.result === "string") return string.trimToNull(event.result) ?? undefined;
102
+ if (typeof event.result === "string") return stringUtils.trimToNull(event.result) ?? undefined;
108
103
  const item = object.isRecord(event.item) ? event.item : undefined;
109
104
  if (item?.type === "agent_message" && typeof item.text === "string") {
110
- return string.trimToNull(item.text) ?? undefined;
105
+ return stringUtils.trimToNull(item.text) ?? undefined;
111
106
  }
112
107
  const message = object.isRecord(event.message) ? event.message : undefined;
113
108
  return contentText(message?.content);
@@ -121,7 +116,7 @@ function eventType(event: Record<string, unknown>): string {
121
116
  function eventDetail(event: Record<string, unknown>): string | undefined {
122
117
  const item = object.isRecord(event.item) ? event.item : undefined;
123
118
  for (const value of [item?.message, event.message, event.error]) {
124
- if (typeof value === "string") return string.trimToNull(value) ?? undefined;
119
+ if (typeof value === "string") return stringUtils.trimToNull(value) ?? undefined;
125
120
  }
126
121
  return undefined;
127
122
  }
@@ -129,7 +124,7 @@ function eventDetail(event: Record<string, unknown>): string | undefined {
129
124
  function eventToolName(event: Record<string, unknown>): string | undefined {
130
125
  const item = object.isRecord(event.item) ? event.item : undefined;
131
126
  for (const value of [event.tool_name, event.toolName, event.name, item?.tool_name, item?.name]) {
132
- if (typeof value === "string") return string.trimToNull(value) ?? undefined;
127
+ if (typeof value === "string") return stringUtils.trimToNull(value) ?? undefined;
133
128
  }
134
129
  const toolCall = object.isRecord(event.tool_call)
135
130
  ? event.tool_call
@@ -185,7 +180,7 @@ async function runProvider(
185
180
  (line) => {
186
181
  const event = json.parseRecord(line);
187
182
  if (!event) {
188
- const text = string.trimToNull(line);
183
+ const text = stringUtils.trimToNull(line);
189
184
  if (text) {
190
185
  textOutput.push(text);
191
186
  logEvent({ type: "text", text });
@@ -198,7 +193,7 @@ async function runProvider(
198
193
  },
199
194
  ],
200
195
  stderr: (line) => {
201
- const message = string.trimToNull(line);
196
+ const message = stringUtils.trimToNull(line);
202
197
  if (message) logger.warn("provider-stderr", { provider: provider.name, message });
203
198
  },
204
199
  stdin: "ignore",
@@ -243,6 +238,7 @@ export async function selectReleaseSummary(
243
238
  }
244
239
 
245
240
  function summaryPrompt(
241
+ projectName: string,
246
242
  version: string,
247
243
  fromRef: string | undefined,
248
244
  commits: string,
@@ -250,7 +246,7 @@ function summaryPrompt(
250
246
  diffStat: string,
251
247
  ): string {
252
248
  return [
253
- `Write a concise user-facing Markdown release summary for dbx-tools ${version}.`,
249
+ `Write a concise user-facing Markdown release summary for ${projectName} ${version}.`,
254
250
  "Use only the supplied Git context. Do not run commands, edit files, speculate,",
255
251
  "mention commit hashes, use emojis, or use em/en dashes.",
256
252
  "Return no title and no fenced block. Start with one short paragraph, then",
@@ -270,7 +266,12 @@ function summaryPrompt(
270
266
  }
271
267
 
272
268
  /** Deterministic fallback when every configured AI provider is unavailable. */
273
- function gitSummary(version: string, commits: string, changedFiles: string): string {
269
+ function gitSummary(
270
+ projectName: string,
271
+ version: string,
272
+ commits: string,
273
+ changedFiles: string,
274
+ ): string {
274
275
  const subjects = commits
275
276
  .split("\n")
276
277
  .map((line) => line.trim())
@@ -290,7 +291,7 @@ function gitSummary(version: string, commits: string, changedFiles: string): str
290
291
  .slice(0, 12);
291
292
  const bullets = subjects.length ? subjects : areas.map((area) => `Updated ${area}`);
292
293
  return [
293
- `dbx-tools ${version} contains the reviewed changes listed below.`,
294
+ `${projectName} ${version} contains the reviewed changes listed below.`,
294
295
  "",
295
296
  "## Changes",
296
297
  ...(bullets.length ? bullets.map((item) => `- ${item}`) : ["- Release metadata updated."]),
@@ -310,6 +311,7 @@ export async function generateReleaseSummary(options: {
310
311
  readonly providers?: readonly ReleaseSummaryProviderName[];
311
312
  readonly runner?: ReleaseSummaryRunner;
312
313
  }): Promise<string | undefined> {
314
+ const projectName = projectUtils.name(options.root);
313
315
  const relativeOutput =
314
316
  options.outputFile ?? releaseSummaryFile(options.version, options.component);
315
317
  const output = join(options.root, relativeOutput);
@@ -347,17 +349,26 @@ export async function generateReleaseSummary(options: {
347
349
  ...scopedPaths,
348
350
  `:(exclude)${relativeOutput}`,
349
351
  ]);
350
- const customSummary = string.trimToNull(options.customSummary);
352
+ const customSummary = stringUtils.trimToNull(options.customSummary);
351
353
  const result = customSummary
352
354
  ? undefined
353
355
  : await selectReleaseSummary(
354
356
  options.root,
355
- summaryPrompt(options.version, options.fromRef, commits, changedFiles, diffStat),
357
+ summaryPrompt(
358
+ projectName,
359
+ options.version,
360
+ options.fromRef,
361
+ commits,
362
+ changedFiles,
363
+ diffStat,
364
+ ),
356
365
  options.runner,
357
366
  options.providers,
358
367
  );
359
368
  const summary =
360
- customSummary ?? result?.summary ?? gitSummary(options.version, commits, changedFiles);
369
+ customSummary ??
370
+ result?.summary ??
371
+ gitSummary(projectName, options.version, commits, changedFiles);
361
372
  const provider = customSummary ? "custom" : (result?.provider ?? "git");
362
373
  if (!customSummary && !result) {
363
374
  logger.info("AI providers unavailable; using Git release summary");
@@ -0,0 +1,44 @@
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 { compareSemver, parseSemver, resolveRemoteVersion } from "../src/workspace-version.ts";
7
+
8
+ export function assertReleaseVersion(
9
+ version: string,
10
+ options: { root?: string; prefixes?: readonly string[]; assertNext?: boolean } = {},
11
+ ): void {
12
+ const parsed = parseSemver(version);
13
+ if (!parsed) {
14
+ throw new Error(
15
+ `release version must be an exact stable x.y.z, got ${JSON.stringify(version)}`,
16
+ );
17
+ }
18
+ if (!options.assertNext) return;
19
+ const root = resolve(options.root ?? projectUtils.root() ?? process.cwd());
20
+ const prefixes = options.prefixes?.length ? options.prefixes : ["v"];
21
+ const current = resolveRemoteVersion(root, prefixes, { fetch: false });
22
+ const currentParsed = current ? parseSemver(current) : undefined;
23
+ if (currentParsed && compareSemver(parsed, currentParsed) <= 0) {
24
+ throw new Error(`release version ${version} must be greater than published ${current}`);
25
+ }
26
+ }
27
+
28
+ if (import.meta.main) {
29
+ const { values } = parseArgs({
30
+ options: {
31
+ version: { type: "string" },
32
+ root: { type: "string" },
33
+ prefix: { type: "string", multiple: true },
34
+ "assert-next": { type: "boolean", default: false },
35
+ },
36
+ strict: true,
37
+ });
38
+ if (!values.version) throw new Error("--version is required");
39
+ assertReleaseVersion(values.version, {
40
+ ...(values.root ? { root: values.root } : {}),
41
+ ...(values.prefix ? { prefixes: values.prefix } : {}),
42
+ assertNext: values["assert-next"],
43
+ });
44
+ }
@@ -0,0 +1,41 @@
1
+ export declare const FINGERPRINT_SCHEMA = 3;
2
+ export declare const VERSION_SLOT_SCHEMA = 1;
3
+ export declare const VERSION_CAPACITY = 64;
4
+ /** Canonical TOML representation with workspace-owned release versions removed. */
5
+ export declare function normalizeManifest(text: string): string;
6
+ /** Canonical Cargo.lock representation with local package versions normalized. */
7
+ export declare function normalizeLockfile(text: string, workspaceVersion: string, packageNames?: ReadonlySet<string>): string;
8
+ /** Hash Cargo-discovered members and every tracked or unignored file beneath them. */
9
+ export declare function sourceHash(root: string, sources?: readonly string[]): string;
10
+ export declare function linkerIdentity(target: string): string;
11
+ export interface TargetKeyOptions {
12
+ readonly rustSourceHash: string;
13
+ readonly target: string;
14
+ readonly targetConfig?: string;
15
+ readonly toolchain?: string;
16
+ readonly rustc?: string;
17
+ }
18
+ export declare function targetKey({ rustSourceHash, target, targetConfig, toolchain, rustc, }: TargetKeyOptions): string;
19
+ export interface FingerprintOptions {
20
+ readonly root?: string;
21
+ readonly output?: string;
22
+ readonly check?: boolean;
23
+ readonly targets: readonly string[];
24
+ readonly toolchain?: string;
25
+ readonly portable?: boolean;
26
+ readonly sources?: readonly string[];
27
+ }
28
+ export interface RustBuildFingerprint {
29
+ readonly schemaVersion: number;
30
+ readonly versionSlotSchema: number;
31
+ readonly rustSourceHash: string;
32
+ readonly targets: Readonly<Record<string, string>>;
33
+ }
34
+ export declare function fingerprint({ root, output, check, targets, toolchain, portable, sources, }: FingerprintOptions): RustBuildFingerprint;
35
+ export interface StampOptions {
36
+ readonly objcopy?: string;
37
+ readonly codesign?: string;
38
+ }
39
+ export declare function stamp(binary: string, version: string, options?: StampOptions): boolean;
40
+ export declare function stampTree(root: string, version: string, options?: StampOptions): number;
41
+ export declare function main(args?: readonly string[]): void;