@dbx-tools/projen 0.6.229 → 0.7.0

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
@@ -173,30 +173,31 @@ a host Rust build. Regenerate bindings through the focused watcher or explicit
173
173
  task while changing a UniFFI API. Release preparation runs Cargo workspace tests
174
174
  without invoking UBRN.
175
175
 
176
- The workspace generates one `release.yml` workflow for every ecosystem. A push
177
- to the configured release branch starts it only when `VERSION` changed. The
178
- context job requires the version to differ from its parent and exceed the latest
179
- tag, creates the corresponding annotated `v*` tag, and exposes the verified tag,
180
- commit, and version to every source job. If the tag already exists, it must be
181
- annotated and reference the same commit. A manual run accepts that same tag and
182
- commit but requires dry-run mode, so it builds and validates without publishing
183
- packages or deploying documentation. One non-cancelling `release` concurrency
184
- group serializes publication.
176
+ The workspace generates one `release.yml` workflow for every ecosystem.
177
+ Independent mode runs Release Please on default-branch pushes. It owns the
178
+ combined release PR, per-component semantic increments, component-qualified
179
+ tags, and GitHub Releases. A Projen reconciliation pass reads the reviewed
180
+ manifest, regenerates every owned version surface, validates a clean second
181
+ synthesis, and commits component-qualified release notes to the release PR.
182
+ One non-cancelling `release` concurrency group serializes publication.
185
183
 
186
184
  Release generation writes `release.yml` without scanning for or removing other
187
185
  workflow files. Consumers explicitly delete exact workflow files they no longer
188
186
  want.
189
187
 
190
- Failed-job reruns consume successful Rust artifacts from the same workflow run.
191
- For a separate recovery run, choose `node`, `python`, or `docs` instead of
192
- `all`. Node and Python recovery can supply the prior `release.yml` run ID; the
193
- workflow verifies that run used the commit selected by the annotated release
194
- tag before downloading its artifacts. Dispatch the workflow with the release
195
- tag as its Git ref so recovery checks out the immutable release boundary. Manual recovery
196
- defaults to dry-run and publishes only when `dry_run` is cleared. npm retries skip an exact version only
197
- after the local archive integrity and repository identity match registry
198
- metadata. PyPI retries enable Twine's hash-aware existing-file behavior, which
199
- skips matching files and fails when an existing filename has different content.
188
+ The release-plan job compares the previous and reviewed release-unit graphs and
189
+ selects only changed units, required dependents, packages, native targets,
190
+ assets, and docs. A Node-only release allocates no Rust runner. Manual recovery
191
+ names one component, reviewed version, and publication stage; it reconstructs
192
+ that component's plan without changing any other version. npm retries compare
193
+ archive content and repository identity, PyPI uses hash-aware existing-file
194
+ behavior, Cargo checks existing versions, and GitHub assets upload to the
195
+ component release.
196
+
197
+ Set `releaseSyncBranch` when a repository retains a long-lived source branch.
198
+ After publication the workflow fast-forwards that branch only when it exists
199
+ and is an ancestor of released `main`; missing or diverged branches are left
200
+ untouched.
200
201
 
201
202
  The Rust matrix has one row per target. Each row installs native dependencies,
202
203
  builds the Cargo workspace once, then packages every discovered output from
@@ -415,6 +416,8 @@ file contract as the CLI.
415
416
  - `barrels` / `moduleExports` - public entrypoint generation.
416
417
  - `codegen` - `.d.ts` to zod schema generation.
417
418
  - `openapi` - tsoa/OpenAPI package generation.
419
+ - `releaseCatalog` / `releasePlan` / `releasePlease` - cross-language ownership,
420
+ affected planning, and Release Please graph propagation.
418
421
  - `bunApp` / `tsconfig` / `vscode` - generated support files/components.
419
422
  - `generated` / `clean` / `watch` / `scaffold` - read-only file stamping,
420
423
  cleanup, watchers, and synth orchestration.
@@ -433,51 +436,28 @@ Every repo-wide task lives on the root, and the root's `compile` / `test`
433
436
  delegate with `bun run --filter '*'` rather than emitting a step per member - so
434
437
  a new package is covered without a re-synth. Work from the root:
435
438
 
