@dbx-tools/projen 0.9.5 → 0.9.7

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.
Files changed (50) hide show
  1. package/README.md +69 -17
  2. package/index.ts +7 -5
  3. package/package.json +9 -6
  4. package/src/_rust-project.ts +356 -0
  5. package/src/_rust-release-workflow.ts +954 -0
  6. package/src/_rust-release.ts +605 -0
  7. package/src/_task-command.ts +89 -0
  8. package/src/barrels.ts +12 -11
  9. package/src/bun-app.ts +96 -85
  10. package/src/bun-workflow.ts +60 -52
  11. package/src/engine-root.ts +5 -5
  12. package/src/openapi.ts +16 -9
  13. package/src/packages.ts +9 -9
  14. package/src/pnpm-workspace.ts +1 -1
  15. package/src/project-js.ts +19 -25
  16. package/src/project-py.ts +28 -23
  17. package/src/project-rs.ts +100 -1098
  18. package/src/projen-version.ts +9 -0
  19. package/src/publish.ts +3 -4
  20. package/src/release-catalog.ts +1 -1
  21. package/src/release.ts +212 -177
  22. package/src/scaffold.ts +1 -1
  23. package/src/uniffi.ts +7 -0
  24. package/src/watch.ts +2 -2
  25. package/src/workspace-version.ts +20 -15
  26. package/tasks/barrels.ts +4 -4
  27. package/tasks/build-rust-release.ts +68 -0
  28. package/tasks/bump.ts +3 -2
  29. package/tasks/clean.ts +3 -3
  30. package/tasks/local-publish.ts +21 -3
  31. package/tasks/openapi.ts +2 -2
  32. package/tasks/publish-npm.ts +12 -6
  33. package/tasks/publish-python.ts +2 -2
  34. package/tasks/publish-uniffi-local.ts +11 -15
  35. package/tasks/publish.ts +88 -88
  36. package/tasks/python-registry.ts +1 -1
  37. package/tasks/release-bootstrap.ts +2 -2
  38. package/tasks/release-plan.ts +1 -1
  39. package/tasks/release-please.ts +1 -1
  40. package/tasks/release-pr.ts +60 -115
  41. package/tasks/release-request.ts +8 -29
  42. package/tasks/release-summary-units.ts +2 -9
  43. package/tasks/release-summary.ts +35 -24
  44. package/tasks/release-version.ts +44 -0
  45. package/tasks/rust-release.d.ts +41 -0
  46. package/tasks/rust-release.mjs +1717 -0
  47. package/tasks/sync.ts +3 -3
  48. package/tasks/uniffi-python.js +1 -1
  49. package/tasks/uniffi.ts +7 -1
  50. package/tasks/version-check.ts +6 -5
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Co-tested Projen release used by the engine and every generated project.
3
+ *
4
+ * The engine exposes Projen as a peer dependency so consumers and generated
5
+ * projects share one runtime instance. Keep the peer range and generated
6
+ * development dependency on this single value; duplicate Projen instances can
7
+ * break its remaining `instanceof`-based file detection.
8
+ */
9
+ export const PROJEN_VERSION = "^0.103.27";
package/src/publish.ts CHANGED
@@ -148,10 +148,9 @@ export function applyCompiledPublish(pkg: javascript.NodeProject): void {
148
148
  const config = publishConfig(pkg);
149
149
  if (config) pkg.package.addField("publishConfig", config);
150
150
 
151
- // The release workflow publishes straight after `bun install`, with no build
152
- // in between, so the compiled output has to be produced by the pack itself
153
- // rather than assumed present. This also covers a bare `pnpm pack` and the
154
- // bump task's local-registry publish.
151
+ // The workspace publisher compiles every selected package once and disables
152
+ // lifecycle scripts while packing. Keep `prepack` for a standalone package
153
+ // publish that did not pass through that workspace-level build.
155
154
  if (!pkg.tasks.tryFind("prepack")) {
156
155
  pkg.addTask("prepack", { description: "Compile before packing the published tarball" });
157
156
  }
@@ -9,8 +9,8 @@
9
9
  * @module
10
10
  */
11
11
 
12
- import { createHash } from "node:crypto";
13
12
  import { execFileSync } from "node:child_process";
13
+ import { createHash } from "node:crypto";
14
14
  import { existsSync, readFileSync, readdirSync } from "node:fs";
15
15
  import { basename, join, relative, resolve } from "node:path";
16
16
  import { Component, JsonFile, type Project } from "projen";
package/src/release.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /** Unified default-branch release workflow generation. */
2
+ import { stringUtils } from "@dbx-tools/shared-core";
2
3
  import { Component } from "projen";
3
4
  import { GithubWorkflow } from "projen/lib/github";
4
5
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
@@ -188,6 +189,12 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
188
189
  "fetch-depth": 2,
189
190
  },
