@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 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 request workflow creates or updates
458
- the source PR. `--approve` enables auto-merge after checks.
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. Publication applies each
465
- file to its matching GitHub Release, then removes the temporary notes tree in a
466
- cleanup commit. Request and final notes never count as source changes.
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.8.1";
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
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "main": "index.ts",
52
52
  "license": "Apache-2.0",
53
- "version": "0.8.1",
53
+ "version": "0.9.0",
54
54
  "types": "index.ts",
55
55
  "type": "module",
56
56
  "exports": {
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
- warnings += errors.length;
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
 
@@ -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: { RELEASE_PRS: "${{ steps.release.outputs.prs }}" },
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 or update source pull request",
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
- 'if [ "$APPROVE" = "true" ]; then gh pr merge "$PR" --auto --merge; fi',
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
- 'if git merge-base --is-ancestor "origin/$SOURCE_BRANCH" HEAD; then',
757
- ' git push origin "HEAD:refs/heads/$SOURCE_BRANCH"',
758
- 'elif git merge-base --is-ancestor HEAD "origin/$SOURCE_BRANCH"; then',
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 "$GITHUB_SHA"; then',
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 " + "${{ github.event_name == 'push' && github.sha || inputs.component }}";
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));
@@ -112,7 +112,6 @@ if (import.meta.main) {
112
112
  "Release Please version manifest",
113
113
  ".release-please-manifest.json",
114
114
  )
115
- .option("--approve", "compatibility alias for the Release Please release pass")
116
115
  .action(
117
116
  async (options: {
118
117
  owner?: string;
@@ -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, approve: options.approve === true });
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("--approve", "enable merge-queue or auto-merge for the source PR")
162
- .option("--message <message>", "commit message for uncommitted source changes")
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
- approve?: boolean;
169
- message?: string;
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
- approve: options.approve,
180
- message: options.message,
181
- notes: options.notes,
180
+ message: optionText(options.message),
181
+ notes: optionText(options.notes),
182
182
  notesFile: options.notesFile,
183
183
  });
184
184
  },