436
- | Task | What it does |
437
- | ----------------------- | ------------------------------------------------------- |
438
- | `bun run build` | synth + workspace compile and tests |
439
- | `bun run compile` | `tsc --build` in each member, in parallel |
440
- | `bun run test` | `eslint` once, then each member's tests |
441
- | `bun run sync` | re-synth (`--watch` to keep synthing) |
442
- | `bun run barrels` | regenerate the read-only `index.ts` barrels |
443
- | `bun run bump` | increment `VERSION` and synchronize generated versions |
444
- | `bun run version:check` | verify every version surface matches `VERSION` |
445
- | `bun run release` | run full local preflight and open a reviewed release PR |
446
-
447
- `release` commits pending work on the current branch, merges the latest remote
448
- release branch when needed, and pushes it. It prepares the dedicated
449
- `release/v<version>` branch inside `.worktrees/<tag>`, leaving the source
450
- checkout on its current branch throughout validation, local publication, and
451
- PR creation. Failed preparation can resume from that worktree; success removes
452
- it. `--message` sets the source commit message. The task never merges
453
- the PR unless `--approve` is passed; that option merges the release branch
454
- directly through GitHub's merge API so no PR checks are created. A repository
455
- that blocks direct merges falls back to an immediate admin-merged PR. The task
456
- then fast-forwards the still-active source branch to the release commit.
457
-
458
- After validation, release preparation optionally generates an immutable
459
- `docs/releases/v<version>.md`. It tries the installed Cursor agent first, then
460
- Codex, then Claude, all in non-interactive read-only modes against bounded Git
461
- context. An unavailable, unauthenticated, failed, or empty provider falls
462
- through to the next; if none works, release preparation continues without a
463
- summary. The same file is included in the release PR and prepended to GitHub's
464
- generated release notes, so repository and GitHub views share one source.
465
-
466
- npm
467
- uses `npm config get registry` and publishes to a local Verdaccio automatically.
468
- Publishable JavaScript members compile once
469
- from the root in parallel, then upload through a bounded pool without rerunning
470
- their `prepack` tasks. Python prefers uv's default index and only
471
- treats a loopback `.../+simple/` URL as writable devpi; a read-only cache such as
472
- proxpi (`.../index/`) is deliberately ignored. The task stamps every Python
473
- member and its sibling dependencies to the release version, builds the workspace
474
- with uv, then runs `devpi upload --from-dir` against the derived writable index.
475
- The npm and Python mirrors run concurrently because they touch disjoint package
476
- trees. Devpi client authentication remains in its normal `~/.devpi` state.
477
-
478
- Use `--local-registry false` or `--local-pypi false` to disable either local
479
- publish. An explicit `--local-pypi http://localhost:3141/user/index/` overrides
480
- auto-detection; `--python-root` defaults to `packages/py`.
439
+ | Task | What it does |
440
+ | --------------------------- | ----------------------------------------------------- |
441
+ | `bun run build` | synth + workspace compile and tests |
442
+ | `bun run compile` | `tsc --build` in each member, in parallel |
443
+ | `bun run test` | `eslint` once, then each member's tests |
444
+ | `bun run sync` | re-synth (`--watch` to keep synthing) |
445
+ | `bun run barrels` | regenerate the read-only `index.ts` barrels |
446
+ | `bun run release:bootstrap` | initialize writable Release Please state once |
447
+ | `bun run release:plan` | emit the affected component and publication plan |
448
+ | `bun run version:check` | verify every package against its owning release unit |
449
+ | `bun run release` | refresh Release Please PRs, tags, and GitHub Releases |
450
+
451
+ `release` runs the pinned Release Please library with the generated
452
+ cross-language graph plugin. Direct changes retain their conventional semantic
453
+ increment, while required dependents receive patches. The combined release PR
454
+ is reconciled by Projen before review. Merging it creates component tags and
455
+ GitHub Releases, then the affected plan selects publication jobs.
456
+
457
+ Each changed component receives
458
+ `docs/releases/<component>-v<version>.md`. Summary generation tries Cursor,
459
+ Codex, and Claude in order, logs provider events, and falls back to Git. The
460
+ committed file becomes that component's GitHub Release body.
481
461
 
482
462
  The GitHub PR workflow runs the explicit `pr:validate` task through Projen's
483
463
  public `BuildWorkflow` `buildTask` option. That task runs synth plus the
@@ -486,13 +466,10 @@ preparation has already run Rust tests, workspace type-checking, and local
486
466
  package preflight before opening the PR. JavaScript behavior tests remain an
487
467
  explicit developer task.
488
468
 