190
191
  },
192
+ {
193
+ name: "Setup Bun",
194
+ uses: "oven-sh/setup-bun@v2",
195
+ with: { "bun-version": BUN_VERSION },
196
+ },
197
+ { name: "Install release validation dependencies", run: "bun install --frozen-lockfile" },
191
198
  {
192
199
  name: "Verify release context",
193
200
  id: "release",
@@ -199,54 +206,52 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
199
206
  "${{ github.event_name == 'workflow_dispatch' && inputs.expected_sha || '' }}",
200
207
  DRY_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}",
201
208
  },
202
- run: [
203
- 'if [ "$GITHUB_EVENT_NAME" = "push" ]; then',
204
- ' test "$GITHUB_REF_TYPE" = "branch"',
205
- ` test "$GITHUB_REF_NAME" = "${releaseBranch}"`,
206
- ' RELEASE_SHA="$GITHUB_SHA"',
207
- ' test "$(git rev-parse HEAD)" = "$RELEASE_SHA"',
208
- " RELEASE_VERSION=\"$(tr -d '\\r\\n' < VERSION)\"",
209
- ' [[ "$RELEASE_VERSION" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]',
210
- ' PREVIOUS_VERSION="$(git show "$RELEASE_SHA^:VERSION" | tr -d \'\\r\\n\')"',
211
- ' test "$PREVIOUS_VERSION" != "$RELEASE_VERSION"',
212
- ` RELEASE_TAG="${tagPrefix}$RELEASE_VERSION"`,
213
- ' if git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_TAG" >/dev/null 2>&1; then',
214
- ' git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"',
215
- ' test "$(git cat-file -t "$RELEASE_TAG")" = "tag"',
216
- ' test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$RELEASE_SHA"',
217
- " else",
218
- " git fetch --force --tags origin",
219
- ' LATEST_VERSION=""',
220
- ` for TAG in $(git tag --list '${tagPrefix}*' '*-${tagPrefix}*'); do`,
221
- ` if [[ "$TAG" == ${tagPrefix}* ]]; then CANDIDATE="\${TAG#${tagPrefix}}"; else CANDIDATE="\${TAG##*-${tagPrefix}}"; fi`,
222
- ' if [[ "$CANDIDATE" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then',
223
- ' if [ -z "$LATEST_VERSION" ] || node -e \'const a=process.argv[1].split(".").map(Number); const b=process.argv[2].split(".").map(Number); process.exit(b[0]>a[0] || b[0]===a[0] && (b[1]>a[1] || b[1]===a[1] && b[2]>a[2]) ? 0 : 1)\' "$LATEST_VERSION" "$CANDIDATE"; then LATEST_VERSION="$CANDIDATE"; fi',
224
- " fi",
225
- " done",
226
- ' if [ -n "$LATEST_VERSION" ]; then',
227
- ' node -e \'const a=process.argv[1].split(".").map(Number); const b=process.argv[2].split(".").map(Number); if (!(b[0]>a[0] || b[0]===a[0] && (b[1]>a[1] || b[1]===a[1] && b[2]>a[2]))) process.exit(1)\' "$LATEST_VERSION" "$RELEASE_VERSION"',
228
- " fi",
229
- ' git config user.name "github-actions[bot]"',
230
- ' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"',
231
- ' git tag -a "$RELEASE_TAG" "$RELEASE_SHA" -m "$RELEASE_TAG"',
232
- ' git push origin "refs/tags/$RELEASE_TAG"',
233
- " fi",
234
- "else",
235
- ` case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac`,
236
- ' git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"',
237
- ' test "$(git cat-file -t "$RELEASE_TAG")" = "tag"',
238
- ' RELEASE_SHA="$(git rev-parse "$RELEASE_TAG^{commit}")"',
239
- ' test "$(git rev-parse HEAD)" = "$RELEASE_SHA"',
240
- ' test "$RELEASE_SHA" = "$EXPECTED_SHA"',
241
- ' if [ -n "${{ inputs.source_run_id }}" ]; then',
242
- ' case "${{ inputs.stage }}" in node|python) ;; *) exit 1 ;; esac',
243
- ' case "${{ inputs.source_run_id }}" in *[!0-9]*|"") exit 1 ;; esac',
244
- " fi",
245
- "fi",
246
- 'echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"',
247
- 'echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"',
248
- `echo "release_version=\${RELEASE_TAG#${tagPrefix}}" >> "$GITHUB_OUTPUT"`,
249
- ].join("\n"),
209
+ // prettier-ignore
210
+ run: stringUtils.dedent(
211
+ // ============================================================================
212
+ /*bash*/`
213
+ if [ "$GITHUB_EVENT_NAME" = "push" ]; then
214
+ test "$GITHUB_REF_TYPE" = "branch"
215
+ test "$GITHUB_REF_NAME" = "${releaseBranch}"
216
+ RELEASE_SHA="$GITHUB_SHA"
217
+ test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
218
+ RELEASE_VERSION="$(tr -d '\\r\\n' < VERSION)"
219
+ bun node_modules/@dbx-tools/projen/tasks/release-version.ts --version "$RELEASE_VERSION"
220
+ PREVIOUS_VERSION="$(git show "$RELEASE_SHA^:VERSION" | tr -d '\\r\\n')"
221
+ test "$PREVIOUS_VERSION" != "$RELEASE_VERSION"
222
+ RELEASE_TAG="${tagPrefix}$RELEASE_VERSION"
223
+ if git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_TAG" >/dev/null 2>&1; then
224
+ git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
225
+ test "$(git cat-file -t "$RELEASE_TAG")" = "tag"
226
+ test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$RELEASE_SHA"
227
+ else
228
+ git fetch --force --tags origin
229
+ bun node_modules/@dbx-tools/projen/tasks/release-version.ts --version "$RELEASE_VERSION" --prefix ${JSON.stringify(tagPrefix)} --assert-next
230
+ git config user.name "github-actions[bot]"
231
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
232
+ git tag -a "$RELEASE_TAG" "$RELEASE_SHA" -m "$RELEASE_TAG"
233
+ git push origin "refs/tags/$RELEASE_TAG"
234
+ fi
235
+ else
236
+ case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac
237
+ RELEASE_VERSION="\${RELEASE_TAG#${tagPrefix}}"
238
+ bun node_modules/@dbx-tools/projen/tasks/release-version.ts --version "$RELEASE_VERSION"
239
+ git fetch --force origin "+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
240
+ test "$(git cat-file -t "$RELEASE_TAG")" = "tag"
241
+ RELEASE_SHA="$(git rev-parse "$RELEASE_TAG^{commit}")"
242
+ test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
243
+ test "$RELEASE_SHA" = "$EXPECTED_SHA"
244
+ if [ -n "\${{ inputs.source_run_id }}" ]; then
245
+ case "\${{ inputs.stage }}" in node|python) ;; *) exit 1 ;; esac
246
+ case "\${{ inputs.source_run_id }}" in *[!0-9]*|"") exit 1 ;; esac
247
+ fi
248
+ fi
249
+ echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
250
+ echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
251
+ echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
252
+ `
253
+ // ============================================================================
254
+ ),
250
255
  },
