@dbx-tools/projen 0.9.146 → 0.9.150

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
@@ -316,14 +316,14 @@ scripts, and generated files on top.
316
316
  Every workspace root exposes a generic development watcher:
317
317
 
318
318
  ```bash
319
- bun run dev:watch [--debounce-ms 15000] [--restart-key r] <command> [args...]
319
+ bun run dev:watch <command> [args...]
320
320
  ```
321
321
 
322
- It restarts after watched writes have been quiet for 15 seconds. Command paths,
323
- `--cwd`, and `--filter` identify the owning package. Its transitive workspace
322
+ It restarts after watched writes have been quiet for 15 seconds. Command paths
323
+ and task selectors identify the owning package. Its transitive workspace
324
324
  dependencies are watched with the shared generated/dependency/test/temp ignores.
325
- Press the configured key for an immediate restart. The demo uses this task to
326
- run its concrete server entrypoint. The `server` tag does not assume one.
325
+ The configured restart key triggers an immediate restart. The demo uses this
326
+ task to run its concrete server entrypoint. The `server` tag does not assume one.
327
327
 
328
328
  AppKit 0.81's `appkitServerConfig()` is the preferred tsdown preset for a
329
329
  consumer app using conventional compiled `server/agents/*/agent.ts` entries. It
@@ -541,10 +541,12 @@ diverged. From `main`, it calls the existing `bump` task, commits the generated
541
541
  version changes, pushes `main`, and pushes the matching annotated `vX.Y.Z` tag.
542
542
  Pass `--no-bump` only to release an existing synchronized local bump. The tag
543
543
  workflow verifies that the tag points at the exact `origin/main` commit. The
544
- local task waits for this workflow by default, displays live job and step
545
- progress, then prints its logs. Local-only releases and repositories without a
546
- resolved authenticated GitHub account skip the watcher. Stopping the local
547
- watcher does not cancel the remote run.
544
+ local task completes local publication and demo deployment first, then waits for
545
+ this workflow by default. It prints the initial job and step state, then only
546
+ append-only status changes, without annotations. Successful runs omit full logs;
547
+ unsuccessful runs print failed-step logs only. Local-only releases and
548
+ repositories without a resolved authenticated GitHub account skip the watcher.
549
+ Stopping the local watcher does not cancel the remote run.
548
550
 
549
551
  One `build-release` job installs workspace dependencies, runs validation, packs
550
552
  the npm archives, builds all Python distributions, and builds the documentation
@@ -556,7 +558,9 @@ workspace. Each Python package retains its own publishing environment and waits
556
558
  for its package dependencies to publish. The npm job uses the bundled archive
557
559
  publisher without a checkout or workspace install. Pages deploys the already
558
560
  built site after registry publication succeeds. Local npm and Python registry publication remains available
559
- through the direct local publication tooling.
561
+ through the direct local publication tooling. Those local publishers run
562
+ concurrently; a failure or release termination stops every live publisher before
563
+ the command returns.
560
564
 
561
565
  The default release publishes npm, Python, and docs, then publishes to
562
566
  configured local registries. Use `bun run release --help` or the generated
@@ -658,8 +662,8 @@ release [options]
658
662
  | `--no-docs` | Disable build and deploy documentation |
659
663
  | `--validation` | Run optional release validation tasks before publishing (default: true, env: PROJEN_RELEASE_VALIDATION) |
660
664
  | `--no-validation` | Disable run optional release validation tasks before publishing |
661
- | `-w, --watch-github-actions` | Wait for and stream the tag-triggered GitHub Actions release (default: true, env: PROJEN_RELEASE_WATCH_GITHUB_ACTIONS) |
662
- | `--no-watch-github-actions` | Disable wait for and stream the tag-triggered github actions release |
665
+ | `-w, --watch-github-actions` | Wait for and log the tag-triggered GitHub Actions release (default: true, env: PROJEN_RELEASE_WATCH_GITHUB_ACTIONS) |
666
+ | `--no-watch-github-actions` | Disable wait for and log the tag-triggered github actions release |
663
667
  | `--release-notes` | Generate and commit release notes for a new version bump (default: true, env: PROJEN_RELEASE_RELEASE_NOTES) |
664
668
  | `--no-release-notes` | Disable generate and commit release notes for a new version bump |