489
- The configured release branch publishes only when a reviewed PR changes
490
- `VERSION`. Its workflow creates the annotated `v*` public release boundary.
491
- Available workflow
492
- stages form the generated chain Rust -> Python -> Node -> docs: Rust builds and
493
- publishes native artifacts and Cargo crates, Python publishes standard
494
- distributions, Node publishes standard workspace packages, and docs deploys
495
- after publication. A stage with no corresponding outputs is omitted.
469
+ The generated workflow runs Release Please on the configured branch. A merged
470
+ release PR produces an affected plan, then selected Rust, Python, Node, GitHub
471
+ asset, and docs jobs run behind a publication barrier. A stage with no selected
472
+ outputs is skipped before runner allocation.
496
473
 
497
474
  Members intentionally keep only the tasks that something OTHER than a human
498
475
  invokes, so there is no second place to run the same thing:
@@ -519,17 +496,14 @@ from the root instead.
519
496
 
520
497
  ## Versioning
521
498
 
522
- The whole repo shares ONE version, stored in the root `VERSION` file (a plain
523
- `x.y.z` string; a fresh tree with no file defaults to `0.0.1`). Synth COPIES that
524
- value into every generated manifest - the root and `projen/` `package.json`, every
525
- JS member, every Python `pyproject.toml`, the generated openapi packages, and the
526
- example apps - so the packages, the engine, and the examples always match.
527
- `src/workspace-version.ts` owns reading and writing it. Synth only ever reads it;
528
- it never resets, upgrades, or downgrades a version on its own.
529
-
530
- `bun run bump` is the only command that changes the number: it fetches remote
531
- tags once, takes the highest `v*` tag as the base, increments by `--level`, writes
532
- `VERSION`, then synths so every manifest copies it. It has no git or publication
533
- side effects. `bun run release` invokes it while preparing the reviewed release
534
- PR. The remote is consulted only on `bump` and on one-time creation of a missing
535
- `VERSION` file, never on an ordinary synth.
499
+ `DBXToolsReleaseCatalog` is the language-neutral version seam. It generates
500
+ `.projen/release-units.json` from attached projects, explicit external members,
501
+ UniFFI families, and binary assets. Independent mode reads component versions
502
+ from `.release-please-manifest.json`; fixed mode reads `VERSION`.
503
+
504
+ Every unit has `.release-units/<component>/source.json`, generated from
505
+ release-affecting inputs. Release Please owns the adjacent `version.txt` and
506
+ `CHANGELOG.md`. Real package manifests remain Projen-owned and reproduce the
507
+ reviewed manifest exactly. `versioningMode: "fixed"` remains the default for
508
+ consumers during migration; `versioningMode: "independent"` enables the
509
+ Release Please and affected-workflow surface.
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.6.229";
6
+ export const PACKAGE_VERSION = "0.7.0";
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";
@@ -24,8 +24,11 @@ export * as projectPy from "./src/project-py.ts";
24
24
  export * as projectRs from "./src/project-rs.ts";
25
25
  export * as publish from "./src/publish.ts";
26
26
  export * as release from "./src/release.ts";
27
+ export * as releaseCatalog from "./src/release-catalog.ts";
27
28
  export * as releaseDispatch from "./src/release-dispatch.ts";
28
29
  export * as releaseGithub from "./src/release-github.ts";
30
+ export * as releasePlan from "./src/release-plan.ts";
31
+ export * as releasePlease from "./src/release-please.ts";
29
32
  export * as scaffold from "./src/scaffold.ts";
30
33
  export * as tags from "./src/tags.ts";
31
34
  export * as tsconfig from "./src/tsconfig.ts";
@@ -56,9 +59,14 @@ export type { CargoDependencyOptions, CargoDependency, RustCliOptions, RustPacka
56
59
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
57
60
  export { DBXToolsRelease } from "./src/release.ts";
58
61
  export type { ReleaseStage, ReleaseDocsOptions, ReleaseSummaryOptions, DBXToolsReleaseOptions } from "./src/release.ts";
62
+ export { DBXToolsReleaseCatalog } from "./src/release-catalog.ts";
63
+ export type { DBXToolsVersioningMode, ReleaseProjectLanguage, ReleaseArtifactKind, ReleaseEdgeKind, ReleasePropagation, ReleaseUnitRule, DBXToolsReleaseCatalogOptions, ReleaseDependencyInput, ReleaseProjectRegistration, ExternalReleaseProjectRegistration, ReleaseArtifactRegistration, ReleaseProjectNode, ReleaseArtifact, ReleaseDependencyEdge, ReleaseUnit, ReleaseUnitGraph } from "./src/release-catalog.ts";
59
64
  export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION, RELEASE_SUMMARY_DIRECTORY, RELEASE_SUMMARY_PROVIDER_NAMES, RELEASE_SUMMARY_FILE } from "./src/release-dispatch.ts";