251
256
  {
252
257
  name: "Verify source artifact run",
@@ -257,15 +262,20 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
257
262
  SOURCE_RUN_ID: "${{ inputs.source_run_id }}",
258
263
  },
259
264
  with: {
260
- script: [
261
- "const run = await github.rest.actions.getWorkflowRun({",
262
- " owner: context.repo.owner,",
263
- " repo: context.repo.repo,",
264
- " run_id: Number(process.env.SOURCE_RUN_ID),",
265
- "});",
266
- 'if (run.data.path !== ".github/workflows/release.yml") core.setFailed("Source run is not release.yml");',
267
- 'if (run.data.head_sha !== process.env.EXPECTED_SHA) core.setFailed("Source run commit does not match the release tag");',
268
- ].join("\n"),
265
+ // prettier-ignore
266
+ script: stringUtils.dedent(
267
+ // ============================================================================
268
+ /*js*/`
269
+ const run = await github.rest.actions.getWorkflowRun({
270
+ owner: context.repo.owner,
271
+ repo: context.repo.repo,
272
+ run_id: Number(process.env.SOURCE_RUN_ID),
273
+ });
274
+ if (run.data.path !== ".github/workflows/release.yml") core.setFailed("Source run is not release.yml");
275
+ if (run.data.head_sha !== process.env.EXPECTED_SHA) core.setFailed("Source run commit does not match the release tag");
276
+ `
277
+ // ============================================================================
278
+ ),
269
279
  },
270
280
  },
271
281
  ],
@@ -423,44 +433,49 @@ function independentReleasePleaseJob(project: DBXToolsJavaScriptProject, branch:
423
433
  RELEASE_PRS: "${{ steps.release.outputs.prs }}",
424
434
  },
