@dbx-tools/projen 0.6.158 → 0.6.161

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
@@ -115,18 +115,23 @@ Cargo/sccache once,
115
115
  builds the Rust workspace once, then packages every discovered output from that
116
116
  shared build. Bun and the workspace install are present only when a Node binding
117
117
  needs TypeScript generation; uv is present only when a Python wheel is needed.
118
+ A new dispatch cancels queued and in-progress release and docs workflows before
119
+ starting, and each workflow's concurrency group also cancels its previous run.
118
120
  A binary-only workspace therefore installs neither. `rustVersion` remains the
119
121
  MSRV recorded in package manifests, while `releaseRustVersion` independently
120
122
  defaults release compilation to `stable`. UBRN uses that same release toolchain
121
123
  unless `ubrnRustVersion` explicitly selects another one.
122
124
 
123
- The Node generator's Rust CLI can be cached under one target directory keyed by
124
- its pinned UBRN version, Rust version, runner OS, and runner architecture. Set
125
- the repository variable `CACHE_UBRN_TARGET=true` to enable that archive while
126
- comparing its transfer cost with the default sccache-only path. Cargo registry
127
- caches and the `SCCACHE_GHA_VERSION` namespace stay stable per target/toolchain
128
- across version tags. Cache keys, restore results, sccache statistics, and phase
129
- timings are written to each build log. Python generation executes the already-built
125
+ The final host UBRN executable is cached by pinned UBRN version, Rust toolchain,
126
+ runner OS, and runner architecture. A hit skips both the workspace `bun install`
127
+ and the UBRN Cargo build. Release packaging passes the cached executable directly
128
+ to the Node binding generator and keeps the package barrel copied from source,
129
+ so it does not need the installed projen dependency graph. A miss saves the
130
+ validated executable immediately, before workspace build and packaging can fail.
131
+ Cargo registry caches
132
+ and the `SCCACHE_GHA_VERSION` namespace stay stable per target/toolchain across
133
+ version tags. Cache keys, restore results, sccache statistics, and phase timings
134
+ are written to each build log. Python generation executes the already-built
130
135
  `target/<triple>/release/uniffi-bindgen` directly. Artifact packaging therefore
131
136
  does no Rust compilation after the main workspace build.
132
137
 
package/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^1.7.0",
29
- "@dbx-tools/core": "0.6.158",
30
- "@dbx-tools/path": "0.6.158",
31
- "@dbx-tools/shared-core": "0.6.158",
29
+ "@dbx-tools/core": "0.6.161",
30
+ "@dbx-tools/path": "0.6.161",
31
+ "@dbx-tools/shared-core": "0.6.161",
32
32
  "commander": "^15.0.0",
33
33
  "concurrently": "^10.0.3",
34
34
  "constructs": "^10.6.0",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "main": "index.ts",
50
50
  "license": "Apache-2.0",
51
- "version": "0.6.158",
51
+ "version": "0.6.161",
52
52
  "types": "index.ts",
53
53
  "type": "module",