60
65
  export type { ReleaseSummaryProviderName } from "./src/release-dispatch.ts";
61
66
  export type { GithubRepositoryIdentity } from "./src/release-github.ts";
67
+ export type { ReleaseReason, PlannedReleaseUnit, PlannedPackage, PlannedArtifact, PlannedRustTarget, ReleasePlan } from "./src/release-plan.ts";
68
+ export { ReleaseUnitWorkspacePlugin } from "./src/release-please.ts";
69
+ export type { ReleaseUnitWorkspacePluginOptions } from "./src/release-please.ts";
62
70
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
63
71
  export type { PackageTag } from "./src/tags.ts";
64
72
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
package/package.json CHANGED
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^24.6.0",
21
+ "@types/semver": "^7.7.1",
21
22
  "constructs": "^10.0.0",
22
23
  "projen": "^0.101.6",
23
24
  "tsoa": "^6.6.0",
@@ -25,9 +26,9 @@
25
26
  },
26
27
  "dependencies": {
27
28
  "@clack/prompts": "^1.7.0",
28
- "@dbx-tools/core": "0.6.229",
29
- "@dbx-tools/path": "0.6.229",
30
- "@dbx-tools/shared-core": "0.6.229",
29
+ "@dbx-tools/core": "^0.6.229",
30
+ "@dbx-tools/path": "^0.6.229",
31
+ "@dbx-tools/shared-core": "^0.6.229",
31
32
  "commander": "^15.0.0",
32
33
  "concurrently": "^10.0.3",
33
34
  "constructs": "^10.6.0",
@@ -35,7 +36,9 @@
35
36
  "openapi-typescript": "^7.13.0",
36
37
  "oxc-parser": "^0.90.0",
37
38
  "projen": "^0.101.16",
38
- "smol-toml": "^1.7.1",
39
+ "release-please": "17.11.2",
40
+ "semver": "^7.7.3",
41
+ "smol-toml": "1.8.0",
39
42
  "ts-to-zod": "^5.1.0",
40
43
  "yaml": "^2.9.0"
41
44
  },
@@ -47,7 +50,7 @@
47
50
  },
48
51
  "main": "index.ts",
49
52
  "license": "Apache-2.0",
50
- "version": "0.6.229",
53
+ "version": "0.7.0",
51
54
  "types": "index.ts",
52
55
  "type": "module",
53
56
  "exports": {
package/src/openapi.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  toPosix,
49
49
  recordedPackages,
50
50
  } from "./packages.ts";
51
+ import type { ReleaseUnitGraph } from "./release-catalog.ts";
51
52
  import { readWorkspaceVersion } from "./workspace-version.ts";
52
53
 
53
54
  const logger = log.logger("projen:openapi");
@@ -174,9 +175,16 @@ export async function generateOpenapi(): Promise<string[]> {
174
175
  skipLibCheck: true,
175
176
  };
176
177
 
177
- const specVersion = readWorkspaceVersion(repoRoot);
178
+ const releaseGraphPath = join(repoRoot, ".projen/release-units.json");
179
+ const releaseGraph = existsSync(releaseGraphPath)
180
+ ? (JSON.parse(readFileSync(releaseGraphPath, "utf8")) as ReleaseUnitGraph)
181
+ : undefined;
178
182
  const written: string[] = [];