425
435
  shell: "bash",
426
- run: [
427
- 'BRANCH="$(jq -r \'.[0].headBranchName\' <<<"$RELEASE_PRS")"',
428
- 'test -n "$BRANCH" && test "$BRANCH" != "null"',
429
- 'git fetch origin "$BRANCH"',
430
- 'git switch --force-create "$BRANCH" "origin/$BRANCH"',
431
- "bun install",
432
- "bunx projen",
433
- "bun node_modules/@dbx-tools/projen/tasks/version-check.ts",
434
- `bun node_modules/@dbx-tools/projen/tasks/release-plan.ts --base-ref origin/${branch}`,
435
- `bun node_modules/@dbx-tools/projen/tasks/release-summary-units.ts --from-ref origin/${branch}`,
436
- 'if ! git diff --quiet || test -n "$(git ls-files --others --exclude-standard)"; then',
437
- ' git config user.name "github-actions[bot]"',
438
- ' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"',
439
- " git add -A",
440
- ' git commit -m "chore: reconcile release metadata"',
441
- ' git push origin "HEAD:$BRANCH"',
442
- "fi",
443
- 'MERGED="false"',
444
- "for ATTEMPT in $(seq 1 15); do",
445
- ' STATE="$(gh pr view "$BRANCH" --json state,mergeable --jq \'[.state,.mergeable] | join(" ")\')"',
446
- ' if [[ "$STATE" == MERGED* ]]; then MERGED="true"; break; fi',
447
- ' if [ "$STATE" = "OPEN CONFLICTING" ]; then',
448
- ' echo "::error::release pull request has merge conflicts"',
449
- " exit 1",
450
- " fi",
451
- ' if [ "$STATE" = "OPEN MERGEABLE" ] && gh pr merge "$BRANCH" --merge; then',
452
- ' MERGED="true"',
453
- " break",
454
- " fi",
455
- ' echo "release pull request is not mergeable yet (attempt $ATTEMPT/15)"',
456
- " sleep 2",
457
- "done",
458
- 'if [ "$MERGED" != "true" ]; then',
459
- ' echo "::error::release pull request did not become mergeable"',
460
- " exit 1",
461
- "fi",
462
- 'gh workflow run release.yml --ref "$RELEASE_BRANCH" -f automatic=true',
463
- ].join("\n"),
436
+ // prettier-ignore
437
+ run: stringUtils.dedent(
438
+ // ============================================================================
439
+ /*bash*/`
440
+ BRANCH="$(jq -r '.[0].headBranchName' <<<"$RELEASE_PRS")"
441
+ test -n "$BRANCH" && test "$BRANCH" != "null"
442
+ git fetch origin "$BRANCH"
443
+ git switch --force-create "$BRANCH" "origin/$BRANCH"
444
+ bun install
445
+ bunx projen
446
+ bun node_modules/@dbx-tools/projen/tasks/version-check.ts
447
+ bun node_modules/@dbx-tools/projen/tasks/release-plan.ts --base-ref origin/${branch}
448
+ bun node_modules/@dbx-tools/projen/tasks/release-summary-units.ts --from-ref origin/${branch}
449
+ if ! git diff --quiet || test -n "$(git ls-files --others --exclude-standard)"; then
450
+ git config user.name "github-actions[bot]"
451
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
452
+ git add -A
453
+ git commit -m "chore: reconcile release metadata"
454
+ git push origin "HEAD:$BRANCH"
455
+ fi
456
+ MERGED="false"
457
+ for ATTEMPT in $(seq 1 15); do
458
+ STATE="$(gh pr view "$BRANCH" --json state,mergeable --jq '[.state,.mergeable] | join(" ")')"
459
+ if [[ "$STATE" == MERGED* ]]; then MERGED="true"; break; fi
460
+ if [ "$STATE" = "OPEN CONFLICTING" ]; then
461
+ echo "::error::release pull request has merge conflicts"
462
+ exit 1
463
+ fi
464
+ if [ "$STATE" = "OPEN MERGEABLE" ] && gh pr merge "$BRANCH" --merge; then
465
+ MERGED="true"
466
+ break
467
+ fi
468
+ echo "release pull request is not mergeable yet (attempt $ATTEMPT/15)"
469
+ sleep 2
470
+ done
471
+ if [ "$MERGED" != "true" ]; then
472
+ echo "::error::release pull request did not become mergeable"
473
+ exit 1
474
+ fi
475
+ gh workflow run release.yml --ref "$RELEASE_BRANCH" -f automatic=true
476
+ `
477
+ // ============================================================================
478
+ ),
464
479
  },