54
54
  "exports": {
package/src/project-py.ts CHANGED
@@ -429,7 +429,11 @@ export class DBXToolsPythonWorkspace extends Component {
429
429
  if (!project.github) return;
430
430
  const publications = this.publications(options);
431
431
  if (publications.length === 0) return;
432
- const workflow = new GithubWorkflow(project.github, options.workflowName ?? "python-release");
432
+ const workflowName = options.workflowName ?? "python-release";
433
+ const workflow = new GithubWorkflow(project.github, workflowName, {
434
+ limitConcurrency: true,
435
+ concurrencyOptions: { group: workflowName, cancelInProgress: true },
436
+ });
433
437
  workflow.file?.addOverride("permissions", {
434
438
  contents: "read",
435
439
  ...(options.upstreamWorkflow ? { actions: "read" } : {}),
package/src/project-rs.ts CHANGED
@@ -641,6 +641,10 @@ export class DBXToolsRustWorkspace {
641
641
  ubrnRustVersion === releaseRustVersion
642
642
  ? ""
643
643
  : `rustup toolchain install ${ubrnRustVersion} --profile minimal\n`;
644
+ const ubrnExecutable =
645
+ "${{ github.workspace }}/.cache/ubrn/uniffi-bindgen-react-native${{ matrix.target.os == 'win32' && '.exe' || '' }}";
646
+ const builtUbrnExecutable =
647
+ "target/ubrn/debug/uniffi-bindgen-react-native${{ matrix.target.os == 'win32' && '.exe' || '' }}";
644
648
  const releaseBranch = projectReleaseBranch(project);
645
649
  const bindings = this.bindingMappings.map((binding) => ({
646
650
  ...binding,
@@ -685,7 +689,7 @@ export class DBXToolsRustWorkspace {
685
689
  `--node-package "${binding.nodePackage}"`,
686
690
  `--python-package "${binding.pythonPackage}"`,
687
691
  ...(binding.node
688
- ? ['--node-generator "node_modules/@dbx-tools/projen/tasks/uniffi.ts"']
692
+ ? ['--node-generator "projen/tasks/uniffi.ts"', '--ubrn "$UBRN_EXECUTABLE"']
689
693
  : []),
690
694
  '--cargo-target "${{ matrix.target.cargo }}"',
691
695
  '--node-triple "${{ matrix.target.node }}"',
@@ -789,13 +793,12 @@ export class DBXToolsRustWorkspace {
789
793
  ...(hasNodeBindings
790
794
  ? [
791
795
  {
792
- name: "Cache UBRN generator",
796
+ name: "Restore UBRN executable",
793
797
  id: "ubrn_cache",
794
- if: "${{ vars.CACHE_UBRN_TARGET == 'true' }}",
795
- uses: "actions/cache@v5",
798
+ uses: "actions/cache/restore@v5",
796
799
  with: {
797
- path: "target/ubrn",
798
- key: `ubrn-\${{ runner.os }}-\${{ runner.arch }}-rust-${ubrnRustVersion}-${UBRN_VERSION}`,
800
+ path: ".cache/ubrn",
801
+ key: `ubrn-executable-\${{ runner.os }}-\${{ runner.arch }}-rust-${ubrnRustVersion}-${UBRN_VERSION}`,
799
802
  },
800
803
  },
801
804
  ]
@@ -811,9 +814,8 @@ export class DBXToolsRustWorkspace {
811
814
  `echo "ubrn_rust_toolchain=${ubrnRustVersion}"`,
812
815
  ...(hasNodeBindings
813
816
  ? [
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
+ `echo "ubrn_executable_cache_key=ubrn-executable-\${{ runner.os }}-\${{ runner.arch }}-rust-${ubrnRustVersion}-${UBRN_VERSION}"`,
818
+ 'echo "ubrn_executable_cache_hit=${{ steps.ubrn_cache.outputs.cache-hit }}"',
817
819
  ]
818
820
  : []),
819
821
  ].join("\n"),
@@ -827,6 +829,7 @@ export class DBXToolsRustWorkspace {
827
829
  ? [
828
830
  {
829
831
  name: "Install Node tooling",
832
+ if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
830
833
  shell: "bash",
831
834
  run: timedBash("node_tooling", "bun install"),
832
835
  },
@@ -836,15 +839,37 @@ export class DBXToolsRustWorkspace {
836
839
  ? [
837
840
  {
838
841
  name: "Prepare UBRN generator",
842
+ if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
839
843
  env: {
840
844
  CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
845
+ UBRN_EXECUTABLE: ubrnExecutable,
841
846
  },
842
847
  shell: "bash",
843
848
  run: timedBash(
844
849
  "ubrn_generator",
845
- `${ubrnToolchainInstall}cargo +${ubrnRustVersion} build --manifest-path node_modules/uniffi-bindgen-react-native/crates/ubrn_cli/Cargo.toml`,
850
+ [
851
+ `${ubrnToolchainInstall}cargo +${ubrnRustVersion} build --manifest-path node_modules/uniffi-bindgen-react-native/crates/ubrn_cli/Cargo.toml`,
852
+ 'mkdir -p "$(dirname "$UBRN_EXECUTABLE")"',
853
+ `cp "${builtUbrnExecutable}" "$UBRN_EXECUTABLE"`,
854
+ 'chmod +x "$UBRN_EXECUTABLE"',
855
+ ].join("\n"),
846
856
  ),
847
857
  },
858
+ {
859
+ name: "Verify UBRN executable",
860
+ env: { UBRN_EXECUTABLE: ubrnExecutable },
861
+ shell: "bash",
862
+ run: 'test -f "$UBRN_EXECUTABLE"',
863
+ },
864
+ {
865
+ name: "Save UBRN executable",
866
+ if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
867
+ uses: "actions/cache/save@v5",
868
+ with: {
869
+ path: ".cache/ubrn",
870
+ key: "${{ steps.ubrn_cache.outputs.cache-primary-key }}",
871
+ },
872
+ },
848
873
  ]
849
874
  : []),
850
875
  {
@@ -864,7 +889,7 @@ export class DBXToolsRustWorkspace {
864
889
  VERSION: RELEASE_TAG,
865
890
  ...(hasNodeBindings
866
891
  ? {
867
- CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
892
+ UBRN_EXECUTABLE: ubrnExecutable,
868
893
  }
869
894
  : {}),
870
895
  },
@@ -990,6 +1015,10 @@ export class DBXToolsRustWorkspace {
990
1015
  },
991
1016
  },
992
1017
  },
1018
+ concurrency: {
1019
+ group: workflowName,
1020
+ "cancel-in-progress": true,
1021
+ },
993
1022
  permissions: { contents: "read" },
994
1023
  jobs: {
995
1024
  "verify-context": {
package/src/release.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * the project has a GitHub component - authors the tag-driven npm publish
5
5
  * workflow that the pushed tag triggers.
6
6
  */
7
- import { rmSync } from "node:fs";
7
+ import { existsSync, rmSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
9
  import { Component, YamlFile } from "projen";
10
10
  import { GithubWorkflow } from "projen/lib/github";
@@ -196,9 +196,23 @@ export class DBXToolsRelease extends Component {
196
196
  object.isRecord(rust) && typeof rust.releaseWorkflow === "string"
197
197
  ? RUST_RELEASE_EVENT
198
198
  : DOWNSTREAM_RELEASE_EVENT;
199
- project
200
- .tryFindObjectFile(".github/workflows/release-dispatch.yml")
201
- ?.addOverride("jobs.dispatch.steps.1.env.RELEASE_EVENT", releaseEvent);
199
+ const dispatcher = project.tryFindObjectFile(".github/workflows/release-dispatch.yml");
200
+ dispatcher?.addOverride("jobs.dispatch.steps.1.env.RELEASE_EVENT", releaseEvent);
201
+ const releaseWorkflows = [
202
+ ...(object.isRecord(rust) && typeof rust.releaseWorkflow === "string"
203
+ ? [rust.releaseWorkflow]
204
+ : []),
205
+ ...(typeof project.dbxToolsConfig.pythonReleaseWorkflow === "string"
206
+ ? [project.dbxToolsConfig.pythonReleaseWorkflow]
207
+ : []),
208
+ ...(this.workflowName ? [this.workflowName] : []),
209
+ ...this.standaloneReleases.map(({ name }) => name),
210
+ ...(existsSync(resolve(project.outdir, ".github/workflows/docs.yml")) ? ["docs"] : []),
211
+ ];
212
+ dispatcher?.addOverride(
213
+ "jobs.dispatch.steps.1.env.RELEASE_WORKFLOWS",
214
+ [...new Set(releaseWorkflows)].join(","),
215
+ );
202
216
  // Release the standalone projects in the SAME run, at the same version. They
203
217
  // are not workspace members, so nothing else would ever bring them along.
204
218
  const siblingArgs = this.standaloneReleases
@@ -233,7 +247,11 @@ export class DBXToolsRelease extends Component {
233
247
  obj: {
234
248
  name: "release-dispatch",
235
249
  on: { push: { tags: [`${this.tagPrefix}*`] } },
236
- permissions: { contents: "write" },
250
+ concurrency: {
251
+ group: "release-dispatch",
252
+ "cancel-in-progress": true,
253
+ },
254
+ permissions: { actions: "write", contents: "write" },
237
255
  jobs: {
238
256
  dispatch: {
239
257
  "runs-on": "ubuntu-latest",
@@ -250,10 +268,19 @@ export class DBXToolsRelease extends Component {
250
268
  GH_TOKEN: "${{ github.token }}",
251
269
  RELEASE_TAG: "${{ github.ref_name }}",
252
270
  RELEASE_EVENT: DOWNSTREAM_RELEASE_EVENT,
271
+ RELEASE_WORKFLOWS: "",
253
272
  },
254
273
  run: [
255
274
  `case "$RELEASE_TAG" in ${this.tagPrefix}*) ;; *) exit 1 ;; esac`,
256
275
  'EXPECTED_SHA="$(git rev-parse "$RELEASE_TAG^{commit}")"',
276
+ 'IFS="," read -r -a workflows <<< "$RELEASE_WORKFLOWS"',
277
+ 'for workflow in "${workflows[@]}"; do',
278
+ " for status in in_progress queued requested waiting pending action_required; do",
279
+ " while IFS= read -r run_id; do",
280
+ ' if [ -n "$run_id" ]; then gh run cancel "$run_id"; fi',
281
+ ' done < <(gh run list --workflow "$workflow.yml" --status "$status" --limit 100 --json databaseId --jq \'.[].databaseId\')',
282
+ " done",
283
+ "done",
257
284
  [
258
285
  'gh api --method POST "repos/$GITHUB_REPOSITORY/dispatches"',
259
286
  '--raw-field event_type="$RELEASE_EVENT"',
@@ -277,11 +304,10 @@ export class DBXToolsRelease extends Component {
277
304
  const setupSteps = publishSetupSteps();
278
305
  const branchDispatch = upstreamWorkflow === undefined;
279
306
  const workflow = new GithubWorkflow(project.github!, name, {
280
- // Serialize publishes so two tags landing together cannot race to the
281
- // registry, but never cancel a run already in flight: a half-published
282
- // release is worse than a queued one.
307
+ // A newer release supersedes an older run even when publication has
308
+ // started; the dispatcher also cancels active ecosystem runs immediately.
283
309
  limitConcurrency: true,
284
- concurrencyOptions: { group: name, cancelInProgress: false },
310
+ concurrencyOptions: { group: name, cancelInProgress: true },
285
311
  });
286
312
  // Read-only floor for any job that does not declare its own permissions;
287
313
  // the publish job below overrides it with the `id-token` it needs.
@@ -26,6 +26,7 @@ const parsed = parseArgs({
26
26
  python: { type: "string" },
27
27
  "node-package": { type: "string" },
28
28
  "node-generator": { type: "string" },
29
+ ubrn: { type: "string" },
29
30
  "python-package": { type: "string" },
30
31
  "cargo-target": { type: "string" },
31
32
  "node-triple": { type: "string" },
@@ -47,9 +48,21 @@ const required = (name) => {
47
48
  };
48
49
 
49
50
  const root = resolve(parsed.values.root ?? process.cwd());
51
+ const commandInvocation = (command, args) => {
52
+ if (process.platform !== "win32" || command !== "npm") return { command, args };
53
+ const npmCli = resolve(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
54
+ if (!existsSync(npmCli)) throw new Error(`Missing npm CLI ${npmCli}`);
55
+ return { command: process.execPath, args: [npmCli, ...args] };
56
+ };
50
57
  const run = (command, args, cwd = root) => {
51
- const result = spawnSync(command, args, { cwd, stdio: "inherit" });
52
- if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
58
+ const invocation = commandInvocation(command, args);
59
+ const result = spawnSync(invocation.command, invocation.args, { cwd, stdio: "inherit" });
60
+ if (result.error) {
61
+ throw new Error(`${invocation.command} failed: ${result.error.message}`, {
62
+ cause: result.error,
63
+ });
64
+ }
65
+ if (result.status !== 0) throw new Error(`${invocation.command} exited with ${result.status}`);
53
66
  };
54
67
 
55
68
  const replaceVersion = (source, version) =>
@@ -137,7 +150,10 @@ const packageNode = ({
137
150
  required("cargo-target"),
138
151
  "--node-package-base",
139
152
  `${nodePackage}-`,
153
+ "--ubrn",
154
+ required("ubrn"),
140
155
  "--skip-build",
156
+ "--skip-barrels",
141
157
  ]);
142
158
  mkdirSync(resolve(output, "npm-facade"), { recursive: true });
143
159
  run("npm", ["pack", "--pack-destination", resolve(output, "npm-facade")], facadeDirectory);
package/tasks/uniffi.ts CHANGED
@@ -29,7 +29,9 @@ const { values } = parseArgs({
29
29
  python: { type: "string" },
30
30
  "cargo-target": { type: "string" },
31
31
  "node-package-base": { type: "string" },
32
+ ubrn: { type: "string" },
32
33
  "skip-build": { type: "boolean" },
34
+ "skip-barrels": { type: "boolean" },
33
35
  },
34
36
  });
35
37
  if (!values.crate || (!values.node && !values.python)) {
@@ -109,7 +111,7 @@ if (!existsSync(library)) throw new Error(`Missing compiled UniFFI library: ${li
109
111
  if (values.node) {
110
112
  const nodeSource = resolve(root, values.node, "src");
111
113
  const nodeOutput = mkdtempSync(join(tmpdir(), `${libraryName}-node-`));
112
- run(join(root, "node_modules/.bin/ubrn"), [
114
+ run(values.ubrn ? resolve(values.ubrn) : join(root, "node_modules/.bin/ubrn"), [
113
115
  "generate",
114
116
  "napi",
115
117
  "bindings",
@@ -187,11 +189,13 @@ if (values.node) {
187
189
  }
188
190
  rmSync(resolve(root, values.node, "src/generated"), { recursive: true, force: true });
189
191
  rmSync(nodeOutput, { recursive: true, force: true });
190
- run(process.execPath, [
191
- resolve(dirname(fileURLToPath(import.meta.url)), "barrels.ts"),
192
- "--dir",
193
- resolve(root, values.node),
194
- ]);
192
+ if (!values["skip-barrels"]) {
193
+ run(process.execPath, [
194
+ resolve(dirname(fileURLToPath(import.meta.url)), "barrels.ts"),
195
+ "--dir",
196
+ resolve(root, values.node),
197
+ ]);
198
+ }
195
199
  }
196
200
 
197
201
  if (values.python) {