@dbx-tools/projen 0.9.38 → 0.9.43

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/README.md CHANGED
@@ -58,16 +58,16 @@ workflow.
58
58
  Repository policy stays in the consuming `.projenrc.ts`:
59
59
 
60
60
  - `releaseDocs` supplies repository-defined preparation and build steps plus
61
- the Pages artifact path. The engine adds release checkout, Bun caching,
62
- artifact upload, and deployment without naming a docs script or output tree.
61
+ the Pages artifact path. The engine adds release checkout, artifact upload,
62
+ and deployment without naming a docs script or output tree.
63
63
  - `releasePythonRoot` passes the actual Python package root to local release
64
64
  preparation. Omit it when the workspace has no standard Python packages.
65
65
  - `releaseValidationTasks` names repository tasks that must pass in tag CI
66
66
  before release artifacts are built and published.
67
+ - `releaseSetupSteps` installs any extra validation/build prerequisites in the
68
+ shared release job before the validation tasks run.
67
69
  - `pullRequestTitlePolicy` configures semantic title types and scope policy.
68
70
  Omit it or pass `false` to disable the title job.
69
- - `workflowCacheIgnorePaths` excludes generated output trees that may contain
70
- package manifests from the dependency-only Bun cache key.
71
71
  - `extraWorkspaceMembers` declares self-synthesizing tooling packages outside
72
72
  `packageRoots`. Their version, generated entrypoint, formatting, linting, and
73
73
  workspace membership are derived from that declaration.
@@ -380,17 +380,45 @@ list, so a new package is covered without a re-synth. Work from the root:
380
380
  | `bun run version:check` | verify every generated version against `VERSION` |
381
381
  | `bun run release` | run the configured release transaction |
382
382
 
383
- Run exactly `bun run release` from any branch. Pending changes are committed and
383
+ Run `bun run release` from any branch. Pending changes are committed and
384
384
  the current branch is pushed first. A non-`main` branch then fast-forwards
385
385
  `main`; the command fails without creating a merge commit if either branch has
386
386
  diverged. From `main`, it calls the existing `bump` task, commits the generated
387
387
  version changes, pushes `main`, and pushes the matching annotated `vX.Y.Z` tag.
388
388
  Pass `--no-bump` only to release an existing synchronized local bump. The tag
389
- workflow verifies that the tag points at the exact `origin/main` commit,
390
- publishes npm and PyPI directly from that checkout, then builds and deploys
391
- documentation. Local npm and Python registry publication remains available
389
+ workflow verifies that the tag points at the exact `origin/main` commit. One
390
+ `build-release` job installs workspace dependencies, runs validation, packs the
391
+ npm archives, builds all Python distributions, and builds the documentation
392
+ site. Install validation prerequisites through `releaseSetupSteps`;
393
+ `releaseDocs.prepareSteps` runs after validation.
394
+
395
+ Publication jobs download the resulting artifacts rather than rebuilding the
396
+ workspace. Each Python package retains its own publishing environment and waits
397
+ for its package dependencies to publish. The npm job uses the bundled archive
398
+ publisher without a checkout or workspace install. Pages deploys the already
399
+ built site after registry publication succeeds. Local npm and Python registry publication remains available
392
400
  through the direct local publication tooling.
393
401
 
402
+ The default release still publishes npm, Python, and docs, then publishes to
403
+ configured local registries automatically. Select a scope or disable individual
404
+ steps for one run without changing the Projen definition:
405
+
406
+ ```sh
407
+ bun run release --publish pypi --no-docs
408
+ bun run release --no-npm --no-local-publish
409
+ bun run release --publish local --install never
410
+ bun run release --local-registry false --local-pypi auto
411
+ ```
412
+
413
+ `--install auto` keeps Projen's normal local dependency-install behavior;
414
+ `always` performs a frozen install first, and `never` uses dependencies already
415
+ installed while still regenerating versioned sources. `--no-validation` skips
416
+ optional task checks locally and in CI, not version or immutable-source checks.
417
+ `--no-docs` skips building and deploying the site, not package README validation.
418
+ Scopes select artifacts, not the normal commit and tag-push transaction.
419
+ The annotated tag records CI step selections, so publishing a selected scope does
420
+ not change the next release's defaults. Local registry URLs stay local.
421
+
394
422
  Members intentionally keep only the tasks that something OTHER than a human