179
183
  for (const p of pkgs) {
184
+ const releaseProject = releaseGraph?.projects.find((project) => project.path === p.path);
185
+ const specVersion =
186
+ releaseGraph?.units.find((unit) => unit.id === releaseProject?.unit)?.version ??
187
+ readWorkspaceVersion(repoRoot);
180
188
  // The generated package's folder is the source's leaf folder name (`api`), not
181
189
  // its npm name - `p.name` is the (possibly-overridden) manifest name.
182
190
  const leaf = p.relPath.split("/").pop() ?? p.relPath;
package/src/project-js.ts CHANGED
@@ -41,11 +41,18 @@ import {
41
41
  import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace.ts";
42
42
  import type { DBXToolsProject, DBXToolsProjectOptions as CommonProjectOptions } from "./project.ts";
43
43
  import { applyCompiledPublish } from "./publish.ts";
44
+ import {
45
+ DBXToolsReleaseCatalog,
46
+ type DBXToolsVersioningMode,
47
+ type ExternalReleaseProjectRegistration,
48
+ type ReleaseDependencyInput,
49
+ type ReleaseUnitRule,
50
+ } from "./release-catalog.ts";
44
51
  import { DBXToolsRelease, type ReleaseDocsOptions, type ReleaseSummaryOptions } from "./release.ts";
45
52
  import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
46
53
  import { DBXToolsRootTsconfig } from "./tsconfig.ts";
47
54
  import { DBXToolsVsCode } from "./vscode.ts";
48
- import { readWorkspaceVersion, syncWorkspaceManifestVersion } from "./workspace-version.ts";
55
+ import { syncWorkspaceManifestVersion } from "./workspace-version.ts";
49
56
 
50
57
  /**
51
58
  * The dbx-tools project surface, backed by projen's Node toolchain. A single
@@ -70,6 +77,8 @@ export interface DBXToolsJavaScriptProject extends DBXToolsProject, javascript.N
70
77
  vsCode?: DBXToolsVsCode;
71
78
  /** Repository outputs excluded from generated workflow dependency-cache hashing. */
72
79
  readonly workflowCacheIgnorePaths: readonly string[];
80
+ /** Cross-language release-unit ownership and version lookup. */
81
+ readonly releaseCatalog: DBXToolsReleaseCatalog;
73
82
  }
74
83
 
75
84
  /** Parsed npm package identifier: optional scope plus the unscoped package name. */
@@ -625,10 +634,20 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
625
634
  readonly releaseValidationTasks?: readonly string[];
626
635
  /** Optional AI-generated release summary. Defaults to enabled. */
627
636
  readonly releaseSummary?: boolean | ReleaseSummaryOptions;
637
+ /** Existing source branch safely fast-forwarded after an independent release. */
638
+ readonly releaseSyncBranch?: string | false;
628
639
  /** Set to `false` to omit normal npm workspace publication. */
629
640
  readonly nodeRelease?: boolean;
630
641
  /** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
631
642
  readonly releaseMode?: DBXToolsReleaseMode;
643
+ /** Package version ownership mode. Defaults to `fixed`. */
644
+ readonly versioningMode?: DBXToolsVersioningMode;
645
+ /** Explicit stable release-unit grouping rules. */
646
+ readonly releaseUnits?: readonly ReleaseUnitRule[];
647
+ /** Commit before the first independent Release Please history. */
648
+ readonly releaseBootstrapSha?: string;
649
+ /** Publishable workspace members synthesized outside the attached project tree. */
650
+ readonly externalReleaseProjects?: readonly ExternalReleaseProjectRegistration[];
632
651
  /** Prefix for generated release tags. Defaults to `v`. */
633
652
  readonly releaseTagPrefix?: string;
634
653
  /** Semantic PR title validation policy. Omitted or `false` disables title validation. */
@@ -682,6 +701,7 @@ export class DBXToolsNodeProject
682
701
  readonly extraWorkspaceMembers: readonly string[];
683
702
  readonly releaseBranch: string;
684
703
  readonly workflowCacheIgnorePaths: readonly string[];
704
+ readonly releaseCatalog: DBXToolsReleaseCatalog;
685
705
  private readonly rootInstallOnly: boolean;
686
706
 
687
707
  constructor(options: DBXToolsJavaScriptProjectOptions = {}) {
@@ -708,9 +728,14 @@ export class DBXToolsNodeProject
708
728
  // component, but the file is still required by the Databricks Apps platform
709
729
  // (its build phase installs with pnpm and reads catalog + `allowBuilds`).
710
730
  pnpmWorkspace.attachWorkspaceFile(this);
711
- // Copy the single workspace version onto the root manifest. The `VERSION` file
712
- // at the workspace root is the source of truth; synth only reads it.
713
- this.package.addField("version", readWorkspaceVersion(this.outdir));
731
+ this.releaseCatalog = new DBXToolsReleaseCatalog(this, {
732
+ mode: options.versioningMode,
733
+ units: options.releaseUnits,
734
+ externalProjects: options.externalReleaseProjects,
735
+ bootstrapSha: options.releaseBootstrapSha,
736
+ });
737
+ registerJavaScriptReleaseProject(this);
738
+ this.package.addField("version", () => this.releaseCatalog.versionFor(this));
714
739
  this.scope = scope;
715
740
  this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
716
741
  this.releaseBranch = options.defaultReleaseBranch ?? "main";
@@ -795,6 +820,7 @@ export class DBXToolsTypeScriptProject
795
820
  readonly extraWorkspaceMembers: readonly string[];
796
821
  readonly releaseBranch: string;
797
822
  readonly workflowCacheIgnorePaths: readonly string[];
823
+ readonly releaseCatalog: DBXToolsReleaseCatalog;
798
824
  private readonly rootInstallOnly: boolean;
799
825
 
800
826
  constructor(options: DBXToolsTypeScriptProjectOptions) {
@@ -837,6 +863,16 @@ export class DBXToolsTypeScriptProject
837
863
  this.releaseBranch = options.defaultReleaseBranch ?? "main";
838
864
  this.workflowCacheIgnorePaths = options.workflowCacheIgnorePaths ?? [];
839
865
  this.rootInstallOnly = options.rootInstallOnly !== false;
866
+ this.releaseCatalog =
867
+ parent &&
868
+ (parent instanceof DBXToolsNodeProject || parent instanceof DBXToolsTypeScriptProject)
869
+ ? parent.releaseCatalog
870
+ : new DBXToolsReleaseCatalog(this, {
871
+ mode: options.versioningMode,
872
+ units: options.releaseUnits,
873
+ externalProjects: options.externalReleaseProjects,
874
+ bootstrapSha: options.releaseBootstrapSha,
875
+ });
840
876
  // Pairs with `jsx` in SHARED_COMPILER_OPTIONS: projen's default `include` is
841
877
  // `src/**/*.ts` only, which silently omits a `.tsx` file from the program
842
878
  // instead of failing, so authoring a React component would otherwise need
@@ -852,10 +888,8 @@ export class DBXToolsTypeScriptProject
852
888
  ".": "./index.ts",
853
889
  "./package.json": "./package.json",
854
890
  });
855
- // Every package carries the single workspace version, copied from the root
856
- // `VERSION` file (the source of truth). `this.root` is the workspace root for a
857
- // discovered member and this project itself for a standalone compiling root.
858
- this.package.addField("version", readWorkspaceVersion(this.root.outdir));
891
+ this.package.addField("version", () => this.releaseCatalog.versionFor(this));
892
+ registerJavaScriptReleaseProject(this);
859
893
  addPackageFiles(this, "index.ts", "src");
860
894
  // `bun test` intercepts `node:test` (the suites keep using node:test) and
861
895
  // runs it with bun's own fast runner. The native no-tests option is portable
@@ -878,6 +912,27 @@ export class DBXToolsTypeScriptProject
878
912
  }
