@dbx-tools/projen 0.6.177 → 0.6.179

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
@@ -128,22 +128,36 @@ removing a crate or `setup_scaffolding!()` marker triggers a full synth. Repos
128
128
  without Rust crates start no Rust watcher. When Rust projects are detected,
129
129
  Cargo is required and the focused task fails immediately if it is unavailable.
130
130
 
131
- The workspace also generates a `rust-release` workflow from discovered crates,
132
- UniFFI bindings, and release-enabled binaries. The root `release-dispatch`
133
- workflow resolves the annotated release tag and sends the tag plus commit SHA to
134
- `rust-release` when Rust is present. The Rust workflow runs in the configured release
135
- branch cache scope, checks out the supplied SHA, fetches the tag, and requires
136
- both the tag target and `HEAD` to equal that SHA before building. Its matrix has
137
- one row per target. Each row installs native dependencies and restores
138
- Cargo/sccache once,
139
- builds the Rust workspace once, then packages every discovered output from that
140
- shared build. Rust rows package native npm archives and Python wheels; they do
141
- not install Bun or UBRN and do not build Node facades.
142
- A new dispatch cancels queued and in-progress release and docs workflows before
143
- starting, and each workflow's concurrency group also cancels its previous run.
144
- A binary-only workspace therefore installs no language package tool. `rustVersion` remains the
145
- MSRV recorded in package manifests, while `releaseRustVersion` independently
146
- defaults release compilation to `stable`.
131
+ The workspace generates one `release.yml` workflow for every ecosystem. An
132
+ annotated `v*` tag starts it directly. The context job resolves the tag to a
133
+ commit, requires the tag object to be annotated, and exposes the verified tag,
134
+ commit, and version to every source job. A manual run accepts that same tag and
135
+ commit but requires dry-run mode, so it builds and validates without publishing
136
+ packages or deploying documentation. One `release` concurrency group cancels a
137
+ superseded run.
138
+
139
+ Release generation writes `release.yml` without scanning for or removing other
140
+ workflow files. Consumers explicitly delete exact workflow files they no longer
141
+ want.
142
+
143
+ Failed-job reruns consume successful Rust artifacts from the same workflow run.
144
+ For a separate recovery run, choose `node`, `python`, or `docs` instead of
145
+ `all`. Node and Python recovery can supply the prior `release.yml` run ID; the
146
+ workflow verifies that run used the commit selected by the annotated release
147
+ tag before downloading its artifacts. Dispatch the workflow with the release
148
+ tag as its Git ref so environment tag policies still apply. Manual recovery
149
+ defaults to dry-run and publishes only when `dry_run` is cleared. npm retries skip an exact version only
150
+ after the local archive integrity and repository identity match registry
151
+ metadata. PyPI retries enable Twine's hash-aware existing-file behavior, which
152
+ skips matching files and fails when an existing filename has different content.
153
+
154
+ The Rust matrix has one row per target. Each row installs native dependencies,
155
+ restores Cargo and sccache once, builds the Cargo workspace once, then packages
156
+ every discovered output from that shared build. Rust rows prepare native npm
157
+ archives and Python wheels; they do not install Bun or UBRN and do not build
158
+ Node facades. A binary-only row therefore installs no language package tool.
159
+ `rustVersion` remains the MSRV recorded in package manifests, while
160
+ `releaseRustVersion` independently defaults release compilation to `stable`.
147
161
  Stable Windows rows verify and use the hosted runner's installed Rust toolchain
148
162
  and select `rust-lld` for the workspace build. Cargo registry caches
149
163
  and the `SCCACHE_GHA_VERSION` namespace stay stable per target/toolchain across
@@ -152,10 +166,9 @@ are written to each build log. Python generation executes the already-built
152
166
  `target/<triple>/release/<crate>-uniffi-bindgen` directly. Artifact packaging therefore
153
167
  does no Rust compilation after the main workspace build.
154
168
 