395
423
  invokes, so there is no second place to run the same thing:
396
424
 
@@ -402,7 +430,7 @@ invokes, so there is no second place to run the same thing:
402
430
  edit/compile loop to one package.
403
431
  - `build` / `package` - a complete compile/test/pack lifecycle when invoked in
404
432
  one package. Release preparation validates through the root's filtered tasks,
405
- and publication concurrently uploads archives that were each packed and
433
+ and publication uploads archives that were each packed and
406
434
  validated once with lifecycle scripts disabled. The package phase also packs
407
435
  with `--ignore-scripts` because its build already compiled; `prepack` remains
408
436
  available for a standalone publish that did not run `build` first.
@@ -421,3 +449,39 @@ from the root instead.
421
449
  Projen-owned and reproduce it exactly across JavaScript and Python packages.
422
450
  Release jobs publish the generated workspace inventory in dependency-safe
423
451
  workflow order without introducing another version owner.
452
+
453
+ <!-- cli-reference:start -->
454
+
455
+ ## Command Reference
456
+
457
+ ### `release`
458
+
459
+ Prepare an annotated release and select its build and publication steps
460
+
461
+ ```sh
462
+ release [options]
463
+ ```
464
+
465
+ #### Options
466
+
467
+ | Option | Description |
468
+ | ------------------------------------- | --------------------------------------------------------------------------------------------- |
469
+ | `--root <path>` | repository root |
470
+ | `--branch <name>` | release branch (default: "main") |
471
+ | `--prefix <prefix>` | release tag prefix (default: "v") |
472
+ | `--remote <name>` | git remote (default: "origin") |
473
+ | `--python-root <path>` | Python package root for local publish (default: "packages/py") |
474
+ | `--validate <task>` | task to run before pushing (default: []) |
475
+ | `--no-bump` | use an existing synchronized local version bump |
476
+ | `--publish <target>` | publication scope (choices: "auto", "npm", "pypi", "local", "none", default: "auto") |
477
+ | `--install <mode>` | local workspace dependency installation (choices: "auto", "always", "never", default: "auto") |
478
+ | `--no-npm` | skip npm build and publication, including local npm publication |
479
+ | `--no-pypi` | skip Python build and publication, including local Python publication |
480
+ | `--docs` | build and deploy docs for a selected scope |
481
+ | `--no-docs` | skip documentation build and deployment |
482
+ | `--no-validation` | skip optional release validation tasks; version/source checks remain mandatory |
483
+ | `--no-local-publish` | skip publishing to configured local registries |
484
+ | `--local-registry <auto\|false\|url>` | local npm registry selection (default: "auto") |
485
+ | `--local-pypi <auto\|false\|url>` | local devpi registry selection (default: "auto") |
486
+
487
+ <!-- cli-reference:end -->
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.9.38";
6
+ export const PACKAGE_VERSION = "0.9.43";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -27,6 +27,7 @@ export * as pythonNodeBindings from "./src/python-node-bindings.ts";
27
27
  export * as pythonNodeBundle from "./src/python-node-bundle.ts";
28
28
  export * as release from "./src/release.ts";
29
29
  export * as releaseContext from "./src/release-context.ts";
30
+ export * as releaseOptions from "./src/release-options.ts";
30
31
  export * as scaffold from "./src/scaffold.ts";
31
32
  export * as tags from "./src/tags.ts";
32
33
  export * as tsconfig from "./src/tsconfig.ts";
@@ -36,7 +37,6 @@ export * as workspaceLock from "./src/workspace-lock.ts";
36
37
  export * as workspaceVersion from "./src/workspace-version.ts";
37
38
  export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
38
39
  export { BUN_VERSION } from "./src/bun-workflow.ts";
39
- export type { BunWorkflowCacheOptions } from "./src/bun-workflow.ts";
40
40
  export { DBXToolsConfig } from "./src/dbx-tools-config.ts";
41
41
  export type { DBXToolsConfigOptions } from "./src/dbx-tools-config.ts";
42
42
  export { resolvePkgRoot } from "./src/engine-root.ts";