879
913
  }
880
914
 
915
+ function registerJavaScriptReleaseProject(project: DBXToolsJavaScriptProject): void {
916
+ const projectPath = toPosix(relative(project.root.outdir, project.outdir));
917
+ project.releaseCatalog.registerProject(project, {
918
+ language: "javascript",
919
+ identity: () => String(project.package.manifest.name ?? project.name),
920
+ publish: () => project.package.manifest.private !== true,
921
+ sourcePaths: ["src", "bin", "tasks"].map((path) =>
922
+ projectPath ? `${projectPath}/${path}` : path,
923
+ ),
924
+ dependencies: () =>
925
+ project.deps.all.map<ReleaseDependencyInput>((dependency) => ({
926
+ target: dependency.name,
927
+ kind:
928
+ dependency.type === "devenv" || dependency.type === "override"
929
+ ? "development"
930
+ : dependency.type,
931
+ ...(dependency.version ? { requirement: dependency.version } : {}),
932
+ })),
933
+ });
934
+ }
935
+
881
936
  function configureBuildWorkflow(
882
937
  project: DBXToolsNodeProject | DBXToolsTypeScriptProject,
883
938
  options: DBXToolsJavaScriptProjectOptions,
@@ -951,9 +1006,11 @@ function resolveRootWorkspace(
951
1006
  extraWorkspaceMembers: readonly string[],
952
1007
  ): void {
953
1008
  if (project.parent) return;
954
- const version = readWorkspaceVersion(project.outdir);
955
1009
  for (const member of extraWorkspaceMembers) {
956
- syncWorkspaceManifestVersion(join(project.outdir, member, "package.json"), version);
1010
+ syncWorkspaceManifestVersion(
1011
+ join(project.outdir, member, "package.json"),
1012
+ project.releaseCatalog.versionForPath(member),
1013
+ );
957
1014
  }
958
1015
  project.pnpmWorkspace?.resolveMembers(project, extraWorkspaceMembers);
959
1016
  }
@@ -1546,6 +1603,7 @@ function initProject(
1546
1603
  pythonRoot: options.releasePythonRoot,
1547
1604
  validationTasks: options.releaseValidationTasks,
1548
1605
  summary: options.releaseSummary,
1606
+ syncBranch: options.releaseSyncBranch,
1549
1607
  });
