@dbx-tools/projen 0.6.153 → 0.6.160

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 {
@@ -140,12 +159,18 @@ const RUST_CACHE_ENV = {
140
159
  SCCACHE_GHA_ENABLED: "true",
141
160
  } as const;
142
161
  const UBRN_VERSION = "0.31.0-5";
162
+ const RELEASE_PLATFORMS_ENV = "DBX_TOOLS_RELEASE_PLATFORMS";
143
163
 
144
- function rustCacheSteps(sharedKey: string): readonly Record<string, unknown>[] {
164
+ function rustCacheSteps(sharedKey: string, idPrefix = ""): readonly Record<string, unknown>[] {
145
165
  return [
146
- { 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
+ },
147
171
  {
148
172
  name: "Cache Cargo registry",
173
+ id: `${idPrefix}cargo_cache`,
149
174
  uses: "Swatinem/rust-cache@v2.9.2",
150
175
  with: {
151
176
  "cache-targets": false,
@@ -157,13 +182,23 @@ function rustCacheSteps(sharedKey: string): readonly Record<string, unknown>[] {
157
182
  ];
158
183
  }
159
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
+
160
193
  function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIReleaseTarget[] {
161
194
  if (options.releaseTargets && options.releasePlatforms) {
162
195
  throw new Error("releaseTargets and releasePlatforms are mutually exclusive");
163
196
  }
164
197
  if (options.releaseTargets) return options.releaseTargets;
165
- if (!options.releasePlatforms) return UNIFFI_RELEASE_TARGETS;
166
- 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) => {
167
202
  const target = UNIFFI_RELEASE_TARGETS.find(
168
203
  (candidate) => candidate.os === platform.os && candidate.cpu === platform.cpu,
169
204
  );
@@ -173,6 +208,23 @@ function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIR
173
208
  });
174
209
  }
175
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
+
176
228
  function uniffiReleaseTaskSource(): string {
177
229
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
178
230
  const candidates = [
@@ -192,6 +244,7 @@ export interface RustBindingMapping {
192
244
  readonly python?: string;
193
245
  readonly nodePackage?: string;
194
246
  readonly pythonPackage?: string;
247
+ readonly pythonModule?: string;
195
248
  readonly facadeTarget?: boolean;
196
249
  }
197
250
 
@@ -200,6 +253,7 @@ export interface RustWorkspaceMapping {
200
253
  readonly root: string;
201
254
  readonly crates: readonly string[];
202
255
  readonly bindings: readonly RustBindingMapping[];
256
+ readonly releaseWorkflow?: string;
203
257
  }
204
258
 
205
259
  function rustSources(directory: string): string[] {
@@ -374,10 +428,18 @@ export class DBXToolsRustWorkspace {
374
428
  constructor(project: javascript.NodeProject, options: DBXToolsRustWorkspaceOptions) {
375
429
  const root = options.root ?? "packages/rs";
376
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("-", "_");
377
439
  const packageOptions = options.packages ?? {};
378
440
  this.packages = discoverRustCrates(resolve(project.outdir, root)).map(
379
441
  (directory) =>
380
- new DBXToolsRustProject(project, root, options.scope, {
442
+ new DBXToolsRustProject(project, root, scope, {
381
443
  directory,
382
444
  ...packageOptions[directory],
383
445
  }),
@@ -393,44 +455,69 @@ export class DBXToolsRustWorkspace {
393
455
  ...(targets.includes("node")
394
456
  ? {
395
457
  node: `${nodeRoot}/${pkg.packageOptions.directory}`,
396
- nodePackage: `@${string.toSlug(options.scope)}/${packageName}`,
458
+ nodePackage: `@${scope}/${packageName}`,
397
459
  }
398
460
  : {}),
399
461
  ...(targets.includes("python")
400
462
  ? {
401
463
  python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
402
464
  pythonPackage: pkg.crateName,
465
+ pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
403
466
  }
404
467
  : {}),
405
468
  };
406
469
  });
470
+ const releaseEnabled =
471
+ (options.release ?? true) &&
472
+ (this.bindingMappings.length > 0 ||
473
+ this.packages.some((pkg) => pkg.packageOptions.release || !pkg.packageOptions.private));
407
474
  this.workspaceMapping = {
408
475
  root,
409
476
  crates: this.packages.map((pkg) => `${root}/${pkg.packageOptions.directory}`),
410
477
  bindings: this.bindingMappings,
478
+ ...(releaseEnabled ? { releaseWorkflow: options.releaseWorkflowName ?? "rust-release" } : {}),
411
479
  };
412
- if (isDBXToolsJavaScriptProject()(project)) {
413
- project.dbxToolsConfig.rust = this.workspaceMapping;
480
+ if (dbxToolsProject) {
481
+ dbxToolsProject.dbxToolsConfig.rust = this.workspaceMapping;
414
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
+ );
415
501
  this.pythonPackages = bindings
416
502
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
417
- .map((pkg) => ({
418
- directory: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`.replace(
419
- /^packages\/py\//,
420
- "",
421
- ),
422
- name: pkg.crateName,
423
- module: `${options.pythonModulePrefix ?? options.scope.replaceAll("-", "_")}.${pkg.packageOptions.directory.replaceAll("-", "_")}`,
424
- description: `Python bindings for ${pkg.crateName}`,
425
- private: true,
426
- trustedPublisher: {
427
- workflowName: options.releaseWorkflowName ?? "rust-release",
428
- environment: `pypi-${pkg.crateName}`,
429
- artifacts: `platform-specific wheels for ${releaseTargets(options)
430
- .map((target) => `${target.os}-${target.cpu}`)
431
- .join(", ")}; all architectures publish to this one PyPI project`,
432
- },
433
- }));
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
+ });
434
521
 
435
522
  const existing = new Map(
436
523
  project.subprojects.map((child) => [relative(project.outdir, child.outdir), child]),
@@ -448,12 +535,12 @@ export class DBXToolsRustWorkspace {
448
535
  : new DBXToolsTypeScriptProject({
449
536
  parent: project,
450
537
  outdir: memberPath,
451
- name: `@${string.toSlug(options.scope)}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
538
+ name: `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
452
539
  tags: ["node"],
453
540
  });
454
541
  node.package.addField(
455
542
  "name",
456
- `@${string.toSlug(options.scope)}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
543
+ `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
457
544
  );
458
545
  node.package.addField("private", true);
459
546
  node.package.file.addDeletionOverride("publishConfig");
@@ -464,7 +551,7 @@ export class DBXToolsRustWorkspace {
464
551
  "optionalDependencies",
465
552
  Object.fromEntries(
466
553
  nativeTargets.map((target) => [
467
- `@${string.toSlug(options.scope)}/${directory}-${target.node}`,
554
+ `@${scope}/${directory}-${target.node}`,
468
555
  readWorkspaceVersion(project.outdir),
469
556
  ]),
470
557
  ),
@@ -499,7 +586,7 @@ export class DBXToolsRustWorkspace {
499
586
  edition: options.edition ?? "2021",
500
587
  "rust-version": options.rustVersion ?? "1.82",
501
588
  license: options.license ?? "Apache-2.0",
502
- repository: options.repository ?? "",
589
+ repository,
503
590
  },
504
591
  ...(options.workspaceDependencies
505
592
  ? {
@@ -535,11 +622,7 @@ export class DBXToolsRustWorkspace {
535
622
  ]),
536
623
  ].join(" && "),
537
624
  });
538
- if (
539
- (options.release ?? true) &&
540
- (this.bindingMappings.length > 0 ||
541
- this.packages.some((pkg) => pkg.packageOptions.release || !pkg.packageOptions.private))
542
- ) {
625
+ if (releaseEnabled) {
543
626
  this.addReleaseWorkflow(project, options, releaseTargets(options));
544
627
  }
545
628
  }
@@ -550,6 +633,15 @@ export class DBXToolsRustWorkspace {
550
633
  targets: readonly UniFFIReleaseTarget[],
551
634
  ): void {
552
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);
553
645
  const bindings = this.bindingMappings.map((binding) => ({
554
646
  ...binding,
555
647
  node: binding.node ?? "",
@@ -571,7 +663,11 @@ export class DBXToolsRustWorkspace {
571
663
  }));
572
664
  const hasNodeBindings = bindings.some((binding) => binding.node);
573
665
  const hasPythonBindings = bindings.some((binding) => binding.python);
574
- 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
+ }
575
671
  const releaseTask = ".projen/uniffi-release.mjs";
576
672
  if (bindings.length) {
577
673
  new TextFile(project, releaseTask, {
@@ -598,7 +694,7 @@ export class DBXToolsRustWorkspace {
598
694
  '--cpu "${{ matrix.target.cpu }}"',
599
695
  '--libc "${{ matrix.target.libc }}"',
600
696
  '--facade "${{ matrix.target.facade }}"',
601
- '--version "${VERSION#v}"',
697
+ `--version "\${VERSION#${releaseTagPrefix}}"`,
602
698
  `--output "dist/release/${binding.crate}/\${{ matrix.target.node }}"`,
603
699
  "--skip-build",
604
700
  ].join(" \\\n "),
@@ -662,14 +758,18 @@ export class DBXToolsRustWorkspace {
662
758
  ];
663
759
  const buildJob = {
664
760
  name: "${{ matrix.target.node }}",
761
+ needs: ["verify-context"],
665
762
  "runs-on": "${{ matrix.target.runner }}",
666
- env: RUST_CACHE_ENV,
763
+ env: {
764
+ ...RUST_CACHE_ENV,
765
+ SCCACHE_GHA_VERSION: `release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`,
766
+ },
667
767
  strategy: {
668
768
  "fail-fast": false,
669
769
  matrix: { include: targetMatrix },
670
770
  },
671
771
  steps: [
672
- { name: "Checkout", uses: "actions/checkout@v6" },
772
+ ...releaseSourceSteps(),
673
773
  ...(hasNodeBindings
674
774
  ? [
675
775
  {
@@ -682,28 +782,56 @@ export class DBXToolsRustWorkspace {
682
782
  ...(hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
683
783
  {
684
784
  name: "Setup Rust",
685
- uses: "dtolnay/rust-toolchain@stable",
785
+ uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
686
786
  with: { targets: "${{ matrix.target.cargo }}" },
687
787
  },
688
- ...rustCacheSteps("release-${{ matrix.target.cargo }}"),
788
+ ...rustCacheSteps(`release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`),
689
789
  ...(hasNodeBindings
690
790
  ? [
691
791
  {
692
792
  name: "Cache UBRN generator",
793
+ id: "ubrn_cache",
794
+ if: "${{ vars.CACHE_UBRN_TARGET == 'true' }}",
693
795
  uses: "actions/cache@v5",
694
796
  with: {
695
797
  path: "target/ubrn",
696
- key: `ubrn-\${{ runner.os }}-\${{ runner.arch }}-${UBRN_VERSION}`,
798
+ key: `ubrn-\${{ runner.os }}-\${{ runner.arch }}-rust-${ubrnRustVersion}-${UBRN_VERSION}`,
697
799
  },
698
800
  },
699
801
  ]
700
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
+ },
701
821
  {
702
822
  name: "Install Linux native dependencies",
703
823
  if: "${{ matrix.target.os == 'linux' }}",
704
824
  run: "sudo apt-get update && sudo apt-get install --yes libdbus-1-dev pkg-config",
705
825
  },
706
- ...(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
+ : []),
707
835
  ...(hasNodeBindings
708
836
  ? [
709
837
  {
@@ -711,13 +839,21 @@ export class DBXToolsRustWorkspace {
711
839
  env: {
712
840
  CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
713
841
  },
714
- run: "cargo build --manifest-path node_modules/uniffi-bindgen-react-native/crates/ubrn_cli/Cargo.toml",
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
+ ),
715
847
  },
716
848
  ]
717
849
  : []),
718
850
  {
719
851
  name: "Build Rust outputs",
720
- 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
+ ),
721
857
  },
722
858
  ...(bindingCommands.length
723
859
  ? [
@@ -725,15 +861,14 @@ export class DBXToolsRustWorkspace {
725
861
  name: "Package UniFFI outputs",
726
862
  shell: "bash",
727
863
  env: {
728
- VERSION:
729
- "${{ github.event_name == 'push' && github.ref_name || inputs.version }}",
864
+ VERSION: RELEASE_TAG,
730
865
  ...(hasNodeBindings
731
866
  ? {
732
867
  CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
733
868
  }
734
869
  : {}),
735
870
  },
736
- run: bindingCommands.join("\n"),
871
+ run: timedBash("uniffi_packaging", bindingCommands.join("\n")),
737
872
  },
738
873
  ]
739
874
  : []),
@@ -742,18 +877,24 @@ export class DBXToolsRustWorkspace {
742
877
  {
743
878
  name: "Package release binaries",
744
879
  shell: "bash",
745
- run: binaryCommands.join("\n"),
880
+ run: timedBash("binary_packaging", binaryCommands.join("\n")),
746
881
  },
747
882
  ]
748
883
  : []),
749
884
  ...artifactSteps,
885
+ {
886
+ name: "Log sccache statistics",
887
+ if: "${{ always() }}",
888
+ shell: "bash",
889
+ run: '"${SCCACHE_PATH}" --show-stats',
890
+ },
750
891
  ],
751
892
  };
752
893
  const bindingPublishJobs = Object.fromEntries(
753
894
  bindings.map((binding) => [
754
895
  `publish-${binding.crate}`,
755
896
  {
756
- if: "${{ github.event_name == 'push' }}",
897
+ if: "${{ github.event_name == 'repository_dispatch' }}",
757
898
  needs: ["build"],
758
899
  "runs-on": "ubuntu-latest",
759
900
  permissions: {
@@ -820,36 +961,86 @@ export class DBXToolsRustWorkspace {
820
961
  },
821
962
  ]),
822
963
  );
823
- 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
+ ];
824
970
  new YamlFile(project, `.github/workflows/${workflowName}.yml`, {
825
971
  obj: {
826
972
  name: workflowName,
973
+ "run-name": `${workflowName} ${RELEASE_TAG}`,
827
974
  on: {
828
- push: { tags: ["v*"] },
975
+ repository_dispatch: {
976
+ types: [RUST_RELEASE_EVENT],
977
+ },
829
978
  workflow_dispatch: {
830
979
  inputs: {
831
- version: {
832
- 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",
833
987
  type: "string",
834
- default: "0.0.0.dev0",
988
+ required: true,
835
989
  },
836
990
  },
837
991
  },
838
992
  },
839
993
  permissions: { contents: "read" },
840
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
+ },
841
1029
  ...(hasTargetOutputs && targetMatrix.length ? { build: buildJob } : {}),
842
1030
  ...bindingPublishJobs,
843
1031
  ...(publicCrates.length
844
1032
  ? {
845
1033
  "publish-cargo": {
846
- if: "${{ github.event_name == 'push' }}",
847
- ...(hasTargetOutputs ? { needs: ["build"] } : {}),
1034
+ if: "${{ github.event_name == 'repository_dispatch' }}",
1035
+ needs: ["build"],
848
1036
  "runs-on": "ubuntu-latest",
849
1037
  permissions: { contents: "read" },
850
1038
  steps: [
851
- { name: "Checkout", uses: "actions/checkout@v6" },
852
- { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
1039
+ ...releaseSourceSteps(),
1040
+ {
1041
+ name: "Setup Rust",
1042
+ uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
1043
+ },
853
1044
  {
854
1045
  name: "Publish public crates",
855
1046
  env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
@@ -864,41 +1055,112 @@ export class DBXToolsRustWorkspace {
864
1055
  },
865
1056
  }
866
1057
  : {}),
867
- "publish-local": {
868
- if: "${{ github.event_name == 'push' && vars.LOCAL_REPOSITORIES == 'true' }}",
869
- "runs-on": ["self-hosted"],
870
- permissions: { contents: "read" },
871
- env: RUST_CACHE_ENV,
872
- steps: [
873
- { name: "Checkout", uses: "actions/checkout@v6" },
874
- {
875
- name: "Setup Bun",
876
- uses: "oven-sh/setup-bun@v2",
877
- with: { "bun-version": "1.3.14" },
878
- },
879
- { name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
880
- { name: "Setup Rust", uses: "dtolnay/rust-toolchain@stable" },
881
- ...rustCacheSteps("local-${{ runner.os }}-${{ runner.arch }}"),
882
- { name: "Install", run: "bun install" },
883
- {
884
- name: "Build and publish host-native packages",
885
- run: [
886
- 'VERSION="${GITHUB_REF_NAME#v}"',
887
- '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 }}"',
888
- ].join("\n"),
889
- env: {
890
- NODE_AUTH_TOKEN: "${{ secrets.LOCAL_NPM_TOKEN }}",
891
- UV_PUBLISH_USERNAME: "${{ secrets.LOCAL_PYPI_USERNAME }}",
892
- UV_PUBLISH_PASSWORD: "${{ secrets.LOCAL_PYPI_PASSWORD }}",
893
- 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
+ ],
894
1128
  },
895
- },
896
- ],
897
- },
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
+ : {}),
898
1160
  ...(releaseBinaries.length
899
1161
  ? {
900
1162
  "publish-github-release": {
901
- if: "${{ github.event_name == 'push' }}",
1163
+ if: "${{ github.event_name == 'repository_dispatch' }}",
902
1164
  needs: ["build"],
903
1165
  "runs-on": "ubuntu-latest",
904
1166
  permissions: { contents: "write" },
@@ -918,12 +1180,40 @@ export class DBXToolsRustWorkspace {
918
1180
  with: {
919
1181
  files: "dist/rust-release/*",
920
1182
  "generate-release-notes": true,
1183
+ tag_name: RELEASE_TAG,
1184
+ target_commitish: RELEASE_SHA,
921
1185
  },
922
1186
  },
923
1187
  ],
924
1188
  },
925
1189
  }
926
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
+ },
927
1217
  },
928
1218
  },
929
1219
  });