@dbx-tools/projen 0.6.152 → 0.6.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/project-rs.ts CHANGED
@@ -2,11 +2,23 @@
2
2
  import { existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { project as coreProject } from "@dbx-tools/core";
5
6
  import { string } from "@dbx-tools/shared-core";
6
7
  import { Project, TextFile, YamlFile, javascript } from "projen";
7
8
  import type { DBXToolsProject } from "./project.ts";
8
- import { DBXToolsTypeScriptProject } from "./project-js.ts";
9
- import type { PythonPackageOptions } from "./project-py.ts";
9
+ import {
10
+ DBXToolsTypeScriptProject,
11
+ projectReleaseBranch,
12
+ projectRepositoryUrl,
13
+ } from "./project-js.ts";
14
+ import { pythonModuleName, type PythonPackageOptions } from "./project-py.ts";
15
+ import {
16
+ DOWNSTREAM_RELEASE_EVENT,
17
+ RELEASE_SHA,
18
+ RELEASE_TAG,
19
+ RUST_RELEASE_EVENT,
20
+ releaseSourceSteps,
21
+ } from "./release-dispatch.ts";
10
22
  import { readWorkspaceVersion } from "./workspace-version.ts";
11
23
  import { isDBXToolsJavaScriptProject } from "./project-predicate.ts";
12
24
 
@@ -42,9 +54,14 @@ export interface RustPackageOptions {
42
54
 
43
55
  export interface DBXToolsRustWorkspaceOptions {
44
56
  readonly root?: string;
45
- readonly scope: string;
57
+ readonly scope?: string;
46
58
  readonly edition?: string;
59
+ /** Minimum supported Rust version recorded in Cargo manifests. */
47
60
  readonly rustVersion?: string;
61
+ /** Rust toolchain used by release builds. Defaults to `stable`. */
62
+ readonly releaseRustVersion?: string;
63
+ /** Rust toolchain used to compile the host-side UBRN generator. Defaults to releaseRustVersion. */
64
+ readonly ubrnRustVersion?: string;
48
65
  readonly license?: string;
49
66
  readonly repository?: string;
50
67
  readonly workspaceDependencies?: Readonly<Record<string, CargoDependency>>;
@@ -61,6 +78,8 @@ export interface DBXToolsRustWorkspaceOptions {
61
78
  readonly releasePlatforms?: readonly RustReleasePlatform[];
62
79
  /** Workflow name used by downstream release stages. Defaults to `rust-release`. */
63
80
  readonly releaseWorkflowName?: string;
81
+ /** Tag prefix dispatched into the branch-scoped release workflow. Defaults to `v`. */
82
+ readonly releaseTagPrefix?: string;
64
83
  }
65
84
 
66
85
  export enum RustReleaseOs {
@@ -139,12 +158,19 @@ const RUST_CACHE_ENV = {
139
158
  RUSTC_WRAPPER: "sccache",
140
159
  SCCACHE_GHA_ENABLED: "true",
141
160
  } as const;
161
+ const UBRN_VERSION = "0.31.0-5";
162
+ const RELEASE_PLATFORMS_ENV = "DBX_TOOLS_RELEASE_PLATFORMS";
142
163
 
143
- function rustCacheSteps(sharedKey: string): readonly Record<string, unknown>[] {
164
+ function rustCacheSteps(sharedKey: string, idPrefix = ""): readonly Record<string, unknown>[] {
144
165
  return [
145
- { name: "Setup sccache", uses: "mozilla-actions/sccache-action@v0.0.11" },
166
+ {
167
+ name: "Setup sccache",
168
+ id: `${idPrefix}sccache`,
169
+ uses: "mozilla-actions/sccache-action@v0.0.11",
170
+ },
146
171
  {
147
172
  name: "Cache Cargo registry",
173
+ id: `${idPrefix}cargo_cache`,
148
174
  uses: "Swatinem/rust-cache@v2.9.2",
149
175
  with: {
150
176
  "cache-targets": false,
@@ -156,13 +182,23 @@ function rustCacheSteps(sharedKey: string): readonly Record<string, unknown>[] {
156
182
  ];
157
183
  }
158
184
 
185
+ function timedBash(phase: string, command: string): string {
186
+ return [
187
+ "SECONDS=0",
188
+ `trap 'status=$?; echo "phase=${phase} duration_seconds=$SECONDS status=$status"; exit "$status"' EXIT`,
189
+ command,
190
+ ].join("\n");
191
+ }
192
+
159
193
  function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIReleaseTarget[] {
160
194
  if (options.releaseTargets && options.releasePlatforms) {
161
195
  throw new Error("releaseTargets and releasePlatforms are mutually exclusive");
162
196
  }
163
197
  if (options.releaseTargets) return options.releaseTargets;
164
- if (!options.releasePlatforms) return UNIFFI_RELEASE_TARGETS;
165
- return options.releasePlatforms.map((platform) => {
198
+ const releasePlatforms =
199
+ options.releasePlatforms ?? releasePlatformsFromEnvironment(process.env[RELEASE_PLATFORMS_ENV]);
200
+ if (!releasePlatforms) return UNIFFI_RELEASE_TARGETS;
201
+ return releasePlatforms.map((platform) => {
166
202
  const target = UNIFFI_RELEASE_TARGETS.find(
167
203
  (candidate) => candidate.os === platform.os && candidate.cpu === platform.cpu,
168
204
  );
@@ -172,6 +208,23 @@ function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIR
172
208
  });
173
209
  }
174
210
 
211
+ function releasePlatformsFromEnvironment(
212
+ value: string | undefined,
213
+ ): RustReleasePlatform[] | undefined {
214
+ if (!value?.trim()) return undefined;
215
+ return value.split(",").map((entry) => {
216
+ const [os, cpu, ...extra] = entry.split(":").map((part) => part.trim());
217
+ if (
218
+ extra.length ||
219
+ !Object.values(RustReleaseOs).includes(os as RustReleaseOs) ||
220
+ !Object.values(RustReleaseCpu).includes(cpu as RustReleaseCpu)
221
+ ) {
222
+ throw new Error(`Invalid ${RELEASE_PLATFORMS_ENV} entry: ${entry}`);
223
+ }
224
+ return { os: os as RustReleaseOs, cpu: cpu as RustReleaseCpu };
225
+ });
226
+ }
227
+
175
228
  function uniffiReleaseTaskSource(): string {
176
229
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
177
230
  const candidates = [
@@ -191,6 +244,7 @@ export interface RustBindingMapping {
191
244
  readonly python?: string;
192
245
  readonly nodePackage?: string;
193
246
  readonly pythonPackage?: string;
247
+ readonly pythonModule?: string;
194
248
  readonly facadeTarget?: boolean;
195
249
  }
196
250
 
@@ -199,6 +253,7 @@ export interface RustWorkspaceMapping {
199
253
  readonly root: string;
200
254
  readonly crates: readonly string[];
201
255
  readonly bindings: readonly RustBindingMapping[];
256
+ readonly releaseWorkflow?: string;
202
257
  }
203
258
 
204
259
  function rustSources(directory: string): string[] {
@@ -373,10 +428,18 @@ export class DBXToolsRustWorkspace {
373
428
  constructor(project: javascript.NodeProject, options: DBXToolsRustWorkspaceOptions) {
374
429
  const root = options.root ?? "packages/rs";
375
430
  const nodeRoot = options.nodeRoot ?? "packages/js/node";
431
+ const dbxToolsProject = isDBXToolsJavaScriptProject()(project) ? project : undefined;
432
+ const scope = string.toSlug(options.scope ?? dbxToolsProject?.scope ?? project.name);
433
+ const repository =
434
+ options.repository ??
435
+ projectRepositoryUrl(project) ??
436
+ coreProject.repositoryUrl(project.outdir) ??
437
+ "";
438
+ const pythonModulePrefix = options.pythonModulePrefix ?? scope.replaceAll("-", "_");
376
439
  const packageOptions = options.packages ?? {};
377
440
  this.packages = discoverRustCrates(resolve(project.outdir, root)).map(
378
441
  (directory) =>
379
- new DBXToolsRustProject(project, root, options.scope, {
442
+ new DBXToolsRustProject(project, root, scope, {
380
443
  directory,
381
444
  ...packageOptions[directory],
382
445
  }),
@@ -392,44 +455,69 @@ export class DBXToolsRustWorkspace {
392
455
  ...(targets.includes("node")
393
456
  ? {
394
457
  node: `${nodeRoot}/${pkg.packageOptions.directory}`,
395
- nodePackage: `@${string.toSlug(options.scope)}/${packageName}`,
458
+ nodePackage: `@${scope}/${packageName}`,
396
459
  }
397
460
  : {}),
398
461
  ...(targets.includes("python")
399
462
  ? {
400
463
  python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
401
464
  pythonPackage: pkg.crateName,
465
+ pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
402
466
  }
403
467
  : {}),
404
468
  };
405
469
  });
470
+ const releaseEnabled =
471
+ (options.release ?? true) &&
472
+ (this.bindingMappings.length > 0 ||
473
+ this.packages.some((pkg) => pkg.packageOptions.release || !pkg.packageOptions.private));
406
474
  this.workspaceMapping = {
407
475
  root,
408
476
  crates: this.packages.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
409
477
  bindings: this.bindingMappings,
478
+ ...(releaseEnabled ? { releaseWorkflow: options.releaseWorkflowName ?? "rust-release" } : {}),
410
479
  };
411
- if (isDBXToolsJavaScriptProject()(project)) {
412
- project.dbxToolsConfig.rust = this.workspaceMapping;
480
+ if (dbxToolsProject) {
481
+ dbxToolsProject.dbxToolsConfig.rust = this.workspaceMapping;
413
482
  }
483
+ project.gitignore.addPatterns(
484
+ "target/",
485
+ ...this.bindingMappings.flatMap((binding) => [
486
+ ...(binding.node
487
+ ? [
488
+ `${binding.node}/src/bindings.ts`,
489
+ `${binding.node}/src/_bindings*.ts`,
490
+ `${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`,
491
+ ]
492
+ : []),
493
+ ...(binding.python && binding.pythonModule
494
+ ? [
495
+ `${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/bindings.py`,
496
+ `${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/*${binding.crate.replaceAll("-", "_")}.*`,
497
+ ]
498
+ : []),
499
+ ]),
500
+ );
414
501
  this.pythonPackages = bindings
415
502
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
416
- .map((pkg) => ({
417
- directory: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`.replace(
418
- /^packages\/py\//,
419
- "",
420
- ),
421
- name: pkg.crateName,
422
- module: `${options.pythonModulePrefix ?? options.scope.replaceAll("-", "_")}.${pkg.packageOptions.directory.replaceAll("-", "_")}`,
423
- description: `Python bindings for ${pkg.crateName}`,
424
- private: true,
425
- trustedPublisher: {
426
- workflowName: options.releaseWorkflowName ?? "rust-release",
427
- environment: `pypi-${pkg.crateName}`,
428
- artifacts: `platform-specific wheels for ${releaseTargets(options)
429
- .map((target) => `${target.os}-${target.cpu}`)
430
- .join(", ")}; all architectures publish to this one PyPI project`,
431
- },
432
- }));
503
+ .map((pkg) => {
504
+ const module = pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory);
505
+ return {
506
+ directory: pkg.packageOptions.directory,
507
+ name: pkg.crateName,
508
+ module,
509
+ description: `Python bindings for ${pkg.crateName}`,
510
+ private: true,
511
+ generatedSources: [`src/${module.replaceAll(".", "/")}/bindings.py`],
512
+ trustedPublisher: {
513
+ workflowName: options.releaseWorkflowName ?? "rust-release",
514
+ environment: `pypi-${pkg.crateName}`,
515
+ artifacts: `platform-specific wheels for ${releaseTargets(options)
516
+ .map((target) => `${target.os}-${target.cpu}`)
517
+ .join(", ")}; all architectures publish to this one PyPI project`,
518
+ },
519
+ };
520
+ });
433
521
 
434
522
  const existing = new Map(
435
523
  project.subprojects.map((child) => [relative(project.outdir, child.outdir), child]),
@@ -447,12 +535,12 @@ export class DBXToolsRustWorkspace {
447
535
  : new DBXToolsTypeScriptProject({
448
536
  parent: project,
449
537
  outdir: memberPath,
450
- name: `@${string.toSlug(options.scope)}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
538
+ name: `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
451
539
  tags: ["node"],
452
540
  });
453
541
  node.package.addField(
454
542
  "name",
455
- `@${string.toSlug(options.scope)}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
543
+ `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
456
544
  );
457
545
  node.package.addField("private", true);
458
546
  node.package.file.addDeletionOverride("publishConfig");
@@ -463,7 +551,7 @@ export class DBXToolsRustWorkspace {
463
551
  "optionalDependencies",
464
552
  Object.fromEntries(
465
553
  nativeTargets.map((target) => [
466
- `@${string.toSlug(options.scope)}/${directory}-${target.node}`,
554
+ `@${scope}/${directory}-${target.node}`,
467
555
  readWorkspaceVersion(project.outdir),
468
556
  ]),
469
557
  ),
@@ -473,7 +561,7 @@ export class DBXToolsRustWorkspace {
473
561
  if (binding.packageOptions.nodeDependencies?.length) {
474
562
  node.addDeps(...binding.packageOptions.nodeDependencies);
475
563
  }
476
- node.addDevDeps("uniffi-bindgen-react-native@0.31.0-5");
564
+ node.addDevDeps(`uniffi-bindgen-react-native@${UBRN_VERSION}`);
477
565
  if (binding.packageOptions.nodeDevDependencies?.length) {
478
566
  node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
479
567
  }
@@ -498,7 +586,7 @@ export class DBXToolsRustWorkspace {
498
586
  edition: options.edition ?? "2021",
499
587
  "rust-version": options.rustVersion ?? "1.82",
500
588
  license: options.license ?? "Apache-2.0",
501
- repository: options.repository ?? "",
589
+ repository,
502
590
  },
503
591
  ...(options.workspaceDependencies
504
592
  ? {
@@ -534,13 +622,7 @@ export class DBXToolsRustWorkspace {
534
622
  ]),
535
623
  ].join(" && "),
536
624
  });
537
- if (
538
- (options.release ?? true) &&
539
- (this.bindingMappings.length > 0 ||
540
- this.packages.some(
541
- (pkg) => pkg.packageOptions.release || !pkg.packageOptions.private,
542
- ))
543
- ) {
625
+ if (releaseEnabled) {
544
626
  this.addReleaseWorkflow(project, options, releaseTargets(options));
545
627
  }
546
628
  }
@@ -551,6 +633,15 @@ export class DBXToolsRustWorkspace {
551
633
  targets: readonly UniFFIReleaseTarget[],
552
634
  ): void {
553
635
  if (!project.github) return;
636
+ const workflowName = options.releaseWorkflowName ?? "rust-release";
637
+ const releaseTagPrefix = options.releaseTagPrefix ?? "v";
638
+ const releaseRustVersion = options.releaseRustVersion ?? "stable";
639
+ const ubrnRustVersion = options.ubrnRustVersion ?? releaseRustVersion;
640
+ const ubrnToolchainInstall =
641
+ ubrnRustVersion === releaseRustVersion
642
+ ? ""
643
+ : `rustup toolchain install ${ubrnRustVersion} --profile minimal\n`;
644
+ const releaseBranch = projectReleaseBranch(project);
554
645
  const bindings = this.bindingMappings.map((binding) => ({
555
646
  ...binding,
556
647
  node: binding.node ?? "",
@@ -572,7 +663,11 @@ export class DBXToolsRustWorkspace {
572
663
  }));
573
664
  const hasNodeBindings = bindings.some((binding) => binding.node);
574
665
  const hasPythonBindings = bindings.some((binding) => binding.python);
575
- const hasTargetOutputs = bindings.length > 0 || releaseBinaries.length > 0;
666
+ const hasTargetOutputs =
667
+ bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
668
+ if (hasTargetOutputs && targetMatrix.length === 0) {
669
+ throw new Error("Rust release requires at least one target");
670
+ }
576
671
  const releaseTask = ".projen/uniffi-release.mjs";
577
672
  if (bindings.length) {
578
673
  new TextFile(project, releaseTask, {
@@ -599,7 +694,7 @@ export class DBXToolsRustWorkspace {
599
694
  '--cpu "${{ matrix.target.cpu }}"',
600
695
  '--libc "${{ matrix.target.libc }}"',
601
696
  '--facade "${{ matrix.target.facade }}"',
602
- '--version "${VERSION#v}"',
697
+ `--version "\${VERSION#${releaseTagPrefix}}"`,
603
698
  `--output "dist/release/${binding.crate}/\${{ matrix.target.node }}"`,
604
699
  "--skip-build",
605
700
  ].join(" \\\n "),
@@ -663,14 +758,18 @@ export class DBXToolsRustWorkspace {
663
758
  ];
664
759
  const buildJob = {
665
760
  name: "${{ matrix.target.node }}",
761
+ needs: ["verify-context"],
666
762
  "runs-on": "${{ matrix.target.runner }}",
667
- env: RUST_CACHE_ENV,
763
+ env: {
764
+ ...RUST_CACHE_ENV,
765
+ SCCACHE_GHA_VERSION: `release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`,
766
+ },
668
767
  strategy: {
669
768
  "fail-fast": false,
670
769
  matrix: { include: targetMatrix },
671
770
  },
672
771
  steps: [
673
- { name: "Checkout", uses: "actions/checkout@v6" },
772
+ ...releaseSourceSteps(),
674
773
  ...(hasNodeBindings
675
774
  ? [
676
775
  {
@@ -680,24 +779,81 @@ export class DBXToolsRustWorkspace {
680
779
  },
681
780
  ]
682
781
  : []),
683
- ...(hasPythonBindings
684
- ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }]
685
- : []),
782
+ ...(hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
686
783
  {
687
784
  name: "Setup Rust",
688
- uses: "dtolnay/rust-toolchain@stable",
785
+ uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
689
786
  with: { targets: "${{ matrix.target.cargo }}" },
690
787
  },
691
- ...rustCacheSteps("release-${{ matrix.target.cargo }}"),
788
+ ...rustCacheSteps(`release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`),
789
+ ...(hasNodeBindings
790
+ ? [
791
+ {
792
+ name: "Cache UBRN generator",
793
+ id: "ubrn_cache",
794
+ if: "${{ vars.CACHE_UBRN_TARGET == 'true' }}",
795
+ uses: "actions/cache@v5",
796
+ with: {
797
+ path: "target/ubrn",
798
+ key: `ubrn-\${{ runner.os }}-\${{ runner.arch }}-rust-${ubrnRustVersion}-${UBRN_VERSION}`,
799
+ },
800
+ },
801
+ ]
802
+ : []),
803
+ {
804
+ name: "Log cache configuration",
805
+ shell: "bash",
806
+ run: [
807
+ `echo "cargo_cache_namespace=release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}"`,
808
+ 'echo "cargo_cache_hit=${{ steps.cargo_cache.outputs.cache-hit }}"',
809
+ 'echo "sccache_scope=${{ github.ref }}"',
810
+ 'echo "sccache_namespace=${SCCACHE_GHA_VERSION}"',
811
+ `echo "ubrn_rust_toolchain=${ubrnRustVersion}"`,
812
+ ...(hasNodeBindings
813
+ ? [
814
+ "echo \"ubrn_target_cache_enabled=${{ vars.CACHE_UBRN_TARGET == 'true' }}\"",
815
+ `echo "ubrn_target_cache_key=ubrn-\${{ runner.os }}-\${{ runner.arch }}-rust-${ubrnRustVersion}-${UBRN_VERSION}"`,
816
+ 'echo "ubrn_target_cache_hit=${{ steps.ubrn_cache.outputs.cache-hit }}"',
817
+ ]
818
+ : []),
819
+ ].join("\n"),
820
+ },
692
821
  {
693
822
  name: "Install Linux native dependencies",
694
823
  if: "${{ matrix.target.os == 'linux' }}",
695
824
  run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
696
825
  },
697
- ...(hasNodeBindings ? [{ name: "Install Node tooling", run: "bun install" }] : []),
826
+ ...(hasNodeBindings
827
+ ? [
828
+ {
829
+ name: "Install Node tooling",
830
+ shell: "bash",
831
+ run: timedBash("node_tooling", "bun install"),
832
+ },
833
+ ]
834
+ : []),
835
+ ...(hasNodeBindings
836
+ ? [
837
+ {
838
+ name: "Prepare UBRN generator",
839
+ env: {
840
+ CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
841
+ },
842
+ shell: "bash",
843
+ run: timedBash(
844
+ "ubrn_generator",
845
+ `${ubrnToolchainInstall}cargo +${ubrnRustVersion} build --manifest-path node_modules/uniffi-bindgen-react-native/crates/ubrn_cli/Cargo.toml`,
846
+ ),
847
+ },
848
+ ]
849
+ : []),
698
850
  {
699
851
  name: "Build Rust outputs",
700
- run: 'cargo build --release --workspace --target "${{ matrix.target.cargo }}"',
852
+ shell: "bash",
853
+ run: timedBash(
854
+ "rust_workspace",
855
+ 'cargo build --release --workspace --target "${{ matrix.target.cargo }}"',
856
+ ),
701
857
  },
702
858
  ...(bindingCommands.length
703
859
  ? [
@@ -705,9 +861,14 @@ export class DBXToolsRustWorkspace {
705
861
  name: "Package UniFFI outputs",
706
862
  shell: "bash",
707
863
  env: {
708
- VERSION: "${{ github.event_name == 'push' && github.ref_name || inputs.version }}",
864
+ VERSION: RELEASE_TAG,
865
+ ...(hasNodeBindings
866
+ ? {
867
+ CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
868
+ }
869
+ : {}),
709
870
  },
710
- run: bindingCommands.join("\n"),
871
+ run: timedBash("uniffi_packaging", bindingCommands.join("\n")),
711
872
  },
712
873
  ]
713
874
  : []),
@@ -716,27 +877,31 @@ export class DBXToolsRustWorkspace {
716
877
  {
717
878
  name: "Package release binaries",
718
879
  shell: "bash",
719
- run: binaryCommands.join("\n"),
880
+ run: timedBash("binary_packaging", binaryCommands.join("\n")),
720
881
  },
721
882
  ]
722
883
  : []),
723
884
  ...artifactSteps,
885
+ {
886
+ name: "Log sccache statistics",
887
+ if: "${{ always() }}",
888
+ shell: "bash",
889
+ run: '"${SCCACHE_PATH}" --show-stats',
890
+ },
724
891
  ],
725
892
  };
726
893
  const bindingPublishJobs = Object.fromEntries(
727
894
  bindings.map((binding) => [
728
895
  `publish-${binding.crate}`,
729
896
  {
730
- if: "${{ github.event_name == 'push' }}",
897
+ if: "${{ github.event_name == 'repository_dispatch' }}",
731
898
  needs: ["build"],
732
899
  "runs-on": "ubuntu-latest",
733
900
  permissions: {
734
901
  contents: "read",
735
902
  ...(binding.python ? { "id-token": "write" } : {}),
736
903
  },
737
- ...(binding.python
738
- ? { environment: { name: `pypi-${binding.crate}` } }
739
- : {}),
904
+ ...(binding.python ? { environment: { name: `pypi-${binding.crate}` } } : {}),
740
905
  steps: [
741
906
  ...(binding.node
742
907
  ? [
@@ -796,36 +961,86 @@ export class DBXToolsRustWorkspace {
796
961
  },
797
962
  ]),
798
963
  );
799
- const workflowName = options.releaseWorkflowName ?? "rust-release";
964
+ const releaseCompletionJobs = [
965
+ "build",
966
+ ...bindings.map((binding) => `publish-${binding.crate}`),
967
+ ...(publicCrates.length ? ["publish-cargo"] : []),
968
+ ...(releaseBinaries.length ? ["publish-github-release"] : []),
969
+ ];
800
970
  new YamlFile(project, `.github/workflows/${workflowName}.yml`, {
801
971
  obj: {
802
972
  name: workflowName,
973
+ "run-name": `${workflowName} ${RELEASE_TAG}`,
803
974
  on: {
804
- push: { tags: ["v*"] },
975
+ repository_dispatch: {
976
+ types: [RUST_RELEASE_EVENT],
977
+ },
805
978
  workflow_dispatch: {
806
979
  inputs: {
807
- version: {
808
- description: "Version to package during a dry run",
980
+ release_tag: {
981
+ description: "Annotated release tag to build",
982
+ type: "string",
983
+ required: true,
984
+ },
985
+ expected_sha: {
986
+ description: "Commit the release tag must reference",
809
987
  type: "string",
810
- default: "0.0.0.dev0",
988
+ required: true,
811
989
  },
812
990
  },
813
991
  },
814
992
  },
815
993
  permissions: { contents: "read" },
816
994
  jobs: {
995
+ "verify-context": {
996
+ "runs-on": "ubuntu-latest",
997
+ steps: [
998
+ {
999
+ name: "Require the default branch cache scope",
1000
+ shell: "bash",
1001
+ env: {
1002
+ RELEASE_BRANCH: releaseBranch,
1003
+ },
1004
+ run: 'test "$GITHUB_REF_NAME" = "$RELEASE_BRANCH"',
1005
+ },
1006
+ {
1007
+ name: "Write release metadata",
1008
+ shell: "bash",
1009
+ env: {
1010
+ RELEASE_TAG,
1011
+ EXPECTED_SHA: RELEASE_SHA,
1012
+ },
1013
+ run: [
1014
+ "mkdir -p .release",
1015
+ 'printf "%s\\n" "$RELEASE_TAG" > .release/tag',
1016
+ 'printf "%s\\n" "$EXPECTED_SHA" > .release/sha',
1017
+ ].join("\n"),
1018
+ },
1019
+ {
1020
+ name: "Upload release metadata",
1021
+ uses: "actions/upload-artifact@v7",
1022
+ with: {
1023
+ name: "release-metadata",
1024
+ path: ".release",
1025
+ },
1026
+ },
1027
+ ],
1028
+ },
817
1029
  ...(hasTargetOutputs && targetMatrix.length ? { build: buildJob } : {}),
818
1030
  ...bindingPublishJobs,
819
1031
  ...(publicCrates.length
820
1032
  ? {
821
1033
  "publish-cargo": {
822
- if: "${{ github.event_name == 'push' }}",
823
- ...(hasTargetOutputs ? { needs: ["build"] } : {}),
1034
+ if: "${{ github.event_name == 'repository_dispatch' }}",
1035
+ needs: ["build"],
824
1036
  "runs-on": "ubuntu-latest",
825
1037
  permissions: { contents: "read" },
826
1038
  steps: [
827
- { name: "Checkout", uses: "actions/checkout@v6" },
828
- { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
1039
+ ...releaseSourceSteps(),
1040
+ {
1041
+ name: "Setup Rust",
1042
+ uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
1043
+ },
829
1044
  {
830
1045
  name: "Publish public crates",
831
1046
  env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
@@ -840,41 +1055,112 @@ export class DBXToolsRustWorkspace {
840
1055
  },
841
1056
  }
842
1057
  : {}),
843
- "publish-local": {
844
- if: "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}",
845
- "runs-on": ["self-hosted"],
846
- permissions: { contents: "read" },
847
- env: RUST_CACHE_ENV,
848
- steps: [
849
- { name: "Checkout", uses: "actions/checkout@v6" },
850
- {
851
- name: "Setup Bun",
852
- uses: "oven-sh/setup-bun@v2",
853
- with: { "bun-version": "1.3.14" },
854
- },
855
- { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
856
- { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
857
- ...rustCacheSteps("local-${{ runner.os }}-${{ runner.arch }}"),
858
- { name: "Install", run: "bun install" },
859
- {
860
- name: "Build and publish host-native packages",
861
- run: [
862
- 'VERSION="${GITHUB_REF_NAME#v}"',
863
- 'bun node_modules/@dbx-tools/projen/tasks/publish-uniffi-local.ts --version "$VERSION" --registry "${{ vars.LOCAL_NPM_REGISTRY }}" --pypi-publish-url "${{ vars.LOCAL_PYPI_PUBLISH_URL }}" --cargo-registry "${{ vars.LOCAL_CARGO_REGISTRY }}"',
864
- ].join("\n"),
865
- env: {
866
- NODE_AUTH_TOKEN: "${{ secrets.LOCAL_NPM_TOKEN }}",
867
- UV_PUBLISH_USERNAME: "${{ secrets.LOCAL_PYPI_USERNAME }}",
868
- UV_PUBLISH_PASSWORD: "${{ secrets.LOCAL_PYPI_PASSWORD }}",
869
- CARGO_REGISTRY_TOKEN: "${{ secrets.LOCAL_CARGO_TOKEN }}",
1058
+ ...(bindings.length
1059
+ ? {
1060
+ "publish-local-bindings": {
1061
+ if: "${{ github.event_name == 'repository_dispatch' && vars.LOCAL_REPOSITORIES == 'true' }}",
1062
+ needs: ["build"],
1063
+ "runs-on": ["self-hosted"],
1064
+ permissions: { contents: "read" },
1065
+ steps: [
1066
+ ...(hasNodeBindings
1067
+ ? [
1068
+ {
1069
+ name: "Setup Node.js",
1070
+ uses: "actions/setup-node@v6",
1071
+ with: {
1072
+ "registry-url": "${{ vars.LOCAL_NPM_REGISTRY }}",
1073
+ },
1074
+ },
1075
+ {
1076
+ name: "Download native npm packages",
1077
+ uses: "actions/download-artifact@v8",
1078
+ with: {
1079
+ pattern: "*-npm",
1080
+ path: "dist/npm",
1081
+ "merge-multiple": true,
1082
+ },
1083
+ },
1084
+ {
1085
+ name: "Download npm facades",
1086
+ uses: "actions/download-artifact@v8",
1087
+ with: {
1088
+ pattern: "*-npm-facade",
1089
+ path: "dist/npm-facade",
1090
+ "merge-multiple": true,
1091
+ },
1092
+ },
1093
+ {
1094
+ name: "Publish npm packages locally",
1095
+ env: {
1096
+ NODE_AUTH_TOKEN: "${{ secrets.LOCAL_NPM_TOKEN }}",
1097
+ },
1098
+ run: [
1099
+ 'for package in dist/npm/*.tgz; do npm publish "$package" --access public; done',
1100
+ 'for package in dist/npm-facade/*.tgz; do npm publish "$package" --access public; done',
1101
+ ].join("\n"),
1102
+ },
1103
+ ]
1104
+ : []),
1105
+ ...(hasPythonBindings
1106
+ ? [
1107
+ { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
1108
+ {
1109
+ name: "Download Python wheels",
1110
+ uses: "actions/download-artifact@v8",
1111
+ with: {
1112
+ pattern: "*-python-wheel",
1113
+ path: "dist/python",
1114
+ "merge-multiple": true,
1115
+ },
1116
+ },
1117
+ {
1118
+ name: "Publish Python wheels locally",
1119
+ env: {
1120
+ UV_PUBLISH_USERNAME: "${{ secrets.LOCAL_PYPI_USERNAME }}",
1121
+ UV_PUBLISH_PASSWORD: "${{ secrets.LOCAL_PYPI_PASSWORD }}",
1122
+ },
1123
+ run: 'uv publish --publish-url "${{ vars.LOCAL_PYPI_PUBLISH_URL }}" dist/python/*.whl',
1124
+ },
1125
+ ]
1126
+ : []),
1127
+ ],
870
1128
  },
871
- },
872
- ],
873
- },
1129
+ }
1130
+ : {}),
1131
+ ...(publicCrates.length
1132
+ ? {
1133
+ "publish-local-cargo": {
1134
+ if: "${{ github.event_name == 'repository_dispatch' && vars.LOCAL_REPOSITORIES == 'true' }}",
1135
+ needs: ["build"],
1136
+ "runs-on": ["self-hosted"],
1137
+ permissions: { contents: "read" },
1138
+ steps: [
1139
+ ...releaseSourceSteps(),
1140
+ {
1141
+ name: "Setup Rust",
1142
+ uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
1143
+ },
1144
+ {
1145
+ name: "Publish Cargo crates locally",
1146
+ env: {
1147
+ CARGO_REGISTRY_TOKEN: "${{ secrets.LOCAL_CARGO_TOKEN }}",
1148
+ },
1149
+ run: publicCrates
1150
+ .map(
1151
+ (crate) =>
1152
+ `cargo publish --package "${crate}" --registry "\${{ vars.LOCAL_CARGO_REGISTRY }}" --no-verify`,
1153
+ )
1154
+ .join("\n"),
1155
+ },
1156
+ ],
1157
+ },
1158
+ }
1159
+ : {}),
874
1160
  ...(releaseBinaries.length
875
1161
  ? {
876
1162
  "publish-github-release": {
877
- if: "${{ github.event_name == 'push' }}",
1163
+ if: "${{ github.event_name == 'repository_dispatch' }}",
878
1164
  needs: ["build"],
879
1165
  "runs-on": "ubuntu-latest",
880
1166
  permissions: { contents: "write" },
@@ -894,12 +1180,40 @@ export class DBXToolsRustWorkspace {
894
1180
  with: {
895
1181
  files: "dist/rust-release/*",
896
1182
  "generate-release-notes": true,
1183
+ tag_name: RELEASE_TAG,
1184
+ target_commitish: RELEASE_SHA,
897
1185
  },
898
1186
  },
899
1187
  ],
900
1188
  },
901
1189
  }
902
1190
  : {}),
1191
+ "dispatch-downstream": {
1192
+ if: "${{ github.event_name == 'repository_dispatch' }}",
1193
+ needs: releaseCompletionJobs,
1194
+ "runs-on": "ubuntu-latest",
1195
+ permissions: { contents: "write" },
1196
+ steps: [
1197
+ {
1198
+ name: "Dispatch downstream releases",
1199
+ shell: "bash",
1200
+ env: {
1201
+ GH_TOKEN: "${{ github.token }}",
1202
+ RELEASE_TAG,
1203
+ EXPECTED_SHA: RELEASE_SHA,
1204
+ RELEASE_EVENT: DOWNSTREAM_RELEASE_EVENT,
1205
+ },
1206
+ run: [
1207
+ [
1208
+ 'gh api --method POST "repos/$GITHUB_REPOSITORY/dispatches"',
1209
+ '--raw-field event_type="$RELEASE_EVENT"',
1210
+ '--raw-field "client_payload[release_tag]=$RELEASE_TAG"',
1211
+ '--raw-field "client_payload[expected_sha]=$EXPECTED_SHA"',
1212
+ ].join(" \\\n "),
1213
+ ].join("\n"),
1214
+ },
1215
+ ],
1216
+ },
903
1217
  },
904
1218
  },
905
1219
  });