1550
1608
  }
1551
1609
  }
package/src/project-py.ts CHANGED
@@ -9,6 +9,8 @@ import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
9
9
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
10
10
  import { RELEASE_VERSION, releaseSourceSteps } from "./release-dispatch.ts";
11
11
  import {
12
+ independentReleaseSetupSteps,
13
+ registerIndependentPublicationJob,
12
14
  releaseArtifactSteps,
13
15
  releasePublishCondition,
14
16
  releaseStageCondition,
@@ -128,6 +130,14 @@ export function pythonModuleName(scope: string, directory: string): string {
128
130
  return [scope, ...directory.split("/")].map((part) => part.replaceAll("-", "_")).join(".");
129
131
  }
130
132
 
133
+ /** Published requirement for one internal Python dependency. */
134
+ export function pythonReleaseRequirement(version: string, exact = false): string {
135
+ if (exact) return `==${version}`;
136
+ const [major, minor] = version.split(".").map(Number);
137
+ const upper = major === 0 ? `0.${minor + 1}.0` : `${major + 1}.0.0`;
138
+ return `>=${version},<${upper}`;
139
+ }
140
+
131
141
  function projectVscode(project: Project): vscode.VsCode | undefined {
132
142
  return (project as Project & { readonly vscode?: vscode.VsCode }).vscode;
133
143
  }
@@ -264,9 +274,10 @@ export class DBXToolsPythonWorkspace extends Component {
264
274
  }));
265
275
  const resolvedOptions = { ...options, packages };
266
276
  this.requiresPython = options.requiresPython ?? ">=3.10";
267
- // The single workspace version, copied from the root `VERSION` file so Python
268
- // members carry the same number as their JS siblings.
269
- this.version = readWorkspaceVersion(project.outdir);
277
+ this.version =
278
+ isDBXToolsJavaScriptProject()(project) && project.releaseCatalog.mode === "independent"
279
+ ? "0.0.0"
280
+ : readWorkspaceVersion(project.outdir);
270
281
  this.file = this.emitWorkspace(project, resolvedOptions, scope);
271
282
  this.packages = packages.map(
272
283
  (pkg) =>
@@ -275,9 +286,45 @@ export class DBXToolsPythonWorkspace extends Component {
275
286
  package: pkg,
276
287
  repository: this.repository,
277
288
  requiresPython: this.requiresPython,
278
- version: this.version,
289
+ version: isDBXToolsJavaScriptProject()(project)
290
+ ? project.releaseCatalog.versionForRegistration(
291
+ "python",
292
+ pkg.name,
293
+ pythonPackagePath(this.repository, pkg.directory),
294
+ )
295
+ : this.version,
279
296
  }),
280
297
  );