465
480
  ],
466
481
  };
@@ -500,22 +515,27 @@ function configureReleaseRequestWorkflow(
500
515
  name: "Read release request",
501
516
  id: "request",
502
517
  shell: "bash",
503
- run: [
504
- 'MESSAGE="$(git log -1 --format=%B)"',
505
- 'if ! grep -q "^Release-Request: true$" <<<"$MESSAGE"; then',
506
- ' echo "requested=false" >> "$GITHUB_OUTPUT"',
507
- " exit 0",
508
- "fi",
509
- `git fetch origin ${JSON.stringify(baseBranch)}`,
510
- `if git diff --quiet "origin/${baseBranch}...HEAD"; then`,
511
- ' echo "requested=false" >> "$GITHUB_OUTPUT"',
512
- " exit 0",
513
- "fi",
514
- 'echo "requested=true" >> "$GITHUB_OUTPUT"',
515
- 'echo "branch=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"',
516
- 'echo "notes_path=$(sed -n \'s/^Release-Notes-Path: //p\' <<<"$MESSAGE" | tail -1)" >> "$GITHUB_OUTPUT"',
517
- 'echo "title=$(git log -1 --skip=1 --format=%s)" >> "$GITHUB_OUTPUT"',
518
- ].join("\n"),
518
+ // prettier-ignore
519
+ run: stringUtils.dedent(
520
+ // ============================================================================
521
+ /*bash*/`
522
+ MESSAGE="$(git log -1 --format=%B)"
523
+ if ! grep -q "^Release-Request: true$" <<<"$MESSAGE"; then
524
+ echo "requested=false" >> "$GITHUB_OUTPUT"
525
+ exit 0
526
+ fi
527
+ git fetch origin ${JSON.stringify(baseBranch)}
528
+ if git diff --quiet "origin/${baseBranch}...HEAD"; then
529
+ echo "requested=false" >> "$GITHUB_OUTPUT"
530
+ exit 0
531
+ fi
532
+ echo "requested=true" >> "$GITHUB_OUTPUT"
533
+ echo "branch=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
534
+ echo "notes_path=$(sed -n 's/^Release-Notes-Path: //p' <<<"$MESSAGE" | tail -1)" >> "$GITHUB_OUTPUT"
535
+ echo "title=$(git log -1 --skip=1 --format=%s)" >> "$GITHUB_OUTPUT"
536
+ `
537
+ // ============================================================================
538
+ ),
519
539
  },
520
540
  {
521
541
  name: "Create, merge, and release source pull request",
@@ -528,20 +548,25 @@ function configureReleaseRequestWorkflow(
528
548
  NOTES_PATH: "${{ steps.request.outputs.notes_path }}",
529
549
  },
530
550
  shell: "bash",
531
- run: [
532
- 'test -f "$NOTES_PATH"',
533
- 'NOTES="$(cat "$NOTES_PATH")"',
534
- 'BODY="$(printf \'## Release notes\\n\\n%s\\n\' "$NOTES")"',
535
- 'PR="$(gh pr list --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --state open --json number --jq \'.[0].number // empty\')"',
536
- 'if [ -n "$PR" ]; then',
537
- ' gh pr edit "$PR" --title "$TITLE" --body "$BODY"',
538
- "else",
539
- ' gh pr create --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --title "$TITLE" --body "$BODY"',
540
- ' PR="$(gh pr list --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --state open --json number --jq \'.[0].number\')"',
541
- "fi",
542
- 'gh pr merge "$PR" --merge',
543
- 'gh workflow run release.yml --repo "$GITHUB_REPOSITORY" --ref "$BASE_BRANCH" -f automatic=true',
544
- ].join("\n"),
551
+ // prettier-ignore
552
+ run: stringUtils.dedent(
553
+ // ============================================================================
554
+ /*bash*/`
555
+ test -f "$NOTES_PATH"
556
+ NOTES="$(cat "$NOTES_PATH")"
557
+ BODY="$(printf '## Release notes\\n\\n%s\\n' "$NOTES")"
558
+ PR="$(gh pr list --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --state open --json number --jq '.[0].number // empty')"
559
+ if [ -n "$PR" ]; then
560
+ gh pr edit "$PR" --title "$TITLE" --body "$BODY"
561
+ else
562
+ gh pr create --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --title "$TITLE" --body "$BODY"
563
+ PR="$(gh pr list --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --state open --json number --jq '.[0].number')"
564
+ fi
565
+ gh pr merge "$PR" --merge
566
+ gh workflow run release.yml --repo "$GITHUB_REPOSITORY" --ref "$BASE_BRANCH" -f automatic=true
567
+ `
568
+ // ============================================================================
569
+ ),
545
570
  },