@@ -60,7 +60,9 @@ export { PythonNodeBundle } from "./src/python-node-bundle.ts";
60
60
  export type { PythonNodeFunctionOverride, PythonNodeBindingsOptions, PythonNodeBundleOptions } from "./src/python-node-bundle.ts";
61
61
  export { DBXToolsRelease } from "./src/release.ts";
62
62
  export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
63
- export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION } from "./src/release-context.ts";
63
+ export { RELEASE_VERSION } from "./src/release-context.ts";
64
+ export { RELEASE_PUBLISH_TARGETS, RELEASE_INSTALL_MODES } from "./src/release-options.ts";
65
+ export type { ReleasePublishTarget, ReleaseInstallMode, ReleaseStepSelection, ReleaseSelectionOptions } from "./src/release-options.ts";
64
66
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
65
67
  export type { PackageTag } from "./src/tags.ts";
66
68
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
package/package.json CHANGED
@@ -32,9 +32,9 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@clack/prompts": "^1.7.0",
35
- "@dbx-tools/core": "^0.9.38",
36
- "@dbx-tools/path": "^0.9.38",
37
- "@dbx-tools/shared-core": "^0.9.38",
35
+ "@dbx-tools/core": "^0.9.43",
36
+ "@dbx-tools/path": "^0.9.43",
37
+ "@dbx-tools/shared-core": "^0.9.43",
38
38
  "commander": "^15.0.0",
39
39
  "concurrently": "^10.0.3",
40
40
  "constructs": "^10.6.0",
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "main": "index.ts",
58
58
  "license": "Apache-2.0",
59
- "version": "0.9.38",
59
+ "version": "0.9.43",
60
60
  "types": "index.ts",
61
61
  "type": "module",