155
- Artifacts identify their crate, target, and type. `rust-release` publishes
156
- non-private Cargo crates with `cargo publish --no-verify`, uploads requested
157
- binary assets to the GitHub release, then dispatches the verified Rust run id
158
- with the tag and SHA. It never publishes npm or PyPI packages.
169
+ Artifacts identify their crate, target, and type. Rust jobs publish non-private
170
+ Cargo crates with `cargo publish --no-verify` and upload requested binary assets
171
+ to the GitHub release. Rust jobs never publish npm or PyPI packages.
159
172
 
160
173
  Set `LOCAL_CARGO_REGISTRY` to a named Cargo registry such as a loopback
161
174
  [Kellnr](https://kellnr.io/) instance and provide `LOCAL_CARGO_TOKEN`. Public
@@ -170,21 +183,20 @@ environment parsing in a consumer.
170
183
 
171
184
  Public UniFFI facades are marked with `dbxToolsConfig.uniffi = true` in Node
172
185
  manifests and `[tool.dbx_tools.config] uniffi = true` in Python manifests. The
173
- Node release downloads and publishes native archives first, publishes normal
174
- workspace packages, then builds and publishes facades from the committed
175
- generated TypeScript in dependency order. The Python release downloads every
176
- platform wheel and publishes them from one trusted-publisher job per
177
- distribution. The docs generators include them because they are publicly installable.
178
- When a conventional Node binding path already belongs to a root subproject,
179
- Rust mapping reuses that project and adds binding dependencies and metadata to
180
- its existing manifest.
181
- After Rust's artifacts finish, it dispatches the downstream `release` event
182
- with its run id and attempt. Python, Node, and docs consume that event
183
- independently with the same verified tag and SHA. Without Rust,
184
- `release-dispatch` sends that event immediately. Configure release GitHub
185
- environments to permit the generated release branch; the trusted-publisher
186
- instructions list that branch with the workflow and environment identity PyPI
187
- verifies.
186
+ Node jobs consume same-run artifacts, publish native archives first with npm
187
+ provenance, compile and publish normal workspace packages including the Projen
188
+ engine, then build facades from committed generated TypeScript without UBRN and
189
+ publish them in binding dependency order. Local Verdaccio publication does not
190
+ enable provenance. Set the `UNIFFI_FACADE_SMOKE` repository variable to `true`
191
+ to run the optional nonblocking registry install and import check after facade
192
+ publication. Python combines same-run platform wheels with standard wheel and
193
+ source builds, then publishes each distribution through its own PyPI
194
+ trusted-publisher environment. Binding publishers wait for their dependencies.
195
+ Trusted-publisher instructions name `release.yml` and the release tag policy.
196
+ The docs jobs generate README and TypeScript API content and deploy GitHub Pages
197
+ from the same workflow. When a conventional Node binding path already belongs
198
+ to a root subproject, Rust mapping reuses that project and adds binding
199
+ dependencies and metadata to its existing manifest.
188
200
 
189
201
  ## Customize Packages With Mixins
190
202
 
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.176";
6
+ export const PACKAGE_VERSION = "0.6.178";
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";
@@ -54,8 +54,8 @@ export { RustReleaseOs, RustReleaseCpu, UNIFFI_RELEASE_TARGETS, DBXToolsRustProj
54
54
  export type { CargoDependencyOptions, CargoDependency, RustPackageOptions, DBXToolsRustWorkspaceOptions, RustReleasePlatform, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping } from "./src/project-rs.ts";
55
55
  export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
56
56
  export { DBXToolsRelease } from "./src/release.ts";
57
- export type { StandaloneRelease, DBXToolsReleaseOptions } from "./src/release.ts";
58
- export { DOWNSTREAM_RELEASE_EVENT, RUST_RELEASE_EVENT, RELEASE_TAG, RELEASE_SHA } from "./src/release-dispatch.ts";
57
+ export type { ReleaseStage, ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
58
+ export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION } from "./src/release-dispatch.ts";
59
59
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
60
60
  export type { PackageTag } from "./src/tags.ts";
61
61
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
package/package.json CHANGED
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@clack/prompts": "^1.7.0",
28
- "@dbx-tools/core": "0.6.177",
29
- "@dbx-tools/path": "0.6.177",
30
- "@dbx-tools/shared-core": "0.6.177",
28
+ "@dbx-tools/core": "0.6.179",
29
+ "@dbx-tools/path": "0.6.179",
30
+ "@dbx-tools/shared-core": "0.6.179",
31
31
  "commander": "^15.0.0",
32
32
  "concurrently": "^10.0.3",
33
33
  "constructs": "^10.6.0",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "main": "index.ts",
49
49
  "license": "Apache-2.0",
50
- "version": "0.6.177",
50
+ "version": "0.6.179",
51
51
  "types": "index.ts",
52
52
  "type": "module",
53
53
  "exports": {
package/src/project-js.ts CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace.ts";
41
41
  import type { DBXToolsProject, DBXToolsProjectOptions as CommonProjectOptions } from "./project.ts";
42
42
  import { applyCompiledPublish } from "./publish.ts";
43
- import { DBXToolsRelease, type StandaloneRelease } from "./release.ts";
43
+ import { DBXToolsRelease, type ReleaseDocsOptions } from "./release.ts";
44
44
  import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
45
45
  import { DBXToolsRootTsconfig } from "./tsconfig.ts";
46
46
  import { DBXToolsVsCode } from "./vscode.ts";
@@ -549,18 +549,10 @@ export interface DBXToolsJavaScriptProjectOptions
549
549
  * (alongside `.projenrc.ts`). Repo-relative, e.g. `".example.projenrc.ts"`.
550
550
  */
551
551
  readonly syncResynthPaths?: readonly string[];
552
- /**
553
- * Standalone in-repo projects (NOT workspace members) that each get their own
554
- * tag-driven release workflow authored alongside the root's `release`
555
- * workflow - see {@link StandaloneRelease}. Use for a project that lives in a
556
- * repo subdirectory but releases on its own tag prefix (e.g. the
557
- * `@dbx-tools/projen` engine in `projen/`, tagged `projen-v*`).
558
- */
559
- readonly standaloneReleases?: readonly StandaloneRelease[];
560
- /** Workflow that must finish successfully before the main Node release runs. */
561
- readonly releaseUpstreamWorkflow?: string;
562
- /** Main Node release workflow name. Defaults to `node-release`. */
563
- readonly nodeReleaseWorkflowName?: string | false;
552
+ /** GitHub Pages documentation included in the unified release workflow. */
553
+ readonly releaseDocs?: ReleaseDocsOptions;
554
+ /** Set to `false` to omit normal npm workspace publication. */
555
+ readonly nodeRelease?: boolean;
564
556
  /**
565
557
  * Extra workspace member paths (repo-relative, POSIX) to list in the workspace
566
558
  * config ALONGSIDE the discovered `packageRoots` members - for a package that
@@ -1264,9 +1256,8 @@ function initProject(
1264
1256
  // authors. Independent of projen's own `release` component.
1265
1257
  new DBXToolsRelease(project as DBXToolsNodeProject, {
1266
1258
  tagPrefix: options.releaseTagPrefix,
1267
- standaloneReleases: options.standaloneReleases,
1268
- upstreamWorkflow: options.releaseUpstreamWorkflow,
1269
- workflowName: options.nodeReleaseWorkflowName ?? options.releaseWorkflowName,
1259
+ nodeRelease: options.nodeRelease,
1260
+ docs: options.releaseDocs,
1270
1261
  });
1271
1262
  }
1272
1263
 
package/src/project-py.ts CHANGED
@@ -3,13 +3,20 @@ import { project as coreProject } from "@dbx-tools/core";
3
3
  import { string } from "@dbx-tools/shared-core";
4
4
  import { Component, TextFile, type Project, javascript, python, vscode } from "projen";
5
5
  import type { IResolver } from "projen/lib/file";
6
- import { GithubWorkflow } from "projen/lib/github";
7
6
  import { JobPermission } from "projen/lib/github/workflows-model";
8
7
  import { parse, stringify } from "smol-toml";
9
- import { projectReleaseBranch, projectRepositoryUrl } from "./project-js.ts";
8
+ import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
9
+ import { projectRepositoryUrl } from "./project-js.ts";
10
10
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
11
11
  import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
12
- import { DOWNSTREAM_RELEASE_EVENT, RELEASE_TAG, releaseSourceSteps } from "./release-dispatch.ts";
12
+ import { RELEASE_VERSION, releaseSourceSteps } from "./release-dispatch.ts";
13
+ import {
14
+ releaseArtifactSteps,
15
+ releasePublishCondition,
16
+ releaseStageCondition,
17
+ releaseTagPattern,
18
+ releaseWorkflow,
19
+ } from "./release.ts";
13
20
  import { readWorkspaceVersion } from "./workspace-version.ts";
14
21
 
15
22
  /** Git location used by direct `#subdirectory=` package dependencies. */
@@ -44,7 +51,6 @@ interface ResolvedPythonPackageOptions extends PythonPackageOptions {
44
51
 
45
52
  /** GitHub Actions publisher for a Python package released by another workflow. */
46
53
  export interface PythonTrustedPublisherOptions {
47
- readonly workflowName: string;
48
54
  readonly environment: string;
49
55
  readonly artifacts?: string;
50
56
  }
@@ -61,9 +67,6 @@ export interface DBXToolsPythonProjectOptions extends DBXToolsProjectOptions {
61
67
 
62
68
  /** Python release workflow configuration. */
63
69
  export interface PythonReleaseOptions {
64
- readonly workflowName?: string;
65
- /** Run after this workflow completes successfully instead of directly on tags. */
66
- readonly upstreamWorkflow?: string;
67
70
  /** GitHub environment by Python distribution name. Defaults to `pypi-<name>`. */
68
71
  readonly environments?: Readonly<Record<string, string>>;
69
72
  readonly environmentUrl?: string;
@@ -73,7 +76,6 @@ interface PythonPublication {
73
76
  readonly directory: string;
74
77
  readonly distribution: string;
75
78
  readonly environment: string;
76
- readonly workflowName: string;
77
79
  readonly artifacts?: string;
78
80
  readonly dependencies?: readonly string[];
79
81
  }
@@ -321,10 +323,6 @@ export class DBXToolsPythonWorkspace extends Component {
321
323
  }
322
324
 
323
325
  if (options.release && this.packages.length > 0) {
324
- if (isDBXToolsJavaScriptProject()(project)) {
325
- project.dbxToolsConfig.pythonReleaseWorkflow =
326
- releaseOptions.workflowName ?? "python-release";
327
- }
328
326
  this.addReleaseWorkflow(project, releaseOptions);
329
327
  }
330
328
  }
@@ -427,193 +425,40 @@ export class DBXToolsPythonWorkspace extends Component {
427
425
  }
428
426
 
429
427
  private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
430
- if (!project.github) return;
428
+ if (!project.github || !isDBXToolsJavaScriptProject()(project)) return;
431
429
  const publications = this.publications(options);
432
430
  const uniffiPublications = this.uniffiPublications(options);
433
431
  const allPublications = [...publications, ...uniffiPublications];
434
432
  if (allPublications.length === 0) return;
435
433
  const usesRustArtifacts = uniffiPublications.length > 0;
436
- const workflowName = options.workflowName ?? "python-release";
437
- const workflow = new GithubWorkflow(project.github, workflowName, {
438
- limitConcurrency: true,
439
- concurrencyOptions: { group: workflowName, cancelInProgress: true },
440
- });
441
- workflow.file?.addOverride("permissions", {
442
- contents: "read",
443
- ...(options.upstreamWorkflow || usesRustArtifacts ? { actions: "read" } : {}),
444
- });
445
- const upstreamWorkflow = options.upstreamWorkflow;
446
- const branchDispatch = upstreamWorkflow === undefined && isDBXToolsJavaScriptProject()(project);
447
- if (upstreamWorkflow) {
448
- workflow.file?.addOverride("on.workflow_run", {
449
- workflows: [upstreamWorkflow],
450
- types: ["completed"],
451
- });
452
- workflow.on({ workflowDispatch: {} });
453
- } else if (branchDispatch) {
454
- workflow.file?.addOverride("on.repository_dispatch", {
455
- types: [DOWNSTREAM_RELEASE_EVENT],
456
- });
457
- workflow.on({ workflowDispatch: {} });
458
- } else {
459
- workflow.on({ push: { tags: ["v*"] }, workflowDispatch: {} });
460
- }
461
- workflow.file?.addOverride("on.workflow_dispatch", {
462
- inputs: {
463
- ...(branchDispatch
464
- ? {
465
- release_tag: {
466
- description: "Release tag to package during a dry run",
467
- type: "string",
468
- required: true,
469
- },
470
- expected_sha: {
471
- description: "Commit the release tag must reference",
472
- type: "string",
473
- required: true,
474
- },
475
- }
476
- : {
477
- version: {
478
- description: "Python package version to build",
479
- type: "string",
480
- required: true,
481
- },
482
- }),
483
- },
484
- });
485
- workflow.addJob("build", {
486
- ...(upstreamWorkflow
487
- ? {
488
- if: "${{ github.event_name != 'workflow_run' || (github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'repository_dispatch')) }}",
489
- }
490
- : {}),
434
+ const workflow = releaseWorkflow(project);
435
+ workflow.addJob("build-python", {
436
+ if: usesRustArtifacts
437
+ ? "${{ 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') }}"
438
+ : releaseStageCondition("python"),
439
+ needs: ["verify-context", ...(usesRustArtifacts ? ["rust-build"] : [])],
491
440
  runsOn: ["ubuntu-latest"],
492
- permissions: {
493
- contents: JobPermission.READ,
494
- ...(upstreamWorkflow || usesRustArtifacts ? { actions: JobPermission.READ } : {}),
495
- },
441
+ permissions: { actions: JobPermission.READ, contents: JobPermission.READ },
496
442
  timeoutMinutes: 20,
443
+ env: { BUN_VERSION },
497
444
  steps: [
498
- ...(upstreamWorkflow
499
- ? [
500
- {
501
- name: "Download release metadata",
502
- if: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.event == 'repository_dispatch' }}",
503
- uses: "actions/download-artifact@v8",
504
- with: {
505
- name: "release-metadata",
506
- path: ".release",
507
- "run-id": "${{ github.event.workflow_run.id }}",
508
- "github-token": "${{ github.token }}",
509
- },
510
- },
511
- {
512
- name: "Read release metadata",
513
- id: "release_metadata",
514
- if: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.event == 'repository_dispatch' }}",
515
- shell: "bash",
516
- run: [
517
- 'RELEASE_TAG="$(cat .release/tag)"',
518
- 'EXPECTED_SHA="$(cat .release/sha)"',
519
- 'test -n "$RELEASE_TAG"',
520
- 'test -n "$EXPECTED_SHA"',
521
- 'echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"',
522
- 'echo "expected_sha=$EXPECTED_SHA" >> "$GITHUB_OUTPUT"',
523
- ].join("\n"),
524
- },
525
- ]
526
- : []),
527
- ...(branchDispatch
528
- ? releaseSourceSteps()
529
- : [
530
- {
531
- name: "Checkout",
532
- uses: "actions/checkout@v6",
533
- with: {
534
- "fetch-depth": 0,
535
- ...(upstreamWorkflow
536
- ? {
537
- ref: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.event == 'repository_dispatch' && steps.release_metadata.outputs.expected_sha || github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}",
538
- }
539
- : {}),
540
- },
541
- },
542
- ]),
543
- ...(upstreamWorkflow
544
- ? [
545
- {
546
- name: "Verify release source",
547
- if: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.event == 'repository_dispatch' }}",
548
- shell: "bash",
549
- env: {
550
- RELEASE_TAG: "${{ steps.release_metadata.outputs.release_tag }}",
551
- EXPECTED_SHA: "${{ steps.release_metadata.outputs.expected_sha }}",
552
- },
553
- run: [
554
- 'git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"',
555
- 'test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$EXPECTED_SHA"',
556
- 'test "$(git rev-parse HEAD)" = "$EXPECTED_SHA"',
557
- ].join("\n"),
558
- },
559
- ]
560
- : []),
445
+ ...releaseSourceSteps(),
446
+ ...bunCacheRestoreSteps(project),
561
447
  { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
562
- ...(usesRustArtifacts
563
- ? [
564
- {
565
- name: "Require Rust artifact handoff",
566
- if: "${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_run' }}",
567
- shell: "bash",
568
- env: {
569
- RUST_RUN_ID:
570
- "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.rust_run_id || github.event.workflow_run.id }}",
571
- RUST_RUN_ATTEMPT:
572
- "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.rust_run_attempt || github.event.workflow_run.run_attempt }}",
573
- },
574
- run: ['test -n "$RUST_RUN_ID"', 'test -n "$RUST_RUN_ATTEMPT"'].join("\n"),
575
- },
576
- ]
577
- : []),
578
- ...uniffiPublications.map((publication) => ({
579
- name: `Download ${publication.distribution} native wheels`,
580
- if: "${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_run' }}",
581
- uses: "actions/download-artifact@v8",
582
- with: {
448
+ { name: "Install release helpers", run: "bun install" },
449
+ bunCacheSaveStep(),
450
+ ...uniffiPublications.flatMap((publication) =>
451
+ releaseArtifactSteps({
452
+ currentName: `Download ${publication.distribution} native wheels`,
453
+ recoveredName: `Download recovered ${publication.distribution} native wheels`,
583
454
  pattern: `${publication.distribution}--*--python-wheel`,
584
455
  path: `dist/${publication.directory}`,
585
- "merge-multiple": true,
586
- "run-id":
587
- "${{ github.event_name == 'repository_dispatch' && github.event.client_payload.rust_run_id || github.event.workflow_run.id }}",
588
- "github-token": "${{ github.token }}",
589
- },
590
- })),
456
+ }),
457
+ ),
591
458
  {
592
459
  name: "Stamp workspace versions",
593
- env: {
594
- VERSION: branchDispatch
595
- ? RELEASE_TAG
596
- : "${{ github.event_name == 'push' && github.ref_name || github.event_name == 'workflow_run' && github.event.workflow_run.event == 'repository_dispatch' && steps.release_metadata.outputs.release_tag || inputs.version }}",
597
- },
598
- run: upstreamWorkflow
599
- ? [
600
- 'if [ "$GITHUB_EVENT_NAME" = "workflow_run" ]; then',
601
- ' if [ "${{ github.event.workflow_run.event }}" != "repository_dispatch" ]; then',
602
- ' VERSION="$(git tag --points-at HEAD --list "v*" | sort -V | tail -1)"',
603
- " fi",
604
- ' test -n "$VERSION"',
605
- "fi",
606
- this.renderVersionStampScript(),
607
- "mkdir -p .release",
608
- 'printf "%s\\n" "$VERSION" > .release/tag',
609
- "git rev-parse HEAD > .release/sha",
610
- ].join("\n")
611
- : [
612
- this.renderVersionStampScript(),
613
- "mkdir -p .release",
614
- 'printf "%s\\n" "$VERSION" > .release/tag',
615
- "git rev-parse HEAD > .release/sha",
616
- ].join("\n"),
460
+ env: { VERSION: RELEASE_VERSION },
461
+ run: `bun node_modules/@dbx-tools/projen/tasks/stamp-python.ts "$VERSION" --root ${quote(this.repository.root)}`,
617
462
  },
618
463
  {
619
464
  name: "Build distributions",
@@ -633,7 +478,7 @@ export class DBXToolsPythonWorkspace extends Component {
633
478
  ),
634
479
  ...uniffiPublications.map(
635
480
  (publication) =>
636
- `if [ "$GITHUB_EVENT_NAME" = "repository_dispatch" ] || [ "$GITHUB_EVENT_NAME" = "workflow_run" ]; then find dist/${publication.directory} -maxdepth 1 -name '*.whl' -print -quit | grep -q .; fi`,
481
+ `find dist/${publication.directory} -maxdepth 1 -name '*.whl' -print -quit | grep -q .`,
637
482
  ),
638
483
  "find dist -type f \\( -name '*.whl' -o -name '*.tar.gz' \\) -print0 | xargs -0 uvx twine check",
639
484
  ].join("\n"),
@@ -641,23 +486,17 @@ export class DBXToolsPythonWorkspace extends Component {
641
486
  {
642
487
  name: "Upload distributions",
643
488
  uses: "actions/upload-artifact@v7",
644
- with: { name: "python-distributions", path: "dist" },
645
- },
646
- {
647
- name: "Upload release metadata",
648
- uses: "actions/upload-artifact@v7",
649
- with: { name: "release-metadata", path: ".release" },
489
+ with: { name: "python-distributions", path: "dist", "retention-days": 7 },
650
490
  },
651
491
  ],
652
492
  });
653
493
  for (const publication of allPublications) {
654
- workflow.addJob(`publish-${publication.directory}`, {
655
- if: branchDispatch
656
- ? "${{ github.event_name == 'repository_dispatch' }}"
657
- : "${{ github.event_name == 'push' || github.event_name == 'workflow_run' }}",
494
+ workflow.addJob(`publish-pypi-${publication.directory}`, {
495
+ if: releasePublishCondition("python"),
658
496
  needs: [
659
- "build",
660
- ...(publication.dependencies ?? []).map((dependency) => `publish-${dependency}`),
497
+ "verify-context",
498
+ "build-python",
499
+ ...(publication.dependencies ?? []).map((dependency) => `publish-pypi-${dependency}`),
661
500
  ],
662
501
  environment: {
663
502
  name: publication.environment,
@@ -677,7 +516,10 @@ export class DBXToolsPythonWorkspace extends Component {
677
516
  {
678
517
  name: `Publish ${publication.distribution} to PyPI`,
679
518
  uses: "pypa/gh-action-pypi-publish@release/v1",
680
- with: { "packages-dir": `dist/${publication.directory}` },
519
+ with: {
520
+ "packages-dir": `dist/${publication.directory}`,
521
+ "skip-existing": true,
522
+ },
681
523
  },
682
524
  ],
683
525
  });
@@ -692,7 +534,6 @@ export class DBXToolsPythonWorkspace extends Component {
692
534
  distribution: pkg.packageOptions.name,
693
535
  environment:
694
536
  options.environments?.[pkg.packageOptions.name] ?? `pypi-${pkg.packageOptions.name}`,
695
- workflowName: options.workflowName ?? "python-release",
696
537
  }));
697
538
  }
698
539
 
@@ -706,7 +547,6 @@ export class DBXToolsPythonWorkspace extends Component {
706
547
  pkg.packageOptions.trustedPublisher?.environment ??
707
548
  options.environments?.[pkg.packageOptions.name] ??
708
549
  `pypi-${pkg.packageOptions.name}`,
709
- workflowName: options.workflowName ?? "python-release",
710
550
  artifacts: pkg.packageOptions.trustedPublisher?.artifacts,
711
551
  dependencies: pkg.packageOptions.internalDependencies,
712
552
  }));
@@ -725,7 +565,6 @@ export class DBXToolsPythonWorkspace extends Component {
725
565
  directory: pkg.packageOptions.directory,
726
566
  distribution: pkg.packageOptions.name,
727
567
  environment: publisher.environment,
728
- workflowName: publisher.workflowName,
729
568
  artifacts: publisher.artifacts,
730
569
  });