546
571
  ],
547
572
  });
@@ -673,20 +698,25 @@ function independentReleaseNotesJob(project: DBXToolsJavaScriptProject): Job {
673
698
  {
674
699
  name: "Remove consumed release notes",
675
700
  shell: "bash",
676
- run: [
677
- `git fetch origin ${JSON.stringify(projectReleaseBranch(project))}`,
678
- `if [ "$(git rev-parse origin/${projectReleaseBranch(project)})" != "$(git rev-parse HEAD)" ]; then`,
679
- ' echo "::warning::main advanced before release-note cleanup; leaving temporary notes for the next release"',
680
- " exit 0",
681
- "fi",
682
- "git rm -r --ignore-unmatch .release-notes",
683
- "if ! git diff --cached --quiet; then",
684
- ' git config user.name "github-actions[bot]"',
685
- ' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"',
686
- ' git commit -m "chore: remove consumed release notes"',
687
- ` git push origin "HEAD:${projectReleaseBranch(project)}"`,
688
- "fi",
689
- ].join("\n"),
701
+ // prettier-ignore
702
+ run: stringUtils.dedent(
703
+ // ============================================================================
704
+ /*bash*/`
705
+ git fetch origin ${JSON.stringify(projectReleaseBranch(project))}
706
+ if [ "$(git rev-parse origin/${projectReleaseBranch(project)})" != "$(git rev-parse HEAD)" ]; then
707
+ echo "::warning::main advanced before release-note cleanup; leaving temporary notes for the next release"
708
+ exit 0
709
+ fi
710
+ git rm -r --ignore-unmatch .release-notes
711
+ if ! git diff --cached --quiet; then
712
+ git config user.name "github-actions[bot]"
713
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
714
+ git commit -m "chore: remove consumed release notes"
715
+ git push origin "HEAD:${projectReleaseBranch(project)}"
716
+ fi
717
+ `
718
+ // ============================================================================
719
+ ),
690
720
  },
691
721
  ],
692
722
  };