298
+ if (isDBXToolsJavaScriptProject()(project)) {
299
+ for (const pkg of this.packages) {
300
+ project.releaseCatalog.registerProject(pkg, {
301
+ language: "python",
302
+ identity: pkg.packageOptions.name,
303
+ sourcePaths: [`${pythonPackagePath(this.repository, pkg.packageOptions.directory)}/src`],
304
+ dependencies: (pkg.packageOptions.internalDependencies ?? []).map((directory) => {
305
+ const dependency = packagesByDirectory.get(directory);
306
+ if (!dependency) {
307
+ throw new Error(
308
+ `Python package ${pkg.packageOptions.directory} references unknown internal package ${directory}`,
309
+ );
310
+ }
311
+ const version = project.releaseCatalog.versionForRegistration(
312
+ "python",
313
+ dependency.name,
314
+ pythonPackagePath(this.repository, dependency.directory),
315
+ );
316
+ return {
317
+ target: dependency.name,
318
+ kind: "runtime" as const,
319
+ requirement: pythonReleaseRequirement(version, dependency.uniffi === true),
320
+ propagation: "outside-range" as const,
321
+ publishOrder: true,
322
+ internal: true,
323
+ };
324
+ }),
325
+ });
326
+ }
327
+ }
281
328
  for (const pkg of this.packages) {
282
329
  const pyproject = `/${pythonPackagePath(this.repository, pkg.packageOptions.directory)}/pyproject.toml`;
283
330
  project.gitignore.include(pyproject);
@@ -415,6 +462,88 @@ export class DBXToolsPythonWorkspace extends Component {
415
462
  if (!workflow) {
416
463
  throw new Error("Python release requires the root dbx-tools release mode");
417
464
  }
465
+ if (project.releaseCatalog.mode === "independent") {
466
+ workflow.addJob("build-python", {
467
+ if: usesRustArtifacts
468
+ ? "${{ always() && needs.release-plan.outputs.python == 'true' && needs.rust-build.result != 'failure' && needs.rust-build.result != 'cancelled' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}"
469
+ : "${{ needs.release-plan.outputs.python == 'true' && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python') }}",
470
+ needs: ["release-plan", ...(usesRustArtifacts ? ["rust-build"] : [])],
471
+ runsOn: ["ubuntu-latest"],
472
+ permissions: { actions: JobPermission.READ, contents: JobPermission.READ },
473
+ timeoutMinutes: 20,
474
+ env: { BUN_VERSION },
475
+ steps: [
476
+ ...independentReleaseSetupSteps(project),
477
+ { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
478
+ ...uniffiPublications.map((publication) => ({
479
+ name: `Download ${publication.distribution} native wheels`,
480
+ uses: "actions/download-artifact@v8",
481
+ with: {
482
+ pattern: `${publication.distribution}--*--python-wheel`,
483
+ path: `dist/${publication.directory}`,
484
+ "merge-multiple": true,
485
+ },
486
+ })),
487
+ {
488
+ name: "Build affected distributions",
489
+ shell: "bash",
490
+ run: publications
491
+ .map(
492
+ (publication) =>
493
+ `if jq -e --arg package ${quote(publication.distribution)} '.pythonPackages[] | select(.identity == $package)' dist/release-plan.json >/dev/null; then uv build --package ${quote(publication.distribution)} --out-dir dist/${publication.directory}; fi`,
494
+ )
495
+ .join("\n"),
496
+ },
497
+ {
498
+ name: "Validate affected distributions",
499
+ run: "find dist -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) -print0 | xargs -0 uvx twine check",
500
+ },
501
+ {
502
+ name: "Upload distributions",
503
+ uses: "actions/upload-artifact@v7",
504
+ with: { name: "python-distributions", path: "dist", "retention-days": 7 },
505
+ },
506
+ ],
507
+ });
508
+ for (const publication of allPublications) {
509
+ const dependencyJobs = (publication.dependencies ?? []).map(
510
+ (dependency) => `publish-pypi-${dependency}`,
511
+ );
512
+ const dependencyCondition = dependencyJobs
513
+ .map((job) => `needs.${job}.result != 'failure' && needs.${job}.result != 'cancelled'`)
514
+ .join(" && ");
515
+ workflow.addJob(`publish-pypi-${publication.directory}`, {
516
+ if: `\${{ always() && needs.build-python.result == 'success' && contains(needs.release-plan.outputs.python_packages, '"identity":"${publication.distribution}"') && (github.event_name == 'push' || inputs.stage == 'all' || inputs.stage == 'python')${dependencyCondition ? ` && ${dependencyCondition}` : ""} }}`,
517
+ needs: ["release-plan", "build-python", ...dependencyJobs],
518
+ environment: {
519
+ name: publication.environment,
520
+ url:
521
+ options.environmentUrl ??
522
+ `https://pypi.org/project/${publication.distribution.replaceAll("_", "-")}/`,
523
+ },
524
+ runsOn: ["ubuntu-latest"],
525
+ permissions: { idToken: JobPermission.WRITE },
526
+ timeoutMinutes: 10,
527
+ steps: [
528
+ {
529
+ name: "Download distributions",
530
+ uses: "actions/download-artifact@v8",
531
+ with: { name: "python-distributions", path: "dist" },
532
+ },
533
+ {
534
+ name: `Publish ${publication.distribution} to PyPI`,
535
+ uses: "pypa/gh-action-pypi-publish@release/v1",
536
+ with: {
537
+ "packages-dir": `dist/${publication.directory}`,
538
+ "skip-existing": true,
539
+ },
540
+ },
541
+ ],
542
+ });
543
+ registerIndependentPublicationJob(workflow, `publish-pypi-${publication.directory}`);
544
+ }
545
+ return;
546
+ }
418
547
  workflow.addJob("build-python", {
419
548
  if: usesRustArtifacts
420
549
  ? "${{ 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') }}"