@dbx-tools/projen 0.9.6 → 0.9.8
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 +5 -1
- package/index.ts +2 -1
- package/package.json +4 -4
- package/src/_rust-project.ts +46 -0
- package/src/_rust-release-workflow.ts +157 -52
- package/src/barrels.ts +3 -4
- package/src/bun-app.ts +96 -85
- package/src/bun-workflow.ts +60 -52
- package/src/openapi.ts +15 -8
- package/src/project-py.ts +22 -17
- package/src/project-rs.ts +48 -11
- package/src/release-catalog.ts +4 -1
- package/src/release.ts +206 -170
- package/src/watch.ts +4 -1
- package/src/workspace-lock.ts +22 -0
- package/tasks/publish-uniffi-local.ts +80 -7
- package/tasks/release-pr.ts +215 -204
- package/tasks/sync.ts +6 -5
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";
|
|
@@ -205,47 +206,52 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
205
206
|
"${{ github.event_name == 'workflow_dispatch' && inputs.expected_sha || '' }}",
|
|
206
207
|
DRY_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}",
|
|
207
208
|
},
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
),
|
|
249
255
|
},
|
|
250
256
|
{
|
|
251
257
|
name: "Verify source artifact run",
|
|
@@ -256,15 +262,20 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
256
262
|
SOURCE_RUN_ID: "${{ inputs.source_run_id }}",
|
|
257
263
|
},
|
|
258
264
|
with: {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
),
|
|
268
279
|
},
|
|
269
280
|
},
|
|
270
281
|
],
|
|
@@ -422,44 +433,49 @@ function independentReleasePleaseJob(project: DBXToolsJavaScriptProject, branch:
|
|
|
422
433
|
RELEASE_PRS: "${{ steps.release.outputs.prs }}",
|
|
423
434
|
},
|
|
424
435
|
shell: "bash",
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
"
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
"
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
),
|
|
463
479
|
},
|
|
464
480
|
],
|
|
465
481
|
};
|
|
@@ -499,22 +515,27 @@ function configureReleaseRequestWorkflow(
|
|
|
499
515
|
name: "Read release request",
|
|
500
516
|
id: "request",
|
|
501
517
|
shell: "bash",
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"
|
|
507
|
-
"
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
"
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
+
),
|
|
518
539
|
},
|
|
519
540
|
{
|
|
520
541
|
name: "Create, merge, and release source pull request",
|
|
@@ -527,20 +548,25 @@ function configureReleaseRequestWorkflow(
|
|
|
527
548
|
NOTES_PATH: "${{ steps.request.outputs.notes_path }}",
|
|
528
549
|
},
|
|
529
550
|
shell: "bash",
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
"
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
+
),
|
|
544
570
|
},
|
|
545
571
|
],
|
|
546
572
|
});
|
|
@@ -672,20 +698,25 @@ function independentReleaseNotesJob(project: DBXToolsJavaScriptProject): Job {
|
|
|
672
698
|
{
|
|
673
699
|
name: "Remove consumed release notes",
|
|
674
700
|
shell: "bash",
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
"
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
+
),
|
|
689
720
|
},
|
|
690
721
|
],
|
|
691
722
|
};
|
|
@@ -769,44 +800,49 @@ function addIndependentBranchSyncJob(
|
|
|
769
800
|
name: `Safely sync ${branch}`,
|
|
770
801
|
shell: "bash",
|
|
771
802
|
env: { SOURCE_BRANCH: branch, RELEASE_BRANCH: releaseBranch },
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
"
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
+
),
|
|
810
846
|
},
|
|
811
847
|
],
|
|
812
848
|
});
|
package/src/watch.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
19
19
|
import { watch as pathWatch } from "@dbx-tools/path";
|
|
20
20
|
import { asyncUtils, log } from "@dbx-tools/shared-core";
|
|
21
21
|
import { isGeneratedFile, recordedRoots, repoRoot } from "./packages.ts";
|
|
22
|
+
import { withWorkspaceMutationLock } from "./workspace-lock.ts";
|
|
22
23
|
|
|
23
24
|
const logger = log.logger("projen:watch");
|
|
24
25
|
const DEBOUNCE_MS = 250;
|
|
@@ -103,7 +104,9 @@ export function watchLoop(
|
|
|
103
104
|
.filter((p) => !ignoredPath(p));
|
|
104
105
|
pending.clear();
|
|
105
106
|
try {
|
|
106
|
-
if (relevant.length)
|
|
107
|
+
if (relevant.length) {
|
|
108
|
+
await withWorkspaceMutationLock(repoRoot, () => onBatch(relevant));
|
|
109
|
+
}
|
|
107
110
|
} catch (err) {
|
|
108
111
|
logger.error(`${tag} cycle failed:`, err instanceof Error ? err.message : err);
|
|
109
112
|
} finally {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Cross-process coordination for generated workspace mutations. */
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import * as fileLock from "@dbx-tools/core/file-lock";
|
|
4
|
+
import { log } from "@dbx-tools/shared-core";
|
|
5
|
+
|
|
6
|
+
const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
|
|
7
|
+
const logger = log.logger("projen:workspace-lock");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Serialize synthesis, generated-file watchers, and release preparation for one
|
|
11
|
+
* repository without coordinating unrelated worktrees or repositories.
|
|
12
|
+
*/
|
|
13
|
+
export function withWorkspaceMutationLock<T>(
|
|
14
|
+
root: string,
|
|
15
|
+
callback: () => T | Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
const repository = resolve(root);
|
|
18
|
+
return fileLock.withFileLock([MUTATION_LOCK_SCOPE, repository], callback, {
|
|
19
|
+
backends: process.platform === "win32" ? ["file"] : ["flock", "file"],
|
|
20
|
+
onWait: () => logger.info("waiting for workspace mutation lock", { repository }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -4,7 +4,7 @@ import { arch, platform } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { log } from "@dbx-tools/shared-core";
|
|
7
|
+
import { json, log, object } from "@dbx-tools/shared-core";
|
|
8
8
|
import { parse, stringify } from "smol-toml";
|
|
9
9
|
import { captureTaskCommand, runTaskCommand, taskCommandSucceeds } from "../src/_task-command.ts";
|
|
10
10
|
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
@@ -94,17 +94,90 @@ function cargoVersionExists(crateName: string, version: string, registry: string
|
|
|
94
94
|
]);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
interface CargoMetadataPackage {
|
|
98
|
+
dependencies: Array<{ name: string; source?: string | null }>;
|
|
99
|
+
manifest_path: string;
|
|
100
|
+
name: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function orderedCargoManifests(config: RustWorkspaceMapping): string[] {
|
|
104
|
+
const configured = new Set(config.crates.map((crate) => resolve(repoRoot, crate, "Cargo.toml")));
|
|
105
|
+
const metadata = json.parseRecord(
|
|
106
|
+
run("cargo", ["metadata", "--format-version", "1", "--no-deps", "--locked"], true),
|
|
107
|
+
);
|
|
108
|
+
if (!metadata || !Array.isArray(metadata.packages)) {
|
|
109
|
+
throw new Error("Cargo metadata did not return workspace packages");
|
|
110
|
+
}
|
|
111
|
+
const packages = metadata.packages.filter(object.isRecord).map((pkg) => {
|
|
112
|
+
if (
|
|
113
|
+
typeof pkg.name !== "string" ||
|
|
114
|
+
typeof pkg.manifest_path !== "string" ||
|
|
115
|
+
!Array.isArray(pkg.dependencies)
|
|
116
|
+
) {
|
|
117
|
+
throw new Error("Cargo metadata returned an invalid package");
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
name: pkg.name,
|
|
121
|
+
manifest_path: resolve(pkg.manifest_path),
|
|
122
|
+
dependencies: pkg.dependencies.filter(object.isRecord).map((dependency) => {
|
|
123
|
+
if (typeof dependency.name !== "string") {
|
|
124
|
+
throw new Error(`Cargo metadata returned an invalid dependency for ${pkg.name}`);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
name: dependency.name,
|
|
128
|
+
source: typeof dependency.source === "string" ? dependency.source : null,
|
|
129
|
+
};
|
|
130
|
+
}),
|
|
131
|
+
} satisfies CargoMetadataPackage;
|
|
132
|
+
});
|
|
133
|
+
const publishable = new Map(
|
|
134
|
+
packages
|
|
135
|
+
.filter((pkg) => configured.has(pkg.manifest_path))
|
|
136
|
+
.filter((pkg) => {
|
|
137
|
+
const manifest = parse(readFileSync(pkg.manifest_path, "utf8")) as {
|
|
138
|
+
package?: { publish?: boolean };
|
|
139
|
+
};
|
|
140
|
+
return manifest.package?.publish !== false;
|
|
141
|
+
})
|
|
142
|
+
.map((pkg) => [pkg.name, pkg]),
|
|
143
|
+
);
|
|
144
|
+
const ordered: CargoMetadataPackage[] = [];
|
|
145
|
+
const visiting = new Set<string>();
|
|
146
|
+
const completed = new Set<string>();
|
|
147
|
+
const visit = (pkg: CargoMetadataPackage): void => {
|
|
148
|
+
if (completed.has(pkg.name)) return;
|
|
149
|
+
if (visiting.has(pkg.name)) {
|
|
150
|
+
throw new Error(`Cyclic Cargo publication dependency: ${pkg.name}`);
|
|
151
|
+
}
|
|
152
|
+
visiting.add(pkg.name);
|
|
153
|
+
for (const dependency of pkg.dependencies) {
|
|
154
|
+
if (dependency.source) continue;
|
|
155
|
+
const workspaceDependency = publishable.get(dependency.name);
|
|
156
|
+
if (workspaceDependency) visit(workspaceDependency);
|
|
157
|
+
}
|
|
158
|
+
visiting.delete(pkg.name);
|
|
159
|
+
completed.add(pkg.name);
|
|
160
|
+
ordered.push(pkg);
|
|
161
|
+
};
|
|
162
|
+
for (const pkg of publishable.values()) visit(pkg);
|
|
163
|
+
return ordered.map((pkg) => pkg.manifest_path);
|
|
164
|
+
}
|
|
165
|
+
|
|
97
166
|
function publishCargo(config: RustWorkspaceMapping, registry: string, version: string): void {
|
|
98
|
-
const manifests = config
|
|
99
|
-
.map((crate) => resolve(repoRoot, crate, "Cargo.toml"))
|
|
100
|
-
.filter((manifest) => !/^publish = false$/m.test(readFileSync(manifest, "utf8")));
|
|
167
|
+
const manifests = orderedCargoManifests(config);
|
|
101
168
|
const originals = new Map(
|
|
102
169
|
manifests.map((manifest) => [manifest, readFileSync(manifest, "utf8")]),
|
|
103
170
|
);
|
|
104
171
|
try {
|
|
105
|
-
const crateNames = manifests.map(
|
|
106
|
-
|
|
107
|
-
|
|
172
|
+
const crateNames = manifests.map((manifest) => {
|
|
173
|
+
const document = parse(readFileSync(manifest, "utf8")) as {
|
|
174
|
+
package?: { name?: string };
|
|
175
|
+
};
|
|
176
|
+
if (!document.package?.name) {
|
|
177
|
+
throw new Error(`Cargo manifest has no package name: ${manifest}`);
|
|
178
|
+
}
|
|
179
|
+
return document.package.name;
|
|
180
|
+
});
|
|
108
181
|
for (const manifest of manifests) {
|
|
109
182
|
const mode = statSync(manifest).mode;
|
|
110
183
|
const document = parse(readFileSync(manifest, "utf8")) as Record<string, unknown>;
|