@dbx-tools/projen 0.8.1 → 0.9.0
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 +9 -5
- package/index.ts +1 -1
- package/package.json +1 -1
- package/src/codegen.ts +18 -6
- package/src/release-catalog.ts +14 -0
- package/src/release.ts +49 -14
- package/tasks/release-please.ts +0 -1
- package/tasks/release-request.ts +12 -12
package/README.md
CHANGED
|
@@ -454,16 +454,20 @@ a new package is covered without a re-synth. Work from the root:
|
|
|
454
454
|
before notes, AI, commits, pushes, or PR creation. It commits dirty source,
|
|
455
455
|
writes custom `--notes` / `--notes-file` content or a bounded Cursor, Codex,
|
|
456
456
|
Claude, then Git fallback to `.release-notes/requests/<branch>.md`, adds one
|
|
457
|
-
request commit, and pushes. The generated
|
|
458
|
-
|
|
457
|
+
request commit, and pushes. The generated workflow creates or updates the source
|
|
458
|
+
PR, merges it directly with `GITHUB_TOKEN`, and dispatches release planning, so
|
|
459
|
+
no UI approval or merge is required. GitHub may briefly record
|
|
460
|
+
`action_required` for the bot-authored PR validation; it is not a required gate.
|
|
459
461
|
|
|
460
462
|
After the source PR merges, Release Please retains direct conventional semantic
|
|
461
463
|
increments and applies required dependent patches. Reconciliation copies the
|
|
462
464
|
readable request Markdown into one temporary
|
|
463
465
|
`.release-notes/final/<component>-v<version>.md` per affected component and
|
|
464
|
-
deletes the consumed request file in the release PR
|
|
465
|
-
|
|
466
|
-
|
|
466
|
+
deletes the consumed request file in the release PR, then merges that generated
|
|
467
|
+
PR and explicitly dispatches publication. A source merge with no affected
|
|
468
|
+
release units creates no release PR or publication work. Publication applies
|
|
469
|
+
each file to its matching GitHub Release, then removes the temporary notes tree
|
|
470
|
+
in a cleanup commit. Request and final notes never count as source changes.
|
|
467
471
|
|
|
468
472
|
The GitHub PR workflow runs the explicit `pr:validate` task through Projen's
|
|
469
473
|
public `BuildWorkflow` `buildTask` option. That task runs synth plus the
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
-
export const PACKAGE_VERSION = "0.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.0";
|
|
7
7
|
export * as barrels from "./src/barrels.ts";
|
|
8
8
|
export * as bunApp from "./src/bun-app.ts";
|
|
9
9
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
package/package.json
CHANGED
package/src/codegen.ts
CHANGED
|
@@ -67,6 +67,16 @@ interface CodegenInput {
|
|
|
67
67
|
name: string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function packageName(dir: string): string {
|
|
71
|
+
const name = readPackageManifest(dir)?.name;
|
|
72
|
+
return typeof name === "string" && name.length > 0 ? name : basename(dir);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sourceLabel(source: string): string {
|
|
76
|
+
const nodeModulesPrefix = "node_modules/";
|
|
77
|
+
return source.startsWith(nodeModulesPrefix) ? source.slice(nodeModulesPrefix.length) : source;
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
/** Read a package's `package.json` `codegen.inputs`, or `undefined` if absent. */
|
|
71
81
|
function codegenInputs(dir: string): string[] | undefined {
|
|
72
82
|
const codegen = readPackageManifest(dir)?.codegen;
|
|
@@ -282,6 +292,7 @@ function generatePackage(
|
|
|
282
292
|
): string {
|
|
283
293
|
const parsed = inputs.map(parseInputArg);
|
|
284
294
|
const srcDir = resolve(dir, "src");
|
|
295
|
+
const destinationPackage = packageName(dir);
|
|
285
296
|
mkdirSync(srcDir, { recursive: true });
|
|
286
297
|
|
|
287
298
|
// Remove prior generated modules (read-only + our header) so a dropped input
|
|
@@ -295,12 +306,13 @@ function generatePackage(
|
|
|
295
306
|
}
|
|
296
307
|
}
|
|
297
308
|
|
|
298
|
-
let warnings = 0;
|
|
299
309
|
for (const input of parsed) {
|
|
300
310
|
const sourcePath = resolveInputSource(input.source, dir, projectRoot);
|
|
301
311
|
if (!existsSync(sourcePath)) {
|
|
302
312
|
throw new Error(`codegen input not found: ${input.source}`);
|
|
303
313
|
}
|
|
314
|
+
const destination = `${destinationPackage}/src/${input.name}.ts`;
|
|
315
|
+
const source = sourceLabel(input.source);
|
|
304
316
|
|
|
305
317
|
const sourceText = preprocess(stripImports(tsRuntime, sourcePath));
|
|
306
318
|
const { getZodSchemasFile, getInferredTypes, errors } = generate({
|
|
@@ -310,8 +322,7 @@ function generatePackage(
|
|
|
310
322
|
keepComments: true,
|
|
311
323
|
});
|
|
312
324
|
if (errors.length) {
|
|
313
|
-
|
|
314
|
-
for (const err of errors) logger.warn(` ! ${err}`);
|
|
325
|
+
for (const err of errors) logger.warn(`${destination} from ${source}: ${err}`);
|
|
315
326
|
}
|
|
316
327
|
|
|
317
328
|
// ts-to-zod adds an import line only when the source references external
|
|
@@ -324,11 +335,12 @@ function generatePackage(
|
|
|
324
335
|
makeWritable(outPath);
|
|
325
336
|
writeFileSync(outPath, content);
|
|
326
337
|
makeReadonly(outPath);
|
|
338
|
+
logger.success(
|
|
339
|
+
`generated ${destination} from ${source}` +
|
|
340
|
+
(errors.length ? ` (${errors.length} warning(s))` : ""),
|
|
341
|
+
);
|
|
327
342
|
}
|
|
328
343
|
|
|
329
|
-
logger.success(
|
|
330
|
-
`${basename(dir)}: ${parsed.length} module(s)` + (warnings ? ` (${warnings} warning(s))` : ""),
|
|
331
|
-
);
|
|
332
344
|
return dir;
|
|
333
345
|
}
|
|
334
346
|
|
package/src/release-catalog.ts
CHANGED
|
@@ -800,12 +800,26 @@ function gitReleaseFiles(root: string, paths: readonly string[]): string[] | und
|
|
|
800
800
|
.filter(Boolean)
|
|
801
801
|
.map((path) => resolve(root, path))
|
|
802
802
|
.filter((path) => existsSync(path))
|
|
803
|
+
.filter((path) => paths.some((sourcePath) => trackedReleaseFile(sourcePath, path)))
|
|
803
804
|
.sort();
|
|
804
805
|
} catch {
|
|
805
806
|
return undefined;
|
|
806
807
|
}
|
|
807
808
|
}
|
|
808
809
|
|
|
810
|
+
function trackedReleaseFile(sourcePath: string, path: string): boolean {
|
|
811
|
+
const sourceRelative = toPosix(relative(sourcePath, path));
|
|
812
|
+
if (sourceRelative.startsWith("../") || sourceRelative === "..") return false;
|
|
813
|
+
const parts = sourceRelative.split("/");
|
|
814
|
+
const file = parts.pop();
|
|
815
|
+
if (!file) return false;
|
|
816
|
+
if (parts.some((part) => IGNORED_DIRECTORIES.has(part))) return false;
|
|
817
|
+
if (IGNORED_FILES.has(file) || file.endsWith(".lock") || file.startsWith("tsconfig")) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
return sourceRelative !== "index.ts";
|
|
821
|
+
}
|
|
822
|
+
|
|
809
823
|
function releaseFiles(path: string): string[] {
|
|
810
824
|
if (!existsSync(path)) return [];
|
|
811
825
|
const entries = readdirSync(path, { withFileTypes: true });
|
package/src/release.ts
CHANGED
|
@@ -352,9 +352,10 @@ function addDocsJobs(
|
|
|
352
352
|
|
|
353
353
|
function independentReleasePleaseJob(project: DBXToolsJavaScriptProject, branch: string): Job {
|
|
354
354
|
return {
|
|
355
|
-
if: "${{ github.event_name == 'push' }}",
|
|
355
|
+
if: "${{ github.event_name == 'push' || inputs.automatic == true }}",
|
|
356
356
|
runsOn: ["ubuntu-latest"],
|
|
357
357
|
permissions: {
|
|
358
|
+
actions: JobPermission.WRITE,
|
|
358
359
|
contents: JobPermission.WRITE,
|
|
359
360
|
pullRequests: JobPermission.WRITE,
|
|
360
361
|
},
|
|
@@ -419,7 +420,11 @@ function independentReleasePleaseJob(project: DBXToolsJavaScriptProject, branch:
|
|
|
419
420
|
{
|
|
420
421
|
name: "Reconcile generated release PR files",
|
|
421
422
|
if: "${{ steps.release.outputs.prs_created == 'true' }}",
|
|
422
|
-
env: {
|
|
423
|
+
env: {
|
|
424
|
+
GH_TOKEN: "${{ github.token }}",
|
|
425
|
+
RELEASE_BRANCH: branch,
|
|
426
|
+
RELEASE_PRS: "${{ steps.release.outputs.prs }}",
|
|
427
|
+
},
|
|
423
428
|
shell: "bash",
|
|
424
429
|
run: [
|
|
425
430
|
'BRANCH="$(jq -r \'.[0].headBranchName\' <<<"$RELEASE_PRS")"',
|
|
@@ -438,6 +443,26 @@ function independentReleasePleaseJob(project: DBXToolsJavaScriptProject, branch:
|
|
|
438
443
|
' git commit -m "chore: reconcile release metadata"',
|
|
439
444
|
' git push origin "HEAD:$BRANCH"',
|
|
440
445
|
"fi",
|
|
446
|
+
'MERGED="false"',
|
|
447
|
+
"for ATTEMPT in $(seq 1 15); do",
|
|
448
|
+
' STATE="$(gh pr view "$BRANCH" --json state,mergeable --jq \'[.state,.mergeable] | join(" ")\')"',
|
|
449
|
+
' if [[ "$STATE" == MERGED* ]]; then MERGED="true"; break; fi',
|
|
450
|
+
' if [ "$STATE" = "OPEN CONFLICTING" ]; then',
|
|
451
|
+
' echo "::error::release pull request has merge conflicts"',
|
|
452
|
+
" exit 1",
|
|
453
|
+
" fi",
|
|
454
|
+
' if [ "$STATE" = "OPEN MERGEABLE" ] && gh pr merge "$BRANCH" --merge; then',
|
|
455
|
+
' MERGED="true"',
|
|
456
|
+
" break",
|
|
457
|
+
" fi",
|
|
458
|
+
' echo "release pull request is not mergeable yet (attempt $ATTEMPT/15)"',
|
|
459
|
+
" sleep 2",
|
|
460
|
+
"done",
|
|
461
|
+
'if [ "$MERGED" != "true" ]; then',
|
|
462
|
+
' echo "::error::release pull request did not become mergeable"',
|
|
463
|
+
" exit 1",
|
|
464
|
+
"fi",
|
|
465
|
+
'gh workflow run release.yml --ref "$RELEASE_BRANCH" -f automatic=true',
|
|
441
466
|
].join("\n"),
|
|
442
467
|
},
|
|
443
468
|
],
|
|
@@ -464,6 +489,7 @@ function configureReleaseRequestWorkflow(
|
|
|
464
489
|
if: `\${{ github.ref_name != '${baseBranch}' }}`,
|
|
465
490
|
runsOn: ["ubuntu-latest"],
|
|
466
491
|
permissions: {
|
|
492
|
+
actions: JobPermission.WRITE,
|
|
467
493
|
contents: JobPermission.WRITE,
|
|
468
494
|
pullRequests: JobPermission.WRITE,
|
|
469
495
|
},
|
|
@@ -490,13 +516,12 @@ function configureReleaseRequestWorkflow(
|
|
|
490
516
|
"fi",
|
|
491
517
|
'echo "requested=true" >> "$GITHUB_OUTPUT"',
|
|
492
518
|
'echo "branch=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"',
|
|
493
|
-
'echo "approve=$(sed -n \'s/^Release-Approve: //p\' <<<"$MESSAGE" | tail -1)" >> "$GITHUB_OUTPUT"',
|
|
494
519
|
'echo "notes_path=$(sed -n \'s/^Release-Notes-Path: //p\' <<<"$MESSAGE" | tail -1)" >> "$GITHUB_OUTPUT"',
|
|
495
520
|
'echo "title=$(git log -1 --skip=1 --format=%s)" >> "$GITHUB_OUTPUT"',
|
|
496
521
|
].join("\n"),
|
|
497
522
|
},
|
|
498
523
|
{
|
|
499
|
-
name: "Create
|
|
524
|
+
name: "Create, merge, and release source pull request",
|
|
500
525
|
if: "${{ steps.request.outputs.requested == 'true' }}",
|
|
501
526
|
env: {
|
|
502
527
|
GH_TOKEN: "${{ github.token }}",
|
|
@@ -504,7 +529,6 @@ function configureReleaseRequestWorkflow(
|
|
|
504
529
|
BASE_BRANCH: baseBranch,
|
|
505
530
|
TITLE: "${{ steps.request.outputs.title }}",
|
|
506
531
|
NOTES_PATH: "${{ steps.request.outputs.notes_path }}",
|
|
507
|
-
APPROVE: "${{ steps.request.outputs.approve }}",
|
|
508
532
|
},
|
|
509
533
|
shell: "bash",
|
|
510
534
|
run: [
|
|
@@ -518,7 +542,8 @@ function configureReleaseRequestWorkflow(
|
|
|
518
542
|
' gh pr create --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --title "$TITLE" --body "$BODY"',
|
|
519
543
|
' PR="$(gh pr list --head "$SOURCE_BRANCH" --base "$BASE_BRANCH" --state open --json number --jq \'.[0].number\')"',
|
|
520
544
|
"fi",
|
|
521
|
-
'
|
|
545
|
+
'gh pr merge "$PR" --merge',
|
|
546
|
+
'gh workflow run release.yml --repo "$GITHUB_REPOSITORY" --ref "$BASE_BRANCH" -f automatic=true',
|
|
522
547
|
].join("\n"),
|
|
523
548
|
},
|
|
524
549
|
],
|
|
@@ -527,7 +552,7 @@ function configureReleaseRequestWorkflow(
|
|
|
527
552
|
|
|
528
553
|
function independentReleasePlanJob(project: DBXToolsJavaScriptProject): Job {
|
|
529
554
|
return {
|
|
530
|
-
if: "${{ always() && (github.event_name == 'workflow_dispatch' || needs.release-please.outputs.releases_created == 'true' || needs.release-please.outputs.docs_changed == 'true' || needs.release-please.outputs.recovery_requested == 'true') }}",
|
|
555
|
+
if: "${{ always() && ((github.event_name == 'workflow_dispatch' && inputs.automatic != true) || needs.release-please.outputs.releases_created == 'true' || (needs.release-please.outputs.docs_changed == 'true' && needs.release-please.outputs.prs_created != 'true') || needs.release-please.outputs.recovery_requested == 'true') }}",
|
|
531
556
|
needs: ["release-please"],
|
|
532
557
|
runsOn: ["ubuntu-latest"],
|
|
533
558
|
permissions: { contents: JobPermission.READ },
|
|
@@ -727,8 +752,9 @@ function addIndependentBranchSyncJob(
|
|
|
727
752
|
project: DBXToolsJavaScriptProject,
|
|
728
753
|
branch: string,
|
|
729
754
|
): void {
|
|
755
|
+
const releaseBranch = projectReleaseBranch(project);
|
|
730
756
|
workflow.addJob("sync-release-branch", {
|
|
731
|
-
if: "${{ always() && github.event_name == 'push' && needs['publication-complete'].result == 'success' }}",
|
|
757
|
+
if: "${{ always() && (github.event_name == 'push' || inputs.automatic == true) && needs['publication-complete'].result == 'success' }}",
|
|
732
758
|
needs: ["publication-complete"],
|
|
733
759
|
runsOn: ["ubuntu-latest"],
|
|
734
760
|
permissions: { contents: JobPermission.WRITE },
|
|
@@ -746,22 +772,23 @@ function addIndependentBranchSyncJob(
|
|
|
746
772
|
{
|
|
747
773
|
name: `Safely sync ${branch}`,
|
|
748
774
|
shell: "bash",
|
|
749
|
-
env: { SOURCE_BRANCH: branch },
|
|
775
|
+
env: { SOURCE_BRANCH: branch, RELEASE_BRANCH: releaseBranch },
|
|
750
776
|
run: [
|
|
751
777
|
'if ! git ls-remote --exit-code --heads origin "refs/heads/$SOURCE_BRANCH" >/dev/null 2>&1; then',
|
|
752
778
|
' echo "source branch $SOURCE_BRANCH no longer exists; nothing to sync"',
|
|
753
779
|
" exit 0",
|
|
754
780
|
"fi",
|
|
755
781
|
'git fetch origin "refs/heads/$SOURCE_BRANCH:refs/remotes/origin/$SOURCE_BRANCH"',
|
|
756
|
-
'
|
|
757
|
-
'
|
|
758
|
-
'
|
|
782
|
+
'git fetch origin "refs/heads/$RELEASE_BRANCH:refs/remotes/origin/$RELEASE_BRANCH"',
|
|
783
|
+
'if git merge-base --is-ancestor "origin/$SOURCE_BRANCH" "origin/$RELEASE_BRANCH"; then',
|
|
784
|
+
' git push origin "refs/remotes/origin/$RELEASE_BRANCH:refs/heads/$SOURCE_BRANCH"',
|
|
785
|
+
'elif git merge-base --is-ancestor "origin/$RELEASE_BRANCH" "origin/$SOURCE_BRANCH"; then',
|
|
759
786
|
' echo "source branch $SOURCE_BRANCH already contains released main"',
|
|
760
787
|
"else",
|
|
761
788
|
' git config user.name "github-actions[bot]"',
|
|
762
789
|
' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"',
|
|
763
790
|
' git switch --force-create "$SOURCE_BRANCH" "origin/$SOURCE_BRANCH"',
|
|
764
|
-
' if git merge --no-ff --no-edit "
|
|
791
|
+
' if git merge --no-ff --no-edit "origin/$RELEASE_BRANCH"; then',
|
|
765
792
|
' git push origin "HEAD:refs/heads/$SOURCE_BRANCH"',
|
|
766
793
|
" else",
|
|
767
794
|
' SAFE_GENERATED="true"',
|
|
@@ -862,11 +889,18 @@ function configureIndependentRelease(
|
|
|
862
889
|
concurrencyOptions: { group: "release", cancelInProgress: false },
|
|
863
890
|
});
|
|
864
891
|
workflow.runName =
|
|
865
|
-
"release units " +
|
|
892
|
+
"release units " +
|
|
893
|
+
"${{ github.event_name == 'push' && github.sha || inputs.automatic && 'automatic' || inputs.component }}";
|
|
866
894
|
workflow.on({
|
|
867
895
|
push: { branches: [branch] },
|
|
868
896
|
workflowDispatch: {
|
|
869
897
|
inputs: {
|
|
898
|
+
automatic: {
|
|
899
|
+
description: "Continue an automatically merged Release Please pull request",
|
|
900
|
+
type: "boolean",
|
|
901
|
+
default: "false",
|
|
902
|
+
required: false,
|
|
903
|
+
},
|
|
870
904
|
component: {
|
|
871
905
|
description: "Component to recover",
|
|
872
906
|
type: "string",
|
|
@@ -889,6 +923,7 @@ function configureIndependentRelease(
|
|
|
889
923
|
},
|
|
890
924
|
},
|
|
891
925
|
});
|
|
926
|
+
workflow.file?.addOverride("on.workflow_dispatch.inputs.automatic.default", false);
|
|
892
927
|
workflow.file?.addOverride("permissions.contents", "read");
|
|
893
928
|
workflow.addJob("release-please", independentReleasePleaseJob(project, branch));
|
|
894
929
|
workflow.addJob("release-plan", independentReleasePlanJob(project));
|
package/tasks/release-please.ts
CHANGED
package/tasks/release-request.ts
CHANGED
|
@@ -13,7 +13,6 @@ const logger = log.logger("projen:release-request");
|
|
|
13
13
|
export interface PrepareReleaseRequestOptions {
|
|
14
14
|
readonly root: string;
|
|
15
15
|
readonly baseBranch: string;
|
|
16
|
-
readonly approve?: boolean;
|
|
17
16
|
readonly message?: string;
|
|
18
17
|
readonly notes?: string;
|
|
19
18
|
readonly notesFile?: string;
|
|
@@ -94,12 +93,11 @@ export async function prepareReleaseRequest(
|
|
|
94
93
|
[
|
|
95
94
|
"Release-Request: true",
|
|
96
95
|
`Release-Source-Branch: ${branch}`,
|
|
97
|
-
`Release-Approve: ${options.approve === true}`,
|
|
98
96
|
`Release-Notes-Path: ${notesPath}`,
|
|
99
97
|
].join("\n"),
|
|
100
98
|
]);
|
|
101
99
|
if (options.push !== false) run(root, ["push", "--set-upstream", "origin", branch]);
|
|
102
|
-
logger.success("release request pushed", { branch
|
|
100
|
+
logger.success("release request pushed", { branch });
|
|
103
101
|
return true;
|
|
104
102
|
}
|
|
105
103
|
|
|
@@ -155,19 +153,22 @@ function run(root: string, args: string[]): void {
|
|
|
155
153
|
});
|
|
156
154
|
}
|
|
157
155
|
|
|
156
|
+
/** Restore option text after Projen forwards task arguments without shell quoting. */
|
|
157
|
+
function optionText(values?: readonly string[]): string | undefined {
|
|
158
|
+
return values?.join(" ");
|
|
159
|
+
}
|
|
160
|
+
|
|
158
161
|
if (import.meta.main) {
|
|
159
162
|
await new Command()
|
|
160
163
|
.option("--base <branch>", "release target branch", "main")
|
|
161
|
-
.option("--
|
|
162
|
-
.option("--
|
|
163
|
-
.option("--notes <notes>", "custom release notes")
|
|
164
|
+
.option("--message <message...>", "commit message for uncommitted source changes")
|
|
165
|
+
.option("--notes <notes...>", "custom release notes")
|
|
164
166
|
.option("--notes-file <path>", "read custom release notes from a file")
|
|
165
167
|
.action(
|
|
166
168
|
async (options: {
|
|
167
169
|
base: string;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
notes?: string;
|
|
170
|
+
message?: string[];
|
|
171
|
+
notes?: string[];
|
|
171
172
|
notesFile?: string;
|
|
172
173
|
}) => {
|
|
173
174
|
if (options.notes && options.notesFile) {
|
|
@@ -176,9 +177,8 @@ if (import.meta.main) {
|
|
|
176
177
|
await prepareReleaseRequest({
|
|
177
178
|
root: project.root() ?? process.cwd(),
|
|
178
179
|
baseBranch: options.base,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
notes: options.notes,
|
|
180
|
+
message: optionText(options.message),
|
|
181
|
+
notes: optionText(options.notes),
|
|
182
182
|
notesFile: options.notesFile,
|
|
183
183
|
});
|
|
184
184
|
},
|