731
570
  }
@@ -738,13 +577,13 @@ export class DBXToolsPythonWorkspace extends Component {
738
577
  ): void {
739
578
  const repository = this.githubRepository();
740
579
  const publications = this.trustedPublisherPublications(options);
741
- const releaseBranch = projectReleaseBranch(project);
580
+ const releaseTags = isDBXToolsJavaScriptProject()(project) ? releaseTagPattern(project) : "v*";
742
581
  const linesBeforeAuthentication = [
743
582
  "# PyPI Trusted Publisher Setup Instructions",
744
583
  "",
745
584
  "Use the system browser to audit and configure the PyPI trusted publishers below.",
746
585
  "Do not use an in-app browser or embedded webview.",
747
- "Do not visit GitHub or use the GitHub API or CLI. Every required GitHub owner, repository, workflow, environment, and branch value is provided below and is authoritative.",
586
+ "Do not visit GitHub or use the GitHub API or CLI. Every required GitHub owner, repository, workflow, environment, and tag value is provided below and is authoritative.",
748
587
  "Use only PyPI pages for this task.",
749
588
  "",
750
589
  "## Audit and confirmation",
@@ -762,7 +601,7 @@ export class DBXToolsPythonWorkspace extends Component {
762
601
  "",
763
602
  "## GitHub environment policy",
764
603
  "",
765
- `- The supplied branch policy value is ${releaseBranch}.`,
604
+ `- The supplied tag policy value is ${releaseTags}.`,
766
605
  "- Do not inspect or configure GitHub environments during this task.",
767
606
  "- GitHub environment administration is a separate task; use the supplied values only when comparing PyPI publisher entries.",
768
607
  "",
@@ -797,14 +636,13 @@ export class DBXToolsPythonWorkspace extends Component {
797
636
  "Publisher type: GitHub Actions",
798
637
  "",
799
638
  ...publications.flatMap((publication) => {
800
- const workflowName = `${publication.workflowName}.yml`;
801
639
  return [
802
640
  `## ${publication.distribution}`,
803
641
  `- Owner: ${repository.owner}`,
804
642
  `- Repository name: ${repository.name}`,
805
- `- Workflow name: ${workflowName}`,
643
+ "- Workflow name: release.yml",
806
644
  `- Environment name: ${publication.environment}`,
807
- `- GitHub environment branch: ${releaseBranch}`,
645
+ `- GitHub environment tag: ${releaseTags}`,
808
646
  ...(publication.artifacts ? [`- Artifacts: ${publication.artifacts}`] : []),
809
647
  "",
810
648
  ];
@@ -846,44 +684,4 @@ export class DBXToolsPythonWorkspace extends Component {
846
684
  }
847
685
  return { owner: match[1], name: match[2] };
848
686
  }
849
-
850
- private renderVersionStampScript(): string {
851
- return [
852
- `chmod -R u+w ${this.repository.root}`,
853
- "python - <<'PY'",
854
- "from pathlib import Path",
855
- "import os",
856
- "import re",
857
- "",
858
- 'version = os.environ["VERSION"].removeprefix("v")',
859
- `package_files = sorted(Path(${quote(this.repository.root)}).glob("*/pyproject.toml"))`,
860
- "packages = {}",
861
- "for path in package_files:",
862
- ' source = path.read_text(encoding="utf-8")',
863
- ' name = re.search(r\'^name = "([^"]+)"$\', source, re.MULTILINE)',
864
- " if name is None:",
865
- ' raise ValueError(f"Missing project name in {path}")',
866
- " packages[name.group(1)] = path.parent.name",
867
- "",
868
- "for path in package_files:",
869
- ' source = path.read_text(encoding="utf-8")',
870
- " source, count = re.subn(",
871
- ' r\'^version = "[^"]+"$\',',
872
- " f'version = \"{version}\"',",
873
- " source,",
874
- " count=1,",
875
- " flags=re.MULTILINE,",
876
- " )",
877
- " if count != 1:",
878
- ' raise ValueError(f"Expected one project version in {path}")',
879
- " for name, directory in packages.items():",
880
- " source = re.sub(",
881
- ` rf'{re.escape(name)} @ git\\+[^" ]+#subdirectory=${this.repository.root}/{re.escape(directory)}',`,
882
- ' f"{name}=={version}",',
883
- " source,",
884
- " )",
885
- ' path.write_text(source, encoding="utf-8")',
886
- "PY",
887
- ].join("\n");
888
- }
889
687
  }