665
669
  | `--release-notes-text <value>` | Write supplied release-note Markdown without invoking Genie (env: PROJEN_RELEASE_RELEASE_NOTES_TEXT) |
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.9.146";
6
+ export const PACKAGE_VERSION = "0.9.150";
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
@@ -30,10 +30,10 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/cli-args": "^0.9.146",
34
- "@dbx-tools/core": "^0.9.146",
35
- "@dbx-tools/path": "^0.9.146",
36
- "@dbx-tools/shared-core": "^0.9.146",
33
+ "@dbx-tools/cli-args": "^0.9.150",
34
+ "@dbx-tools/core": "^0.9.150",
35
+ "@dbx-tools/path": "^0.9.150",
36
+ "@dbx-tools/shared-core": "^0.9.150",
37
37
  "commander": "^15.0.0",
38
38
  "concurrently": "^10.0.3",
39
39
  "constructs": "^10.6.0",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "main": "index.ts",
57
57
  "license": "Apache-2.0",
58
- "version": "0.9.146",
58
+ "version": "0.9.150",
59
59
  "types": "index.ts",
60
60
  "type": "module",
61
61
  "exports": {
@@ -40,7 +40,17 @@ export async function runTaskCommandAsync(
40
40
  args: readonly string[],
41
41
  options: TaskCommandOptions & { readonly signal?: AbortSignal } = {},
42
42
  ): Promise<void> {
43
- await exec.spawn(executable(command), [...args], {
43
+ await spawnTaskCommand(cwd, command, args, options);
44
+ }
45
+
46
+ /** Start one supervised task command and return its live process handle. */
47
+ export function spawnTaskCommand(
48
+ cwd: string,
49
+ command: string,
50
+ args: readonly string[],
51
+ options: TaskCommandOptions & { readonly signal?: AbortSignal } = {},
52
+ ): exec.ChildProcessResult {
53
+ return exec.spawn(executable(command), [...args], {
44
54
  cwd,
45
55
  env: options.env,
46
56
  stdout: "inherit",
@@ -1,10 +1,11 @@
1
1
  /** Build and publish the current workspace to configured loopback registries. */
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import type { ChildProcessResult } from "@dbx-tools/core/exec";
4
5
  import * as projectUtils from "@dbx-tools/core/project-utils";
5
6
  import { log, net } from "@dbx-tools/shared-core";
6
7
  import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
7
- import { runTaskCommandAsync } from "../src/_task-command.ts";
8
+ import { spawnTaskCommand } from "../src/_task-command.ts";
8
9
 
9
10
  const logger = log.logger("projen:local-publish");
10
11
 
@@ -38,6 +39,24 @@ export function localPublishTaskPath(task: "publish.ts" | "publish-python.ts"):
38
39
  return fileURLToPath(new URL(task, import.meta.url));
39
40
  }
40
41
 
42
+ /** Await concurrent publishers and terminate every live sibling after one fails. */
43
+ export async function awaitLocalPublishers(
44
+ publishes: readonly ChildProcessResult[],
45
+ ): Promise<void> {
46
+ try {
47
+ await Promise.all(publishes);
48
+ } catch (error) {
49
+ await Promise.all(
50
+ publishes.map((publish) =>
51
+ publish.exitCode === null && publish.signalCode === null
52
+ ? publish.shutdown()
53
+ : Promise.resolve(),
54
+ ),
55
+ );
56
+ throw error;
57
+ }
58
+ }
59
+
41
60
  /** Build and publish the current workspace directly to local registries. */
42
61
  export async function publishLocalRelease(
43
62
  options: LocalPublishOptions,
@@ -46,7 +65,7 @@ export async function publishLocalRelease(
46
65
  const localRegistry = resolveLocalRegistry(options.localRegistry);
47
66
  const activeIndexes = activePythonIndexes();
48
67
  const localPypi = resolveLocalPypi(options.localPypi, activeIndexes);
49
- const publishes: Promise<unknown>[] = [];
68
+ const publishes: ChildProcessResult[] = [];
50
69
 
51
70
  if (
52
71
  options.localPypi.toLowerCase() === "auto" &&
@@ -62,7 +81,7 @@ export async function publishLocalRelease(
62
81
  const publishNpmScript = localPublishTaskPath("publish.ts");
63
82
  logger.info(`building and publishing npm workspace to ${localRegistry}`);
64
83
  publishes.push(
65
- runTaskCommandAsync(root, process.execPath, [
84
+ spawnTaskCommand(root, process.execPath, [
66
85
  publishNpmScript,
67
86
  options.version,
68
87
  "--registry",
@@ -75,7 +94,7 @@ export async function publishLocalRelease(
75
94
  const publishPythonScript = localPublishTaskPath("publish-python.ts");
76
95
  logger.info(`building and publishing Python workspace to ${localPypi.publishUrl}`);
77
96
  publishes.push(
78
- runTaskCommandAsync(root, process.execPath, [
97
+ spawnTaskCommand(root, process.execPath, [
79
98
  publishPythonScript,
80
99
  options.version,
81
100
  "--root",
@@ -93,7 +112,7 @@ export async function publishLocalRelease(
93
112
  return { npm: false, python: false };
94
113
  }
95
114
 
96
- await Promise.all(publishes);
115
+ await awaitLocalPublishers(publishes);
97
116
  logger.success(`published local workspace ${options.version}`);
98
117
  return { npm: Boolean(localRegistry), python: Boolean(localPypi) };
99
118
  }
package/tasks/release.ts CHANGED
@@ -38,6 +38,33 @@ const GithubReleaseRunSchema = z
38
38
  })
39
39
  .passthrough();
40
40
 
41
+ const GithubReleaseStepSchema = z
42
+ .object({
43
+ name: z.string(),
44
+ number: z.number().int(),
45
+ status: z.string(),
46
+ conclusion: z.string().nullable().optional(),
47
+ })
48
+ .passthrough();
49
+
50
+ const GithubReleaseJobSchema = z
51
+ .object({
52
+ databaseId: z.number().int().positive(),
53
+ name: z.string(),
54
+ status: z.string(),
55
+ conclusion: z.string().nullable().optional(),
56
+ steps: z.array(GithubReleaseStepSchema),
57
+ })
58
+ .passthrough();
59
+
60
+ const GithubReleaseRunViewSchema = z
61
+ .object({
62
+ status: z.string(),
63
+ conclusion: z.string().nullable().optional(),
64
+ jobs: z.array(GithubReleaseJobSchema),
65
+ })
66
+ .passthrough();
67
+
41
68
  /** Inputs required to locate and watch one tag-triggered release workflow. */
42
69
  export interface WatchReleaseGithubActionsOptions {
43
70
  readonly account: projectUtils.ProjectGhAccount;
@@ -63,6 +90,61 @@ function delay(milliseconds: number): Promise<void> {
63
90
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
64
91
  }
65
92
 
93
+ interface GithubProgressEntry {
94
+ readonly conclusion?: string | null;
95
+ readonly key: string;
96
+ readonly label: string;
97
+ readonly status: string;
98
+ }
99
+
100
+ function githubProgressEntries(
101
+ run: z.infer<typeof GithubReleaseRunViewSchema>,
102
+ ): GithubProgressEntry[] {
103
+ return [
104
+ {
105
+ key: "run",
106
+ label: "release",
107
+ status: run.status,
108
+ conclusion: run.conclusion,
109
+ },
110
+ ...run.jobs.flatMap((job) => [
111
+ {
112
+ key: `job:${job.databaseId}`,
113
+ label: job.name,
114
+ status: job.status,
115
+ conclusion: job.conclusion,
116
+ },
117
+ ...job.steps.map((step) => ({
118
+ key: `job:${job.databaseId}:step:${step.number}`,
119
+ label: `${job.name} / ${step.name}`,
120
+ status: step.status,
121
+ conclusion: step.conclusion,
122
+ })),
123
+ ]),
124
+ ];
125
+ }
126
+
127
+ function githubProgressMarker(entry: GithubProgressEntry): string {
128
+ if (entry.status === "completed") {
129
+ if (entry.conclusion === "success") return "[done]";
130
+ if (entry.conclusion === "skipped") return "[skip]";
131
+ return "[fail]";
132
+ }
133
+ return entry.status === "in_progress" ? "[run]" : "[wait]";
134
+ }
135
+
136
+ function reportGithubProgress(
137
+ run: z.infer<typeof GithubReleaseRunViewSchema>,
138
+ previous: Map<string, string>,
139
+ ): void {
140
+ for (const entry of githubProgressEntries(run)) {
141
+ const state = `${entry.status}:${entry.conclusion ?? ""}`;
142
+ if (previous.get(entry.key) === state) continue;
143
+ previous.set(entry.key, state);
144
+ logger.info(`${githubProgressMarker(entry)} ${entry.label}`);
145
+ }
146
+ }
147
+
66
148
  /** Whether this release selection has remote work worth watching. */
67
149
  export function shouldWatchReleaseGithubActions(
68
150
  options: Pick<ReleaseSelectionOptions, "publish"> & { readonly watchGithubActions?: boolean },
@@ -134,18 +216,39 @@ export async function watchReleaseGithubActions(
134
216
  url: run.url,
135
217
  });
136
218
  try {
137
- runTaskCommand(
138
- root,
139
- "gh",
140
- ["run", "watch", String(run.databaseId), "--repo", repository, "--interval", "3"],
141
- { env: account.env },
142
- );
143
- runTaskCommand(
144
- root,
145
- "gh",
146
- ["run", "view", String(run.databaseId), "--repo", repository, "--log"],
147
- { env: account.env },
148
- );
219
+ const progress = new Map<string, string>();
220
+ let completed = false;
221
+ let conclusion: string | null | undefined;
222
+ while (!completed) {
223
+ const output = captureTaskCommand(
224
+ root,
225
+ "gh",
226
+ [
227
+ "run",
228
+ "view",
229
+ String(run.databaseId),
230
+ "--repo",
231
+ repository,
232
+ "--json",
233
+ "status,conclusion,jobs",
234
+ ],
235
+ { check: true, env: account.env, stderr: "inherit" },
236
+ );
237
+ const parsed = GithubReleaseRunViewSchema.safeParse(json.parse(output));
238
+ if (!parsed.success) throw new Error("GitHub returned invalid release progress metadata");
239
+ reportGithubProgress(parsed.data, progress);
240
+ completed = parsed.data.status === "completed";
241
+ conclusion = parsed.data.conclusion;
242
+ if (!completed) await delay(pollIntervalMs);
243
+ }
244
+ if (conclusion !== "success" && conclusion !== "skipped") {
245
+ runTaskCommand(
246
+ root,
247
+ "gh",
248
+ ["run", "view", String(run.databaseId), "--repo", repository, "--log-failed"],
249
+ { env: account.env },
250
+ );
251
+ }
149
252
  } catch (error) {
150
253
  logger.warn("GitHub Actions release watch stopped; the remote run was not cancelled", {
151
254
  error: errorUtils.toError(error),
@@ -374,9 +477,10 @@ export async function runRelease(
374
477
  /** Path to complete release notes supplied instead of invoking Genie. */
375
478
  readonly releaseNotesFile?: string;
376
479
  readonly writeReleaseNotes?: typeof writeReleaseNotes;
377
- /** Wait for and stream a matching tag-triggered GitHub Actions run. */
480
+ /** After local release actions, wait for and stream the matching GitHub Actions run. */
378
481
  readonly watchGithubActions?: boolean;
379
482
  readonly githubActionsWatcher?: ReleaseGithubActionsWatcher;
483
+ readonly resolveGithubAccount?: typeof projectUtils.resolveProjectGhAccount;
380
484
  },
381
485
  ): Promise<string> {
382
486
  const { branch, prefix, remote, root } = options;
@@ -480,23 +584,6 @@ export async function runRelease(
480
584
  }
481
585
  logger.success(`released ${tag}`, { sha: head });
482
586
 
483
- if (shouldWatchReleaseGithubActions(options, selection)) {
484
- const account = projectUtils.resolveProjectGhAccount(root, { remote });
485
- if (account) {
486
- await (options.githubActionsWatcher ?? watchReleaseGithubActions)({
487
- account,
488
- head,
489
- root,
490
- tag,
491
- });
492
- } else {
493
- logger.info("skipping GitHub Actions release watch: no authenticated account", {
494
- remote,
495
- tag,
496
- });
497
- }
498
- }
499
-
500
587
  // Restore local deploys: when a non-standard (loopback) npm or uv registry is
501
588
  // configured, publish the freshly tagged version to it directly. Detection and
502
589
  // publishing are owned by local-publish.ts; this no-ops on standard registries.
@@ -518,6 +605,25 @@ export async function runRelease(
518
605
  ]);
519
606
  }
520
607
 
608
+ if (shouldWatchReleaseGithubActions(options, selection)) {
609
+ const account = (options.resolveGithubAccount ?? projectUtils.resolveProjectGhAccount)(root, {
610
+ remote,
611
+ });
612
+ if (account) {
613
+ await (options.githubActionsWatcher ?? watchReleaseGithubActions)({
614
+ account,
615
+ head,
616
+ root,
617
+ tag,
618
+ });
619
+ } else {
620
+ logger.info("skipping GitHub Actions release watch: no authenticated account", {
621
+ remote,
622
+ tag,
623
+ });
624
+ }
625
+ }
626
+
521
627
  return tag;
522
628
  }
523
629
 
@@ -552,7 +658,7 @@ export const ReleaseOptionsSchema = z.object({
552
658
  watchGithubActions: z
553
659
  .boolean()
554
660
  .default(true)
555
- .describe("Wait for and stream the tag-triggered GitHub Actions release"),
661
+ .describe("Wait for and log the tag-triggered GitHub Actions release"),
556
662
  releaseNotes: z
557
663
  .boolean()
558
664
  .default(true)