62
62
  "exports": {
@@ -1,172 +1,12 @@
1
- /** Shared Bun setup and package-cache steps for generated workflows. */
2
- import { stringUtils } from "@dbx-tools/shared-core";
3
- import { TextFile, javascript } from "projen";
1
+ /** Shared Bun setup for generated workflows. */
4
2
 
5
3
  export const BUN_VERSION = "1.3.14";
6
4
 
7
- const DEFAULT_CACHE_IGNORE_PATHS = [
8
- ".git",
9
- ".venv",
10
- ".worktrees",
11
- "coverage",
12
- "dist",
13
- "lib",
14
- "node_modules",
15
- "target",
16
- ] as const;
17
-
18
- function cacheKeyScript(extraIgnorePaths: readonly string[]): string {
19
- const ignorePaths = [...new Set(extraIgnorePaths)].sort();
20
- // prettier-ignore
21
- const source = (
22
- // ============================================================================
23
- /*js*/`
24
- #!/usr/bin/env node
25
- import { createHash } from "node:crypto";
26
- import { readdirSync, readFileSync } from "node:fs";
27
- import { join, sep } from "node:path";
28
-
29
- const root = process.cwd();
30
- const ignoredNames = new Set(${JSON.stringify(DEFAULT_CACHE_IGNORE_PATHS)});
31
- const ignoredPaths = new Set(${JSON.stringify(ignorePaths)});
32
- const manifests = [];
33
- const walk = (directory) => {
34
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
35
- const path = join(directory, entry.name);
36
- const relativePath = path.slice(root.length + 1).split(sep).join("/");
37
- if (ignoredNames.has(entry.name) || ignoredPaths.has(relativePath)) continue;
38
- if (entry.isDirectory()) walk(path);
39
- else if (entry.name === "package.json") manifests.push(path);
40
- }
41
- };
42
- walk(root);
43
-
44
- const dependencyFields = [
45
- "catalog",
46
- "dependencies",
47
- "devDependencies",
48
- "optionalDependencies",
49
- "overrides",
50
- "peerDependencies",
51
- "peerDependenciesMeta",
52
- "resolutions",
53
- "trustedDependencies",
54
- ];
55
- const canonical = (value) => {
56
- if (Array.isArray(value)) return value.map(canonical);
57
- if (!value || typeof value !== "object") return value;
58
- return Object.fromEntries(
59
- Object.entries(value)
60
- .sort(([left], [right]) => left.localeCompare(right))
61
- .map(([key, child]) => [key, canonical(child)]),
62
- );
63
- };
64
- const dependencies = manifests
65
- .sort()
66
- .map((path) => {
67
- const manifest = JSON.parse(readFileSync(path, "utf8"));
68
- return [
69
- path.slice(root.length + 1),
70
- Object.fromEntries(
71
- dependencyFields
72
- .filter((field) => manifest[field] !== undefined)
73
- .map((field) => [field, canonical(manifest[field])]),
74
- ),
75
- ];
76
- });
77
- process.stdout.write(createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"));
78
- `
79
- // ============================================================================
80
- );
81
- return stringUtils.dedent(source, { trimEnd: false });
82
- }
83
-
84
- const configured = new WeakMap<javascript.NodeProject, string>();
85
-
86
- function ensureCacheKeyScript(
87
- project: javascript.NodeProject,
88
- ignorePaths: readonly string[],
89
- ): void {
90
- const root = project.root as javascript.NodeProject;
91
- const key = JSON.stringify([...new Set(ignorePaths)].sort());
92
- const existing = configured.get(root);
93
- if (existing !== undefined) {
94
- if (existing !== key) {
95
- throw new Error("Bun workflow cache ignore paths must be consistent across one project");
96
- }
97
- return;
98
- }
99
- new TextFile(root, ".projen/bun-cache-key.mjs", {
100
- lines: cacheKeyScript(ignorePaths).trimEnd().split("\n"),
101
- });
102
- configured.set(root, key);
103
- }
104
-
105
- function stepCondition(condition: string | undefined, cacheMiss = false): string | undefined {
106
- const expressions = [
107
- condition,
108
- ...(cacheMiss ? ["steps.bun_cache.outputs.cache-hit != 'true'"] : []),
109
- ].filter(Boolean);
110
- return expressions.length ? `\${{ ${expressions.join(" && ")} }}` : undefined;
111
- }
112
-
113
- export interface BunWorkflowCacheOptions {
114
- readonly setupCondition?: string;
115
- readonly condition?: string;
116
- /** Repository-relative output paths excluded from dependency manifest hashing. */
117
- readonly ignorePaths?: readonly string[];
118
- }
119
-
120
- /** Set up Bun and restore its global package cache. */
121
- export function bunCacheRestoreSteps(
122
- project: javascript.NodeProject,
123
- options: BunWorkflowCacheOptions = {},
124
- ): readonly Record<string, unknown>[] {
125
- ensureCacheKeyScript(project, options.ignorePaths ?? []);
126
- const condition = stepCondition(options.condition);
127
- const setupCondition = stepCondition(options.setupCondition);
128
- return [
129
- {
130
- name: "Setup Bun",
131
- ...(setupCondition ? { if: setupCondition } : {}),
132
- uses: "oven-sh/setup-bun@v2",
133
- with: { "bun-version": "${{ env.BUN_VERSION }}" },
134
- },
135
- {
136
- name: "Resolve Bun cache",
137
- id: "bun_cache_metadata",
138
- ...(condition ? { if: condition } : {}),
139
- shell: "bash",
140
- run: [
141
- 'echo "path=$(bun pm cache)" >> "$GITHUB_OUTPUT"',
142
- 'echo "dependency_hash=$(node .projen/bun-cache-key.mjs)" >> "$GITHUB_OUTPUT"',
143
- ].join("\n"),
144
- },
145
- {
146
- name: "Restore Bun cache",
147
- id: "bun_cache",
148
- ...(condition ? { if: condition } : {}),
149
- uses: "actions/cache/restore@v5",
150
- with: {
151
- path: "${{ steps.bun_cache_metadata.outputs.path }}",
152
- key: `bun-\${{ runner.os }}-\${{ runner.arch }}-\${{ env.BUN_VERSION }}-\${{ steps.bun_cache_metadata.outputs.dependency_hash }}`,
153
- "restore-keys": `bun-\${{ runner.os }}-\${{ runner.arch }}-\${{ env.BUN_VERSION }}-`,
154
- },
155
- },
156
- ];
157
- }
158
-
159
- /** Save Bun's global package cache immediately after installation. */
160
- export function bunCacheSaveStep(
161
- options: BunWorkflowCacheOptions = {},
162
- ): Readonly<Record<string, unknown>> {
5
+ /** Install the pinned Bun version. */
6
+ export function bunSetupStep(): Readonly<Record<string, unknown>> {
163
7
  return {
164
- name: "Save Bun cache",
165
- if: stepCondition(options.condition, true),
166
- uses: "actions/cache/save@v5",
167
- with: {
168
- path: "${{ steps.bun_cache_metadata.outputs.path }}",
169
- key: "${{ steps.bun_cache.outputs.cache-primary-key }}",
170
- },
8
+ name: "Setup Bun",
9
+ uses: "oven-sh/setup-bun@v2",
10
+ with: { "bun-version": "${{ env.BUN_VERSION }}" },
171
11
  };
172
12
  }
package/src/codegen.ts CHANGED
@@ -316,9 +316,9 @@ function generatePackage(
316
316
  const sourceText = preprocess(stripImports(tsRuntime, sourcePath));
317
317
  const { getZodSchemasFile, getInferredTypes, errors } = generate({
318
318
  sourceText,
319
- // Carry JSDoc through so the `@description` tag stays visible alongside
320
- // the matching `.describe(...)`.
321
- keepComments: true,
319
+ // JSDoc is rewritten to `@description` so ts-to-zod emits `.describe()`.
320
+ // Keep the generated module comment-free; Zod owns the prose.
321
+ keepComments: false,
322
322
  });
323
323
  if (errors.length) {
324
324
  for (const err of errors) logger.warn(`${destination} from ${source}: ${err}`);
package/src/project-js.ts CHANGED
@@ -18,7 +18,7 @@ import type { ReleaseProjectOptions } from "projen/lib/release";
18
18
  import { generateBarrels } from "./barrels.ts";
19
19
 
20
20
  import { BUN_APP_OVERRIDES, RootBunfigFile } from "./bun-app.ts";
21
- import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
21
+ import { BUN_VERSION, bunSetupStep } from "./bun-workflow.ts";
22
22
  import { codegenModulePaths, generateCodegen } from "./codegen.ts";
23
23
  import { DBXToolsConfig, type DBXToolsConfigOptions } from "./dbx-tools-config.ts";
24
24
  import { resolvePkgRoot } from "./engine-root.ts";
@@ -61,8 +61,6 @@ export interface DBXToolsJavaScriptProject extends DBXToolsProject, javascript.N
61
61
  pnpmWorkspace?: PnpmWorkspaceState;
62
62
  /** Root projenrc tsconfigs - only a tree ROOT has one. */
63
63
  rootTsconfig?: DBXToolsRootTsconfig;
64
- /** Repository outputs excluded from generated workflow dependency-cache hashing. */
65
- readonly workflowCacheIgnorePaths: readonly string[];
66
64
  }
67
65
 
68
66
  /** Parsed npm package identifier: optional scope plus the unscoped package name. */
@@ -573,14 +571,14 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
573
571
  readonly releasePythonRoot?: string;
574
572
  /** Repository task names run during local release preparation before publication. */
575
573
  readonly releaseValidationTasks?: readonly string[];
574
+ /** Release build prerequisites installed before repository validation tasks. */
575
+ readonly releaseSetupSteps?: readonly JobStep[];
576
576
  /** Set to `false` to omit normal npm workspace publication. */
577
577
  readonly nodeRelease?: boolean;
578
578
  /** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
579
579
  readonly releaseMode?: DBXToolsReleaseMode;
580
580
  /** Prefix for generated release tags. Defaults to `v`. */
581
581
  readonly releaseTagPrefix?: string;
582
- /** Repository output paths excluded from generated workflow dependency-cache hashing. */
583
- readonly workflowCacheIgnorePaths?: readonly string[];
584
582
  /**
585
583
  * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
586
584
  * config ALONGSIDE the discovered `packageRoots` members - for a package that
@@ -607,16 +605,6 @@ export type DBXToolsTypeScriptProjectOptions = Partial<
607
605
  > &
608
606
  DBXToolsJavaScriptProjectOptions;
609
607
 
610
- const initializingWorkflowCachePaths = new Map<string, readonly string[]>();
611
-
612
- function configuredOutdir(
613
- options: Pick<DBXToolsJavaScriptProjectOptions, "parent" | "outdir">,
614
- ): string {
615
- return options.parent
616
- ? resolve(options.parent.outdir, options.outdir ?? ".")
617
- : resolve(options.outdir ?? process.cwd());
618
- }
619
-
620
608
  /**
621
609
  * A monorepo root. Scans `packageRoots` and appends a
622
610
  * {@link DBXToolsTypeScriptProject} per `src`-bearing folder, then emits the
@@ -633,7 +621,6 @@ export class DBXToolsNodeProject
633
621
  rootTsconfig?: DBXToolsRootTsconfig;
634
622
  readonly extraWorkspaceMembers: readonly string[];
635
623
  readonly releaseBranch: string;
636
- readonly workflowCacheIgnorePaths: readonly string[];
637
624
  private readonly rootInstallOnly: boolean;
638
625
 
639
626
  constructor(options: DBXToolsJavaScriptProjectOptions = {}) {
@@ -646,8 +633,6 @@ export class DBXToolsNodeProject
646
633
  // into `package.json` (`workspaces`/`catalog`) for bun to read. The
647
634
  // `pnpm-workspace.yaml` is still emitted for the Databricks Apps pnpm install.
648
635
  const pnpmWorkspace = new PnpmWorkspaceState(options);
649
- const outdir = configuredOutdir(options);
650
- initializingWorkflowCachePaths.set(outdir, options.workflowCacheIgnorePaths ?? []);
651
636
  super({
652
637
  ...defaultProjectOptions(options),
653
638
  pnpmOptions: {
@@ -666,8 +651,6 @@ export class DBXToolsNodeProject
666
651
  this.scope = scope;
667
652
  this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
668
653
  this.releaseBranch = options.defaultReleaseBranch ?? "main";
669
- this.workflowCacheIgnorePaths = options.workflowCacheIgnorePaths ?? [];
670
- initializingWorkflowCachePaths.delete(outdir);
671
654
  this.rootInstallOnly = options.rootInstallOnly !== false;
672
655
  this.dbxToolsConfig = new DBXToolsConfig(this, options);
673
656
  initProject(this, options);
@@ -676,28 +659,10 @@ export class DBXToolsNodeProject
676
659
  public override renderWorkflowSetup(options?: javascript.RenderWorkflowSetupOptions): JobStep[] {
677
660
  const steps = super.renderWorkflowSetup(options);
678
661
  if (this.parent) return steps;
679
- return steps.flatMap((step) => {
680
- if (step.uses?.startsWith("oven-sh/setup-bun@")) {
681
- return [
682
- ...bunCacheRestoreSteps(this, {
683
- ignorePaths:
684
- this.workflowCacheIgnorePaths ??
685
- initializingWorkflowCachePaths.get(resolve(this.outdir)) ??
686
- [],
687
- }),
688
- ];
689
- }
690
- if (step.run?.startsWith("bun install")) {
691
- return [
692
- step,
693
- bunCacheSaveStep({
694
- condition:
695
- "github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository",
696
- }),
697
- ];
698
- }
699
- return [step];
700
- }) as JobStep[];
662
+ return [
663
+ bunSetupStep() as JobStep,
664
+ ...steps.filter((step) => !step.uses?.startsWith("oven-sh/setup-bun@")),
665
+ ];
701
666
  }
702
667
 
703
668
  public override preSynthesize(): void {
@@ -752,7 +717,6 @@ export class DBXToolsTypeScriptProject
752
717
  rootTsconfig?: DBXToolsRootTsconfig;
753
718
  readonly extraWorkspaceMembers: readonly string[];
754
719
  readonly releaseBranch: string;
755
- readonly workflowCacheIgnorePaths: readonly string[];
756
720
  private readonly rootInstallOnly: boolean;
757
721
 
758
722
  constructor(options: DBXToolsTypeScriptProjectOptions) {
@@ -793,7 +757,6 @@ export class DBXToolsTypeScriptProject
793
757
  this.scope = scope;
794
758
  this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
795
759
  this.releaseBranch = options.defaultReleaseBranch ?? "main";
796
- this.workflowCacheIgnorePaths = options.workflowCacheIgnorePaths ?? [];
797
760
  this.rootInstallOnly = options.rootInstallOnly !== false;
798
761
  // Pairs with `jsx` in SHARED_COMPILER_OPTIONS: projen's default `include` is
799
762
  // `src/**/*.ts` only, which silently omits a `.tsx` file from the program
@@ -1365,6 +1328,7 @@ function initProject(
1365
1328
  docs: options.releaseDocs,
1366
1329
  pythonRoot: options.releasePythonRoot,
1367
1330
  validationTasks: options.releaseValidationTasks,
1331
+ setupSteps: options.releaseSetupSteps,
1368
1332
  });
1369
1333
  }
1370
1334
  }
package/src/project-py.ts CHANGED
@@ -2,16 +2,15 @@
2
2
  import * as projectUtils from "@dbx-tools/core/project-utils";
3
3
  import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
4
4
  import { Component, License, TextFile, type Project, javascript, python } from "projen";
5
- import { JobPermission } from "projen/lib/github/workflows-model";
6
- import { BUN_VERSION } from "./bun-workflow.ts";
5
+ import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
7
6
  import { DBX_TOOLS_LICENSE, projectRepositoryUrl } from "./project-js.ts";
8
7
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
9
8
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
10
9
  import { PythonNodeBundle, type PythonNodeBindingsOptions } from "./python-node-bundle.ts";
11
- import { RELEASE_VERSION, releaseSourceSteps } from "./release-context.ts";
12
10
  import {
13
11
  refreshReleaseDocsDependencies,
14
12
  releaseCondition,
13
+ releaseBuildSteps,
15
14
  releaseTagPattern,
16
15
  tryReleaseWorkflow,
17
16
  uvSetupStep,
@@ -88,6 +87,8 @@ export interface DBXToolsPythonWorkspaceOptions {
88
87
  readonly devDependencies?: readonly string[];
89
88
  readonly testPaths?: readonly string[];
90
89
  readonly lintPaths?: readonly string[];
90
+ /** Repository-relative paths Ruff must not lint or format. */
91
+ readonly ruffExcludes?: readonly string[];
91
92
  readonly ruffPerFileIgnores?: Readonly<Record<string, readonly string[]>>;
92
93
  /** Generated Python implementation files Pyrefly should resolve but not type-check. */
93
94
  readonly pyreflyProjectExcludes?: readonly string[];
@@ -348,6 +349,17 @@ export class DBXToolsPythonWorkspace extends Component {
348
349
  if (options.indexStrategy) {
349
350
  file.addOverride("tool.uv.index-strategy", options.indexStrategy);
350
351
  }
352
+ const ruffExcludes = [
353
+ ...(options.ruffExcludes ?? []),
354
+ ...options.packages.flatMap((pkg) =>
355
+ pythonNodeGeneratedSources(pkg).map(
356
+ (source) => `${this.repository.root}/${pkg.directory}/${source}`,
357
+ ),
358
+ ),
359
+ ];
360
+ if (ruffExcludes.length) {
361
+ file.addOverride("tool.ruff.exclude", [...new Set(ruffExcludes)]);
362
+ }
351
363
  file.addOverride("tool.pyrefly.ignore-errors-in-generated-code", true);
352
364
  const projectExcludes = [
353
365
  ...(options.pyreflyProjectExcludes ?? []),
@@ -401,16 +413,45 @@ export class DBXToolsPythonWorkspace extends Component {
401
413
  if (!workflow) {
402
414
  throw new Error("Python release requires the root dbx-tools release mode");
403
415
  }
416
+ const build = workflow.getJob("build-release") as Job | undefined;
417
+ if (!build) throw new Error("Python release requires the shared release build job");
418
+ const pythonSteps: JobStep[] = [
419
+ uvSetupStep(),
420
+ {
421
+ name: "Build Python distributions",
422
+ env: { RELEASE_VERSION: "${{ steps.release.outputs.release_version }}" },
423
+ run: [
424
+ 'bun node_modules/@dbx-tools/projen/tasks/publish-python.ts "$RELEASE_VERSION"',
425
+ `--root ${JSON.stringify(this.repository.root)}`,
426
+ `--package ${allPublications.map((publication) => JSON.stringify(publication.directory)).join(" ")}`,
427
+ "--output .release/python --package-directories",
428
+ ].join(" \\\n "),
429
+ },
430
+ ...allPublications.map((publication) => ({
431
+ name: `Upload ${publication.distribution} distributions`,
432
+ uses: "actions/upload-artifact@v4",
433
+ with: {
434
+ name: `release-python-${publication.directory}`,
435
+ path: `.release/python/${publication.directory}`,
436
+ "if-no-files-found": "error",
437
+ },
438
+ })),
439
+ ];
440
+ workflow.updateJob("build-release", {
441
+ ...build,
442
+ steps: [...build.steps, ...releaseBuildSteps("pypi", pythonSteps)],
443
+ });
404
444
  for (const publication of allPublications) {
405
445
  const dependencyJobs = (publication.dependencies ?? []).map(
406
446
  (dependency) => `publish-pypi-${dependency}`,
407
447
  );
408
448
  workflow.addJob(`publish-pypi-${publication.directory}`, {
409
449
  if: releaseCondition([
410
- "needs.verify-context.result == 'success'",
450
+ "needs.build-release.result == 'success'",
451
+ "needs.build-release.outputs.pypi == 'true'",
411
452
  ...dependencyJobs.map((job) => `needs['${job}'].result == 'success'`),
412
453
  ]),
413
- needs: ["verify-context", ...dependencyJobs],
454
+ needs: ["build-release", ...dependencyJobs],
414
455
  environment: {
415
456
  name: publication.environment,
416
457
  url:
@@ -420,20 +461,14 @@ export class DBXToolsPythonWorkspace extends Component {
420
461
  runsOn: ["ubuntu-latest"],
421
462
  permissions: { idToken: JobPermission.WRITE },
422
463
  timeoutMinutes: 10,
423
- env: { BUN_VERSION },
424
464
  steps: [
425
- ...releaseSourceSteps(),
426
- ...project.renderWorkflowSetup({ mutable: false }),
427
- uvSetupStep(),
428
465
  {
429
- name: `Build ${publication.distribution} distributions`,
430
- env: { RELEASE_VERSION },
431
- run: [
432
- 'bun node_modules/@dbx-tools/projen/tasks/publish-python.ts "$RELEASE_VERSION"',
433
- `--root ${JSON.stringify(this.repository.root)}`,
434
- `--package ${JSON.stringify(publication.directory)}`,
435
- `--output dist/${publication.directory}`,
436
- ].join(" \\\n "),
466
+ name: `Download ${publication.distribution} distributions`,
467
+ uses: "actions/download-artifact@v4",
468
+ with: {
469
+ name: `release-python-${publication.directory}`,
470
+ path: `dist/${publication.directory}`,
471
+ },
437
472
  },
438
473
  {
439
474
  name: `Publish ${publication.distribution} to PyPI`,
@@ -1,34 +1,3 @@
1
1
  /** Verified release context shared by every job in the unified workflow. */
2
- import { github } from "projen";
3
- import type { JobStep } from "projen/lib/github/workflows-model";
4
-
5
- export const RELEASE_TAG = "${{ needs.verify-context.outputs.release_tag }}";
6
- export const RELEASE_SHA = "${{ needs.verify-context.outputs.expected_sha }}";
7
- export const RELEASE_VERSION = "${{ needs.verify-context.outputs.release_version }}";
8
-
9
- /** Check out and verify the immutable commit selected by the release tag. */
10
- export function releaseSourceSteps(): readonly JobStep[] {
11
- return [
12
- github.WorkflowSteps.checkout({
13
- name: "Checkout release commit",
14
- with: {
15
- ref: RELEASE_SHA,
16
- fetchDepth: 1,
17
- },
18
- }),
19
- {
20
- name: "Verify release source",
21
- shell: "bash",
22
- env: {
23
- RELEASE_TAG,
24
- EXPECTED_SHA: RELEASE_SHA,
25
- },
26
- run: [
27
- 'git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"',
28
- 'test "$(git cat-file -t "$RELEASE_TAG")" = "tag"',
29
- 'test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$EXPECTED_SHA"',
30
- 'test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"',
31
- ].join("\n"),
32
- },
33
- ];
34
- }
2
+ /** Version verified by the shared build that produced every publication artifact. */
3
+ export const RELEASE_VERSION = "${{ needs.build-release.outputs.release_version }}";