@@ -770,44 +800,49 @@ function addIndependentBranchSyncJob(
770
800
  name: `Safely sync ${branch}`,
771
801
  shell: "bash",
772
802
  env: { SOURCE_BRANCH: branch, RELEASE_BRANCH: releaseBranch },
773
- run: [
774
- 'if ! git ls-remote --exit-code --heads origin "refs/heads/$SOURCE_BRANCH" >/dev/null 2>&1; then',
775
- ' echo "source branch $SOURCE_BRANCH no longer exists; nothing to sync"',
776
- " exit 0",
777
- "fi",
778
- 'git fetch origin "refs/heads/$SOURCE_BRANCH:refs/remotes/origin/$SOURCE_BRANCH"',
779
- 'git fetch origin "refs/heads/$RELEASE_BRANCH:refs/remotes/origin/$RELEASE_BRANCH"',
780
- 'if git merge-base --is-ancestor "origin/$SOURCE_BRANCH" "origin/$RELEASE_BRANCH"; then',
781
- ' git push origin "refs/remotes/origin/$RELEASE_BRANCH:refs/heads/$SOURCE_BRANCH"',
782
- 'elif git merge-base --is-ancestor "origin/$RELEASE_BRANCH" "origin/$SOURCE_BRANCH"; then',
783
- ' echo "source branch $SOURCE_BRANCH already contains released main"',
784
- "else",
785
- ' git config user.name "github-actions[bot]"',
786
- ' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"',
787
- ' git switch --force-create "$SOURCE_BRANCH" "origin/$SOURCE_BRANCH"',
788
- ' if git merge --no-ff --no-edit "origin/$RELEASE_BRANCH"; then',
789
- ' git push origin "HEAD:refs/heads/$SOURCE_BRANCH"',
790
- " else",
791
- ' SAFE_GENERATED="true"',
792
- " while IFS= read -r FILE; do",
793
- ' case "$FILE" in',
794
- " .projen/release-units.json|.release-please-manifest.json|.release-units/*/source.json|.release-units/*/version.txt|.release-units/*/CHANGELOG.md|Cargo.lock|*/Cargo.toml|*/package.json|*/pyproject.toml|*/index.ts) ;;",
795
- ' *) SAFE_GENERATED="false" ;;',
796
- " esac",
797
- " done < <(git diff --name-only --diff-filter=U)",
798
- ' if [ "$SAFE_GENERATED" = "true" ] && bunx projen; then',
799
- " git add -A",
800
- ' if [ -z "$(git diff --name-only --diff-filter=U)" ]; then',
801
- " git commit --no-edit",
802
- ' git push origin "HEAD:refs/heads/$SOURCE_BRANCH"',
803
- " exit 0",
804
- " fi",
805
- " fi",
806
- " git merge --abort",
807
- ' echo "::warning::source branch $SOURCE_BRANCH has handwritten or unresolved conflicts with released main; leaving it untouched"',
808
- " fi",
809
- "fi",
810
- ].join("\n"),
803
+ // prettier-ignore
804
+ run: stringUtils.dedent(
805
+ // ============================================================================
806
+ /*bash*/`
807
+ if ! git ls-remote --exit-code --heads origin "refs/heads/$SOURCE_BRANCH" >/dev/null 2>&1; then
808
+ echo "source branch $SOURCE_BRANCH no longer exists; nothing to sync"
809
+ exit 0
810
+ fi
811
+ git fetch origin "refs/heads/$SOURCE_BRANCH:refs/remotes/origin/$SOURCE_BRANCH"
812
+ git fetch origin "refs/heads/$RELEASE_BRANCH:refs/remotes/origin/$RELEASE_BRANCH"
813
+ if git merge-base --is-ancestor "origin/$SOURCE_BRANCH" "origin/$RELEASE_BRANCH"; then
814
+ git push origin "refs/remotes/origin/$RELEASE_BRANCH:refs/heads/$SOURCE_BRANCH"
815
+ elif git merge-base --is-ancestor "origin/$RELEASE_BRANCH" "origin/$SOURCE_BRANCH"; then
816
+ echo "source branch $SOURCE_BRANCH already contains released main"
817
+ else
818
+ git config user.name "github-actions[bot]"
819
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
820
+ git switch --force-create "$SOURCE_BRANCH" "origin/$SOURCE_BRANCH"
821
+ if git merge --no-ff --no-edit "origin/$RELEASE_BRANCH"; then
822
+ git push origin "HEAD:refs/heads/$SOURCE_BRANCH"
823
+ else
824
+ SAFE_GENERATED="true"
825
+ while IFS= read -r FILE; do
826
+ case "$FILE" in
827
+ .projen/release-units.json|.release-please-manifest.json|.release-units/*/source.json|.release-units/*/version.txt|.release-units/*/CHANGELOG.md|Cargo.lock|*/Cargo.toml|*/package.json|*/pyproject.toml|*/index.ts) ;;
828
+ *) SAFE_GENERATED="false" ;;
829
+ esac
830
+ done < <(git diff --name-only --diff-filter=U)
831
+ if [ "$SAFE_GENERATED" = "true" ] && bunx projen; then
832
+ git add -A
833
+ if [ -z "$(git diff --name-only --diff-filter=U)" ]; then
834
+ git commit --no-edit
835
+ git push origin "HEAD:refs/heads/$SOURCE_BRANCH"
836
+ exit 0
837
+ fi
838
+ fi
839
+ git merge --abort
840
+ echo "::warning::source branch $SOURCE_BRANCH has handwritten or unresolved conflicts with released main; leaving it untouched"
841
+ fi
842
+ fi
843
+ `
844
+ // ============================================================================
845
+ ),
811
846
  },
812
847
  ],
813
848
  });
package/src/scaffold.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Runs a projen re-synth, for the `sync` task and its watchers.
3
3
  */
4
4
  import { join } from "node:path";
5
- import { exec } from "@dbx-tools/core";
5
+ import * as exec from "@dbx-tools/core/exec";
6
6
  import { repoRoot } from "./packages.ts";
7
7
 
