@dbx-tools/projen 0.6.223 → 0.6.225

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/src/project-py.ts CHANGED
@@ -2,9 +2,7 @@
2
2
  import { project as coreProject } from "@dbx-tools/core";
3
3
  import { string } from "@dbx-tools/shared-core";
4
4
  import { Component, License, TextFile, type Project, javascript, python, vscode } from "projen";
5
- import type { IResolver } from "projen/lib/file";
6
5
  import { JobPermission } from "projen/lib/github/workflows-model";
7
- import { parse, stringify } from "smol-toml";
8
6
  import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
9
7
  import { DBX_TOOLS_LICENSE, projectReleaseBranch, projectRepositoryUrl } from "./project-js.ts";
10
8
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
@@ -14,7 +12,7 @@ import {
14
12
  releaseArtifactSteps,
15
13
  releasePublishCondition,
16
14
  releaseStageCondition,
17
- releaseWorkflow,
15
+ tryReleaseWorkflow,
18
16
  } from "./release.ts";
19
17
  import { readWorkspaceVersion } from "./workspace-version.ts";
20
18
 
@@ -111,24 +109,6 @@ const DEFAULT_DEV_DEPENDENCIES = [
111
109
 
112
110
  const quote = (value: string): string => JSON.stringify(value);
113
111
 
114
- interface TomlSynthesizer {
115
- synthesizeContent(resolver: IResolver): string | undefined;
116
- }
117
-
118
- function formatPyproject(file: python.PyprojectTomlFile): void {
119
- const target = file as unknown as TomlSynthesizer;
120
- const synthesize = target.synthesizeContent.bind(file);
121
- target.synthesizeContent = (resolver) => {
122
- const content = synthesize(resolver);
123
- if (!content) return content;
124
- const marker = content.startsWith("# ") ? content.slice(0, content.indexOf("\n")) : undefined;
125
- const body = stringify(parse(content))
126
- .trimEnd()
127
- .replace(/ = \[ (.*) \]$/gm, " = [$1]");
128
- return `${marker ? `${marker}\n\n` : ""}${body}\n`;
129
- };
130
- }
131
-
132
112
  /** Repository-relative path for a Python package directory. */
133
113
  export function pythonPackagePath(repository: PythonRepositoryOptions, directory: string): string {
134
114
  return `${repository.root ?? "packages/py"}/${directory}`;
@@ -222,7 +202,6 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
222
202
  if (pkg.uniffi !== undefined) {
223
203
  this.uv.file.addOverride("tool.dbx_tools.config.uniffi", pkg.uniffi);
224
204
  }
225
- formatPyproject(this.uv.file);
226
205
  this.uv.file.readonly = true;
227
206
 
228
207
  for (const path of [".gitattributes", ".gitignore"]) {
@@ -397,7 +376,6 @@ export class DBXToolsPythonWorkspace extends Component {
397
376
  "tool.uv.sources",
398
377
  Object.fromEntries(options.packages.map((pkg) => [pkg.name, { workspace: true }])),
399
378
  );
400
- formatPyproject(file);
401
379
  file.readonly = true;
402
380
  return file;
403
381
  }
@@ -433,7 +411,10 @@ export class DBXToolsPythonWorkspace extends Component {
433
411
  const allPublications = [...publications, ...uniffiPublications];
434
412
  if (allPublications.length === 0) return;
435
413
  const usesRustArtifacts = uniffiPublications.length > 0;
436
- const workflow = releaseWorkflow(project);
414
+ const workflow = tryReleaseWorkflow(project);
415
+ if (!workflow) {
416
+ throw new Error("Python release requires the root dbx-tools release mode");
417
+ }
437
418
  workflow.addJob("build-python", {
438
419
  if: usesRustArtifacts
439
420
  ? "${{ always() && needs.verify-context.result == 'success' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}"
@@ -445,7 +426,9 @@ export class DBXToolsPythonWorkspace extends Component {
445
426
  env: { BUN_VERSION },
446
427
  steps: [
447
428
  ...releaseSourceSteps(),
448
- ...bunCacheRestoreSteps(project),
429
+ ...bunCacheRestoreSteps(project, {
430
+ ignorePaths: project.workflowCacheIgnorePaths,
431
+ }),
449
432
  { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
450
433
  { name: "Install release helpers", run: "bun install" },
451
434
  bunCacheSaveStep(),
package/src/project-rs.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /** Filesystem-discovered Rust workspaces and UniFFI binding package wiring. */
2
2
  import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { createRequire } from "node:module";
3
4
  import { dirname, join, relative, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { exec, project as coreProject } from "@dbx-tools/core";
6
7
  import { string } from "@dbx-tools/shared-core";
7
- import { Component, Project, TextFile, javascript } from "projen";
8
+ import { Component, Project, TextFile, TomlFile, javascript } from "projen";
8
9
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
9
10
  import { BUN_VERSION } from "./bun-workflow.ts";
10
11
  import {
@@ -18,6 +19,7 @@ import { pythonModuleName, type PythonPackageOptions } from "./project-py.ts";
18
19
  import type { DBXToolsProject } from "./project.ts";
19
20
  import {
20
21
  RELEASE_SHA,
22
+ RELEASE_SUMMARY_FILE,
21
23
  RELEASE_TAG,
22
24
  RELEASE_VERSION,
23
25
  releaseSourceSteps,
@@ -28,7 +30,7 @@ import {
28
30
  nodeReleaseSetupSteps,
29
31
  releaseArtifactSteps,
30
32
  releaseStageCondition,
31
- releaseWorkflow,
33
+ tryReleaseWorkflow,
32
34
  } from "./release.ts";
33
35
  import { readWorkspaceVersion } from "./workspace-version.ts";
34
36
 
@@ -180,6 +182,7 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
180
182
 
181
183
  const UBRN_VERSION = "0.31.0-5";
182
184
  const RELEASE_PLATFORMS_ENV = "DBX_TOOLS_RELEASE_PLATFORMS";
185
+ const require = createRequire(import.meta.url);
183
186
  const RUST_CACHE_ENV = {
184
187
  CARGO_INCREMENTAL: "0",
185
188
  CARGO_TERM_COLOR: "always",
@@ -262,17 +265,25 @@ function rustCliRegistrySource(binaries: readonly RustReleaseBinaryMapping[]): s
262
265
  ].join("\n");
263
266
  }
264
267
 
265
- function uniffiReleaseTaskSource(): string {
268
+ function taskSource(name: string): string {
266
269
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
267
270
  const candidates = [
268
- resolve(sourceDirectory, "../tasks/uniffi-release.mjs"),
269
- resolve(sourceDirectory, "../../tasks/uniffi-release.mjs"),
271
+ resolve(sourceDirectory, `../tasks/${name}`),
272
+ resolve(sourceDirectory, `../../tasks/${name}`),
270
273
  ];
271
274
  const source = candidates.find(existsSync);
272
- if (!source) throw new Error("Could not locate tasks/uniffi-release.mjs");
275
+ if (!source) throw new Error(`Could not locate tasks/${name}`);
273
276
  return readFileSync(source, "utf8");
274
277
  }
275
278
 
279
+ function smolTomlSource(name: "dist/index.cjs" | "LICENSE"): string {
280
+ const entry = require.resolve("smol-toml");
281
+ return readFileSync(
282
+ resolve(dirname(entry), name === "LICENSE" ? "../LICENSE" : "index.cjs"),
283
+ "utf8",
284
+ );
285
+ }
286
+
276
287
  /** Keep tracked workspace package versions in Cargo.lock aligned with VERSION. */
277
288
  class RustWorkspaceVersionLock extends Component {
278
289
  public override postSynthesize(): void {
@@ -390,26 +401,23 @@ function cargoDependency(
390
401
  };
391
402
  }
392
403
 
393
- function tomlValue(value: unknown): string {
394
- if (typeof value === "string") return JSON.stringify(value);
395
- if (typeof value === "boolean" || typeof value === "number") return String(value);
396
- if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
397
- if (value && typeof value === "object") {
398
- return `{ ${Object.entries(value)
399
- .map(([key, entry]) => `${key} = ${tomlValue(entry)}`)
400
- .join(", ")} }`;
401
- }
402
- throw new Error(`Unsupported Cargo TOML value: ${String(value)}`);
403
- }
404
-
405
- function renderToml(value: Record<string, unknown>): string {
406
- const blocks: string[] = [];
407
- for (const [section, entries] of Object.entries(value)) {
408
- if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
409
- const rows = Object.entries(entries).map(([key, entry]) => `${key} = ${tomlValue(entry)}`);
410
- blocks.push(`${section === "bin" ? "[[bin]]" : `[${section}]`}\n${rows.join("\n")}`);
404
+ function structuredTomlSections(value: Readonly<Record<string, unknown>>): Record<string, unknown> {
405
+ const result: Record<string, unknown> = {};
406
+ for (const [section, contents] of Object.entries(value)) {
407
+ const path = section.split(".");
408
+ let target = result;
409
+ for (const part of path.slice(0, -1)) {
410
+ const current = target[part];
411
+ if (current === undefined) {
412
+ target[part] = {};
413
+ } else if (!current || typeof current !== "object" || Array.isArray(current)) {
414
+ throw new Error(`Conflicting TOML section ${section}`);
415
+ }
416
+ target = target[part] as Record<string, unknown>;
417
+ }
418
+ target[path.at(-1)!] = contents;
411
419
  }
412
- return `${blocks.join("\n\n")}\n`;
420
+ return result;
413
421
  }
414
422
 
415
423
  /** One generated Cargo workspace member. */
@@ -455,9 +463,11 @@ export class DBXToolsRustProject extends Project implements DBXToolsProject {
455
463
  : {}),
456
464
  ...(this.uniffi || binary
457
465
  ? {
458
- bin: this.uniffi
459
- ? { name: `${crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }
460
- : { name: binaryName, path: "src/main.rs" },
466
+ bin: [
467
+ this.uniffi
468
+ ? { name: `${crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }
469
+ : { name: binaryName, path: "src/main.rs" },
470
+ ],
461
471
  }
462
472
  : {}),
463
473
  ...(options.features || options.defaultFeatures
@@ -489,20 +499,19 @@ export class DBXToolsRustProject extends Project implements DBXToolsProject {
489
499
  }
490
500
  : {}),
491
501
  };
492
- new TextFile(this, "Cargo.toml", { lines: renderToml(manifest).trimEnd().split("\n") });
502
+ new TomlFile(this, "Cargo.toml", { marker: false, obj: manifest });
493
503
  if (this.uniffi) {
494
504
  new TextFile(this, "uniffi-bindgen.rs", {
495
505
  lines: ["fn main() {", " uniffi::uniffi_bindgen_main();", "}", ""],
496
506
  });
497
- new TextFile(this, "uniffi.toml", {
498
- lines: renderToml(
507
+ new TomlFile(this, "uniffi.toml", {
508
+ marker: false,
509
+ obj: structuredTomlSections(
499
510
  options.uniffiConfig ?? {
500
511
  "bindings.python": { cdylib_name: crateName.replaceAll("-", "_") },
501
512
  "bindings.typescript": { strictTypeChecking: true },
502
513
  },
503
- )
504
- .trimEnd()
505
- .split("\n"),
514
+ ),
506
515
  });
507
516
  }
508
517
  }
@@ -833,8 +842,9 @@ function configureRustBindingFiles(
833
842
  const dependencies = bindingDependencies(plan, pkg, "python");
834
843
  if (dependencies.length === 0) continue;
835
844
  pkg.tryRemoveFile("uniffi.toml");
836
- new TextFile(pkg, "uniffi.toml", {
837
- lines: renderToml({
845
+ new TomlFile(pkg, "uniffi.toml", {
846
+ marker: false,
847
+ obj: structuredTomlSections({
838
848
  "bindings.python": { cdylib_name: pkg.crateName.replaceAll("-", "_") },
839
849
  "bindings.typescript": { strictTypeChecking: true },
840
850
  ...pkg.packageOptions.uniffiConfig,
@@ -847,9 +857,7 @@ function configureRustBindingFiles(
847
857
  )}.bindings`,
848
858
  ]),
849
859
  ),
850
- })
851
- .trimEnd()
852
- .split("\n"),
860
+ }),
853
861
  });
854
862
  }
855
863
  }
@@ -936,39 +944,41 @@ function configureRustWorkspaceFiles(
936
944
  .filter((pkg) => !pkg.uniffi)
937
945
  .map((pkg) => `${resolved.root}/${pkg.packageOptions.directory}`),
938
946
  resolver: "2",
947
+ package: {
948
+ version: readWorkspaceVersion(project.outdir),
949
+ edition: options.edition ?? "2021",
950
+ "rust-version": options.rustVersion ?? "1.82",
951
+ license: options.license ?? DBX_TOOLS_LICENSE,
952
+ repository: resolved.repository,
953
+ },
954
+ ...(options.workspaceDependencies
955
+ ? {
956
+ dependencies: Object.fromEntries(
957
+ Object.entries(options.workspaceDependencies).map(([name, value]) => [
958
+ name,
959
+ cargoDependency(value),
960
+ ]),
961
+ ),
962
+ }
963
+ : {}),
939
964
  },
940
- "workspace.package": {
941
- version: readWorkspaceVersion(project.outdir),
942
- edition: options.edition ?? "2021",
943
- "rust-version": options.rustVersion ?? "1.82",
944
- license: options.license ?? DBX_TOOLS_LICENSE,
945
- repository: resolved.repository,
946
- },
947
- ...(options.workspaceDependencies
948
- ? {
949
- "workspace.dependencies": Object.fromEntries(
950
- Object.entries(options.workspaceDependencies).map(([name, value]) => [
951
- name,
952
- cargoDependency(value),
953
- ]),
954
- ),
955
- }
956
- : {}),
957
965
  };
958
- new TextFile(project, "Cargo.toml", {
959
- lines: renderToml(manifest).trimEnd().split("\n"),
966
+ new TomlFile(project, "Cargo.toml", {
967
+ marker: false,
968
+ obj: manifest,
960
969
  });
961
- new TextFile(project, ".cargo/config.toml", {
962
- lines: renderToml({
963
- "target.x86_64-pc-windows-msvc": {
964
- rustflags: ["-C", "target-feature=+crt-static"],
965
- },
966
- "target.aarch64-pc-windows-msvc": {
967
- rustflags: ["-C", "target-feature=+crt-static"],
970
+ new TomlFile(project, ".cargo/config.toml", {
971
+ marker: false,
972
+ obj: {
973
+ target: {
974
+ "x86_64-pc-windows-msvc": {
975
+ rustflags: ["-C", "target-feature=+crt-static"],
976
+ },
977
+ "aarch64-pc-windows-msvc": {
978
+ rustflags: ["-C", "target-feature=+crt-static"],
979
+ },
968
980
  },
969
- })
970
- .trimEnd()
971
- .split("\n"),
981
+ },
972
982
  });
973
983
  new RustWorkspaceVersionLock(project);
974
984
  }
@@ -1086,12 +1096,27 @@ function planRustRelease(
1086
1096
  }
1087
1097
 
1088
1098
  function configureRustReleaseTask(project: javascript.NodeProject, plan: RustReleasePlan): void {
1099
+ const supportFiles = [
1100
+ plan.releaseTask,
1101
+ ".projen/uniffi-python.js",
1102
+ ".projen/smol-toml.cjs",
1103
+ ".projen/smol-toml.LICENSE",
1104
+ ] as const;
1089
1105
  if (plan.bindings.length) {
1090
1106
  new TextFile(project, plan.releaseTask, {
1091
- lines: uniffiReleaseTaskSource().trimEnd().split("\n"),
1107
+ lines: taskSource("uniffi-release.mjs").trimEnd().split("\n"),
1108
+ });
1109
+ new TextFile(project, supportFiles[1], {
1110
+ lines: taskSource("uniffi-python.js").trimEnd().split("\n"),
1111
+ });
1112
+ new TextFile(project, supportFiles[2], {
1113
+ lines: smolTomlSource("dist/index.cjs").trimEnd().split("\n"),
1114
+ });
1115
+ new TextFile(project, supportFiles[3], {
1116
+ lines: smolTomlSource("LICENSE").trimEnd().split("\n"),
1092
1117
  });
1093
1118
  } else {
1094
- project.tryRemoveFile(plan.releaseTask);
1119
+ for (const path of supportFiles) project.tryRemoveFile(path);
1095
1120
  }
1096
1121
  }
1097
1122
 
@@ -1335,6 +1360,7 @@ function rustGitHubReleaseJob(): Job {
1335
1360
  uses: "softprops/action-gh-release@v2",
1336
1361
  with: {
1337
1362
  files: "dist/rust-release/*",
1363
+ body_path: RELEASE_SUMMARY_FILE,
1338
1364
  "generate-release-notes": true,
1339
1365
  tag_name: RELEASE_TAG,
1340
1366
  target_commitish: RELEASE_SHA,
@@ -1476,8 +1502,11 @@ export class DBXToolsRustWorkspace {
1476
1502
  ): void {
1477
1503
  if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
1478
1504
  const plan = planRustRelease(project, options, targets, this.packages, this.bindingMappings);
1505
+ const workflow = tryReleaseWorkflow(project);
1506
+ if (!workflow) {
1507
+ throw new Error("Rust release requires the root dbx-tools release mode");
1508
+ }
1479
1509
  configureRustReleaseTask(project, plan);
1480
- const workflow = releaseWorkflow(project);
1481
1510
  if (plan.hasTargetOutputs && plan.targets.length) {
1482
1511
  workflow.addJob("rust-build", rustBuildJob(plan));
1483
1512
  }
@@ -4,6 +4,16 @@ import type { JobStep } from "projen/lib/github/workflows-model";
4
4
  export const RELEASE_TAG = "${{ needs.verify-context.outputs.release_tag }}";
5
5
  export const RELEASE_SHA = "${{ needs.verify-context.outputs.expected_sha }}";
6
6
  export const RELEASE_VERSION = "${{ needs.verify-context.outputs.release_version }}";
7
+ export const RELEASE_SUMMARY_DIRECTORY = "docs/releases";
8
+ export const RELEASE_SUMMARY_PROVIDER_NAMES = ["cursor", "codex", "claude"] as const;
9
+ export type ReleaseSummaryProviderName = (typeof RELEASE_SUMMARY_PROVIDER_NAMES)[number];
10
+
11
+ /** Immutable repository path for one version's release summary. */
12
+ export function releaseSummaryFile(version: string): string {
13
+ return `${RELEASE_SUMMARY_DIRECTORY}/v${version.replace(/^v/, "")}.md`;
14
+ }
15
+
16
+ export const RELEASE_SUMMARY_FILE = releaseSummaryFile(RELEASE_VERSION);
7
17
 
8
18
  /** Check out and verify the immutable commit selected by the release tag. */
9
19
  export function releaseSourceSteps(): readonly JobStep[] {
@@ -0,0 +1,37 @@
1
+ /** GitHub repository identity used by release preparation. */
2
+ export interface GithubRepositoryIdentity {
3
+ hostname: string;
4
+ owner: string;
5
+ repository: string;
6
+ }
7
+
8
+ /** Parse a GitHub repository URL without treating its owner as the authenticated actor. */
9
+ export function githubRepositoryIdentity(repositoryUrl: string): GithubRepositoryIdentity {
10
+ const url = new URL(repositoryUrl);
11
+ const [owner, name] = url.pathname.split("/").filter(Boolean);
12
+ if (!owner || !name) {
13
+ throw new Error(`Cannot determine GitHub repository identity from ${repositoryUrl}`);
14
+ }
15
+ return {
16
+ hostname: url.hostname,
17
+ owner,
18
+ repository: name.replace(/\.git$/, ""),
19
+ };
20
+ }
21
+
22
+ /** Ask GitHub CLI for the current actor's token on the repository host. */
23
+ export function githubTokenArguments(hostname: string): string[] {
24
+ return ["auth", "token", "--hostname", hostname];
25
+ }
26
+
27
+ /** GitHub CLI token variable for a public, data-residency, or enterprise host. */
28
+ export function githubTokenEnvironmentName(hostname: string): "GH_TOKEN" | "GH_ENTERPRISE_TOKEN" {
29
+ return hostname === "github.com" || hostname.endsWith(".ghe.com")
30
+ ? "GH_TOKEN"
31
+ : "GH_ENTERPRISE_TOKEN";
32
+ }
33
+
34
+ /** Host-qualified repository selector consumed by GitHub CLI. */
35
+ export function githubRepositorySpecifier(identity: GithubRepositoryIdentity): string {
36
+ return `${identity.hostname}/${identity.owner}/${identity.repository}`;
37
+ }
package/src/release.ts CHANGED
@@ -5,7 +5,12 @@ import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflo
5
5
  import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
6
6
  import { projectReleaseBranch, type DBXToolsJavaScriptProject } from "./project-js.ts";
7
7
  import { applyTasks, taskScript } from "./project.ts";
8
- import { RELEASE_VERSION, releaseSourceSteps } from "./release-dispatch.ts";
8
+ import {
9
+ RELEASE_VERSION,
10
+ releaseSourceSteps,
11
+ type ReleaseSummaryProviderName,
12
+ } from "./release-dispatch.ts";
13
+ import { readWorkspaceVersion } from "./workspace-version.ts";
9
14
 
10
15
  const NODE_VERSION = "lts/*";
11
16
  const NPM_REGISTRY_URL = "https://registry.npmjs.org";
@@ -20,6 +25,22 @@ export type ReleaseStage = "all" | "node" | "python" | "docs";
20
25
  export interface ReleaseDocsOptions {
21
26
  readonly siteUrl: string;
22
27
  readonly base?: string;
28
+ /** Repository-defined setup and generation steps run before saving the Bun cache. */
29
+ readonly prepareSteps: readonly JobStep[];
30
+ /** Repository-defined documentation build and validation steps. */
31
+ readonly buildSteps: readonly JobStep[];
32
+ /** Directory uploaded as the GitHub Pages artifact. */
33
+ readonly artifactPath: string;
34
+ }
35
+
36
+ /** Optional local AI summary generation during reviewed release preparation. */
37
+ export interface ReleaseSummaryOptions {
38
+ /**
39
+ * Provider fallback order.
40
+ *
41
+ * @default ["cursor", "codex", "claude"]
42
+ */
43
+ readonly providers?: readonly ReleaseSummaryProviderName[];
23
44
  }
24
45
 
25
46
  /** Options for {@link DBXToolsRelease}. */
@@ -30,6 +51,13 @@ export interface DBXToolsReleaseOptions {
30
51
  readonly nodeRelease?: boolean;
31
52
  /** Build and deploy generated documentation through GitHub Pages. */
32
53
  readonly docs?: ReleaseDocsOptions;
54
+ /** Python package root passed to local release preparation. */
55
+ readonly pythonRoot?: string;
56
+ /**
57
+ * Generate a versioned release summary locally. Defaults to enabled with the
58
+ * standard Cursor, Codex, Claude fallback order.
59
+ */
60
+ readonly summary?: boolean | ReleaseSummaryOptions;
33
61
  }
34
62
 
35
63
  /** Locate the unified workflow so attached language workspaces can add jobs. */
@@ -39,6 +67,11 @@ export function releaseWorkflow(project: DBXToolsJavaScriptProject): GithubWorkf
39
67
  return workflow;
40
68
  }
41
69
 
70
+ /** Locate the unified workflow when release generation is enabled. */
71
+ export function tryReleaseWorkflow(project: DBXToolsJavaScriptProject): GithubWorkflow | undefined {
72
+ return releaseWorkflows.get(project);
73
+ }
74
+
42
75
  /** Whether the unified workflow publishes the normal npm workspace. */
43
76
  export function hasNodeRelease(project: DBXToolsJavaScriptProject): boolean {
44
77
  return nodeReleaseProjects.has(project);
@@ -98,7 +131,7 @@ export function releaseArtifactSteps(options: {
98
131
  export function nodeReleaseSetupSteps(project: DBXToolsJavaScriptProject): readonly JobStep[] {
99
132
  return [
100
133
  ...releaseSourceSteps(),
101
- ...bunCacheRestoreSteps(project),
134
+ ...bunCacheRestoreSteps(project, { ignorePaths: project.workflowCacheIgnorePaths }),
102
135
  {
103
136
  name: "Setup Node.js",
104
137
  uses: "actions/setup-node@v6",
@@ -270,44 +303,14 @@ function addDocsJobs(
270
303
  },
271
304
  steps: [
272
305
  ...releaseSourceSteps(),
273
- ...bunCacheRestoreSteps(project),
274
- {
275
- name: "Setup Node.js",
276
- uses: "actions/setup-node@v6",
277
- with: { "node-version": "22" },
278
- },
279
- {
280
- name: "Setup Python",
281
- uses: "actions/setup-python@v6",
282
- with: { "python-version": "3.11" },
283
- },
284
- { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
285
- { name: "Configure Pages", uses: "actions/configure-pages@v5" },
286
- { name: "Install dependencies", run: "bun install" },
287
- {
288
- name: "Validate public source documentation",
289
- run: "bun docs/scripts/check-source-docs.mjs",
290
- },
291
- { name: "Generate docs from READMEs", run: "bun docs/scripts/sync-readmes.mjs" },
292
- { name: "Install docs dependencies", run: "bun install --cwd .docs-build/site" },
306
+ ...bunCacheRestoreSteps(project, { ignorePaths: project.workflowCacheIgnorePaths }),
307
+ ...options.prepareSteps,
293
308
  bunCacheSaveStep(),
294
- {
295
- name: "Generate API docs",
296
- run: "bun docs/scripts/generate-api-docs.mjs",
297
- },
298
- {
299
- name: "Check generated titles",
300
- run: "bun docs/scripts/check-generated-titles.mjs",
301
- },
302
- { name: "Build docs", run: "bun run --cwd .docs-build/site build" },
303
- {
304
- name: "Check generated links",
305
- run: "bun run --cwd .docs-build/site check-links",
306
- },
309
+ ...options.buildSteps,
307
310
  {
308
311
  name: "Upload Pages artifact",
309
312
  uses: "actions/upload-pages-artifact@v4",
310
- with: { path: ".docs-build/dist" },
313
+ with: { path: options.artifactPath },
311
314
  },
312
315
  ],
313
316
  });
@@ -337,6 +340,13 @@ export class DBXToolsRelease extends Component {
337
340
  super(project);
338
341
  const tagPrefix = options.tagPrefix ?? "v";
339
342
  const releaseBranch = projectReleaseBranch(project);
343
+ const summary = options.summary ?? true;
344
+ const summaryArgs =
345
+ summary === false || (typeof summary === "object" && summary.providers?.length === 0)
346
+ ? ["--no-release-summary"]
347
+ : typeof summary === "object" && summary.providers
348
+ ? [`--release-summary-providers ${summary.providers.join(",")}`]
349
+ : [];
340
350
  releaseTagPrefixes.set(project, tagPrefix);
341
351
  if (options.nodeRelease !== false) nodeReleaseProjects.add(project);
342
352
  applyTasks(project, {
@@ -355,7 +365,14 @@ export class DBXToolsRelease extends Component {
355
365
  exec: taskScript(
356
366
  project,
357
367
  "release-pr.ts",
358
- `--prefix ${tagPrefix} --base ${releaseBranch}`,
368
+ [
369
+ `--prefix ${tagPrefix}`,
370
+ `--base ${releaseBranch}`,
371
+ ...(options.pythonRoot
372
+ ? [`--python-root ${JSON.stringify(options.pythonRoot)}`]
373
+ : []),
374
+ ...summaryArgs,
375
+ ].join(" "),
359
376
  ),
360
377
  receiveArgs: true,
361
378
  description: "Prepare, validate, locally publish, and open a reviewed release PR",
@@ -370,8 +387,10 @@ export class DBXToolsRelease extends Component {
370
387
  limitConcurrency: true,
371
388
  concurrencyOptions: { group: "release", cancelInProgress: false },
372
389
  });
390
+ const version = readWorkspaceVersion(project.outdir);
373
391
  workflow.runName =
374
- "release ${{ github.event_name == 'push' && github.sha || inputs.release_tag }}";
392
+ `release ${version} ` +
393
+ "${{ github.event_name == 'push' && github.sha || inputs.release_tag }}";
375
394
  workflow.on({
376
395
  push: { branches: [releaseBranch], paths: ["VERSION"] },
377
396
  workflowDispatch: {
package/src/tsconfig.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Root `tsconfig.base.json` and `tsconfig.json` for the projenrc program.
3
3
  */
4
- import { Component, JsonFile, type Project, javascript } from "projen";
4
+ import { Component, type Project, javascript, typescript } from "projen";
5
5
 
6
6
  /**
7
7
  * Compiler options for the ROOT program only (`.projenrc.ts` + the engine as
@@ -37,24 +37,24 @@ export class DBXToolsRootTsconfig extends Component {
37
37
  constructor(scope: Project) {
38
38
  super(scope);
39
39
 
40
- new JsonFile(scope, "tsconfig.base.json", {
41
- marker: true,
42
- readonly: true,
43
- obj: { compilerOptions: BASE_COMPILER_OPTIONS },
44
- });
40
+ if (scope instanceof typescript.TypeScriptProject) {
41
+ return;
42
+ }
45
43
 
46
- new JsonFile(scope, "tsconfig.json", {
47
- marker: true,
48
- readonly: true,
49
- obj: {
50
- extends: "./tsconfig.base.json",
51
- compilerOptions: {
52
- lib: ["ESNext"],
53
- types: ["node"],
54
- } satisfies javascript.TypeScriptCompilerOptions,
55
- include: [".projenrc.ts"],
56
- exclude: ["node_modules", "**/dist", "**/node_modules"],
44
+ const base = new javascript.TypescriptConfig(scope, {
45
+ fileName: "tsconfig.base.json",
46
+ compilerOptions: BASE_COMPILER_OPTIONS,
47
+ });
48
+ base.file.readonly = true;
49
+ const root = new javascript.TypescriptConfig(scope, {
50
+ extends: javascript.TypescriptConfigExtends.fromTypescriptConfigs([base]),
51
+ compilerOptions: {
52
+ lib: ["ESNext"],
53
+ types: ["node"],
57
54
  },
55
+ include: [".projenrc.ts"],
56
+ exclude: ["node_modules", "**/dist", "**/node_modules"],
58
57
  });
58
+ root.file.readonly = true;
59
59
  }
60
60
  }
@@ -16,7 +16,7 @@ export interface LocalPublishOptions {
16
16
  readonly version: string;
17
17
  readonly localRegistry: string;
18
18
  readonly localPypi: string;
19
- readonly pythonRoot: string;
19
+ readonly pythonRoot?: string;
20
20
  readonly localCargo: boolean;
21
21
  }
22
22
 
@@ -50,7 +50,7 @@ export async function publishLocalRelease(options: LocalPublishOptions): Promise
50
50
  const localRegistry = resolveLocalRegistry(options.localRegistry);
51
51
  const activeIndexes = activePythonIndexes();
52
52
  const localPypi = resolveLocalPypi(options.localPypi, activeIndexes);
53
- const pythonRoot = resolve(options.root, options.pythonRoot);
53
+ const pythonRoot = options.pythonRoot ? resolve(options.root, options.pythonRoot) : undefined;
54
54
  const localPublishes: Promise<unknown>[] = [];
55
55
 
56
56
  if (
@@ -79,7 +79,7 @@ export async function publishLocalRelease(options: LocalPublishOptions): Promise
79
79
  );
80
80
  }
81
81
 
82
- if (localPypi && existsSync(pythonRoot)) {
82
+ if (localPypi && pythonRoot && existsSync(pythonRoot)) {
83
83
  const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
84
84
  logger.info(`publishing Python ${options.version} to local devpi ${localPypi.publishUrl}`);
85
85
  localPublishes.push(