8
8
  /**
package/src/uniffi.ts CHANGED
@@ -31,6 +31,13 @@ export const makeDefaultedInterfaceParametersOptional = (source: string): string
31
31
  );
32
32
  };
33
33
 
34
+ /** Remove UBRN's obsolete `*Interface` aliases once the `*Like` owner exists. */
35
+ export const removeObsoleteInterfaceAliases = (source: string): string =>
36
+ source.replace(
37
+ /\n\/\*\*\n \* @deprecated Use `\w+Like` instead\.\n \*\/\nexport type \w+Interface = \w+Like;\n/g,
38
+ "\n",
39
+ );
40
+
34
41
  export interface TypeScriptBindingModule {
35
42
  readonly specifier: string;
36
43
  readonly source: string;
package/src/watch.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { isAbsolute, resolve } from "node:path";
19
19
  import { watch as pathWatch } from "@dbx-tools/path";
20
- import { async, log } from "@dbx-tools/shared-core";
20
+ import { asyncUtils, log } from "@dbx-tools/shared-core";
21
21
  import { isGeneratedFile, recordedRoots, repoRoot } from "./packages.ts";
22
22
 
23
23
  const logger = log.logger("projen:watch");
@@ -132,7 +132,7 @@ export function watchLoop(
132
132
  if (closing) return;
133
133
  closing = true;
134
134
  clearTimeout(timer);
135
- await Promise.race([watcher.close().catch(() => {}), async.sleep(CLOSE_GRACE_MS)]);
135
+ await Promise.race([watcher.close().catch(() => {}), asyncUtils.sleep(CLOSE_GRACE_MS)]);
136
136
  process.exit(code);
137
137
  }
138
138
 
@@ -17,7 +17,8 @@
17
17
  */
18
18
  import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
19
19
  import { join } from "node:path";
20
- import { exec } from "@dbx-tools/core";
20
+ import { parse as parseVersion } from "semver";
21
+ import { captureTaskCommand } from "./_task-command.ts";
21
22
 
22
23
  /** Name of the repo-root file holding the workspace version. */
23
24
  export const VERSION_FILE = "VERSION";
@@ -33,10 +34,20 @@ export type Semver = [number, number, number];
33
34
  /** Supported semantic release increments. */
34
35
  export type VersionLevel = "patch" | "minor" | "major";
35
36
 
36
- /** Parse `x.y.z` (ignoring any leading `v`/prefix), or `undefined` when it does not match. */
37
+ /** Parse an exact stable `x.y.z`, or `undefined` for prefixes, prereleases, or embedded versions. */
37
38
  export function parseSemver(raw: string): Semver | undefined {
38
- const m = /(\d+)\.(\d+)\.(\d+)/.exec(raw.trim());
39
- return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : undefined;
39
+ const value = raw.trim();
40
+ if (!SEMVER.test(value)) return undefined;
41
+ const parsed = parseVersion(value);
42
+ if (
43
+ !parsed ||
44
+ parsed.version !== value ||
45
+ parsed.prerelease.length > 0 ||
46
+ parsed.build.length > 0
47
+ ) {
48
+ return undefined;
49
+ }
50
+ return [parsed.major, parsed.minor, parsed.patch];
40
51
  }
41
52
 
42
53
  /** Ordering comparator: negative when `a < b`, positive when `a > b`, zero when equal. */
@@ -66,7 +77,7 @@ export function readWorkspaceVersion(root: string): string {
66
77
  const path = versionPath(root);
67
78
  if (!existsSync(path)) return DEFAULT_VERSION;
68
79
  const raw = readFileSync(path, "utf8").trim();
69
- if (!SEMVER.test(raw)) {
80
+ if (!parseSemver(raw)) {
70
81
  throw new Error(`${VERSION_FILE} must contain an x.y.z version, got ${JSON.stringify(raw)}`);
71
82
  }
72
83
  return raw;
@@ -74,7 +85,7 @@ export function readWorkspaceVersion(root: string): string {
74
85
 
75
86
  /** Write the workspace version to `<root>/VERSION`. Only `bump` and bootstrap call this. */
76
87
  export function writeWorkspaceVersion(root: string, version: string): void {
77
- if (!SEMVER.test(version)) {
88
+ if (!parseSemver(version)) {
78
89
  throw new Error(`workspace version must be x.y.z, got ${JSON.stringify(version)}`);
79
90
  }
80
91
  writeFileSync(versionPath(root), `${version}\n`);
@@ -107,14 +118,7 @@ export function syncWorkspaceManifestVersion(manifestPath: string, version: stri
107
118
  /** Run git in `cwd`, capturing stdout and swallowing failure (offline, no repo). */
108
119
  function gitCapture(cwd: string, args: string[]): string {
109
120
  try {
110
- const res = exec.spawnSync("git", args, {
111
- cwd,
112
- stdout: "capture",
113
- stderr: "ignore",
114
- stdin: "ignore",
115
- check: false,
116
- });
117
- return res.stdout?.trim() ?? "";
121
+ return captureTaskCommand(cwd, "git", args);
118
122
  } catch {
119
123
  return "";
120
124
  }
@@ -131,7 +135,8 @@ export function latestTagVersion(cwd: string, prefix: string): Semver | undefine
131
135
  `${prefix}*`,
132
136
  ]);
133
137
  for (const tag of out.split("\n")) {
134
- const v = parseSemver(tag.replace(prefix, ""));
138
+ if (!tag.startsWith(prefix)) continue;
139
+ const v = parseSemver(tag.slice(prefix.length));
135
140
  if (v) return v;
136
141
  }
137
142
  return undefined;