@fuzdev/fuz_gitops 0.71.0 → 0.72.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.
@@ -20,6 +20,7 @@ export declare const Args: z.ZodObject<{
20
20
  'no-plan': z.ZodDefault<z.ZodBoolean>;
21
21
  max_wait: z.ZodDefault<z.ZodNumber>;
22
22
  skip_install: z.ZodDefault<z.ZodBoolean>;
23
+ emit_json: z.ZodDefault<z.ZodBoolean>;
23
24
  outfile: z.ZodOptional<z.ZodString>;
24
25
  verbose: z.ZodDefault<z.ZodBoolean>;
25
26
  }, z.core.$strict>;
@@ -1 +1 @@
1
- {"version":3,"file":"gitops_publish.task.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/gitops_publish.task.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AActB,cAAc;AACd,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;kBAkCf,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;AAExC,cAAc;AACd,eAAO,MAAM,IAAI,EAAE,IAAI,CAAC,IAAI,CA0F3B,CAAC"}
1
+ {"version":3,"file":"gitops_publish.task.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/gitops_publish.task.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAetB,cAAc;AACd,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;kBAsCf,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;AAExC,cAAc;AACd,eAAO,MAAM,IAAI,EAAE,IAAI,CAAC,IAAI,CA+F3B,CAAC"}
@@ -3,6 +3,7 @@ import { createInterface } from 'node:readline/promises';
3
3
  import { styleText as st } from 'node:util';
4
4
  import { get_gitops_ready } from './gitops_task_helpers.js';
5
5
  import { publish_repos, } from './multi_repo_publisher.js';
6
+ import { stdout_handler } from './publishing_event_handler.js';
6
7
  import { generate_publishing_plan, log_publishing_plan } from './publishing_plan.js';
7
8
  import { format_and_output } from './output_helpers.js';
8
9
  import { GITOPS_CONFIG_PATH_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT } from './gitops_constants.js';
@@ -39,6 +40,10 @@ export const Args = z.strictObject({
39
40
  .boolean()
40
41
  .meta({ description: 'skip npm install after dependency updates' })
41
42
  .default(false),
43
+ emit_json: z
44
+ .boolean()
45
+ .meta({ description: 'stream structured publishing events as JSON-lines to stdout' })
46
+ .default(false),
42
47
  outfile: z.string().meta({ description: 'write output to file instead of logging' }).optional(),
43
48
  verbose: z.boolean().meta({ description: 'show additional details in plan output' }).default(false),
44
49
  });
@@ -47,7 +52,7 @@ export const task = {
47
52
  summary: 'publish all repos in dependency order',
48
53
  Args,
49
54
  run: async ({ args, log }) => {
50
- const { config, dir, peer_strategy, wetrun, format, deploy, plan, max_wait, skip_install, outfile, verbose, } = args;
55
+ const { config, dir, peer_strategy, wetrun, format, deploy, plan, max_wait, skip_install, emit_json, outfile, verbose, } = args;
51
56
  // Load repos
52
57
  const { local_repos: repos } = await get_gitops_ready({
53
58
  config,
@@ -81,6 +86,8 @@ export const task = {
81
86
  max_wait,
82
87
  skip_install,
83
88
  log,
89
+ // Live JSON-lines stream when requested; events also surface on the result.
90
+ events: emit_json ? stdout_handler() : undefined,
84
91
  };
85
92
  // Execute publishing (may throw on fatal errors like circular dependencies)
86
93
  let result;
@@ -97,6 +104,8 @@ export const task = {
97
104
  // Note: FATAL_ERROR is a placeholder - only fatal_error.message is displayed in output
98
105
  failed: [{ name: 'FATAL_ERROR', error: fatal_error }],
99
106
  duration: 0,
107
+ events: [],
108
+ summary: { total: 0, published: 0, failed: 1, skipped: 0, duration: 0 },
100
109
  };
101
110
  }
102
111
  // Format and output result (always runs, even on fatal errors)
@@ -2,6 +2,8 @@ import type { Logger } from '@fuzdev/fuz_util/log.js';
2
2
  import type { LocalRepo } from './local_repo.js';
3
3
  import { type VersionStrategy } from './dependency_updater.js';
4
4
  import type { GitopsOperations } from './operations.js';
5
+ import { type PublishingEvent, type PublishingRunSummary } from './publishing_event.js';
6
+ import { type PublishingEventHandler } from './publishing_event_handler.js';
5
7
  export interface PublishingOptions {
6
8
  wetrun: boolean;
7
9
  update_deps: boolean;
@@ -11,6 +13,8 @@ export interface PublishingOptions {
11
13
  skip_install?: boolean;
12
14
  log?: Logger;
13
15
  ops?: GitopsOperations;
16
+ /** Structured event sink; defaults to capture-only (events surface on the result). */
17
+ events?: PublishingEventHandler;
14
18
  }
15
19
  export interface PublishedVersion {
16
20
  name: string;
@@ -29,6 +33,10 @@ export interface PublishingResult {
29
33
  error: Error;
30
34
  }>;
31
35
  duration: number;
36
+ /** The structured event stream for this run, in emission order. */
37
+ events: Array<PublishingEvent>;
38
+ /** Tallied outcome, derived from `events`. */
39
+ summary: PublishingRunSummary;
32
40
  }
33
41
  export declare const publish_repos: (repos: Array<LocalRepo>, options: PublishingOptions) => Promise<PublishingResult>;
34
42
  //# sourceMappingURL=multi_repo_publisher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"multi_repo_publisher.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/multi_repo_publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAsB,KAAK,eAAe,EAAC,MAAM,yBAAyB,CAAC;AAIlF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iBAAiB,CAAC;AAQtD,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,gBAAgB,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACnC,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAC,CAAC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,aAAa,GACzB,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,SAAS,iBAAiB,KACxB,OAAO,CAAC,gBAAgB,CAqU1B,CAAC"}
1
+ {"version":3,"file":"multi_repo_publisher.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/multi_repo_publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAsB,KAAK,eAAe,EAAC,MAAM,yBAAyB,CAAC;AAIlF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iBAAiB,CAAC;AAOtD,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAEzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,KAAK,sBAAsB,EAG3B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,sFAAsF;IACtF,MAAM,CAAC,EAAE,sBAAsB,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACnC,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAC,CAAC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAC/B,8CAA8C;IAC9C,OAAO,EAAE,oBAAoB,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,GACzB,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,SAAS,iBAAiB,KACxB,OAAO,CAAC,gBAAgB,CA2Z1B,CAAC"}
@@ -7,9 +7,17 @@ import { needs_update, is_breaking_change, detect_bump_type } from './version_ut
7
7
  import { default_gitops_operations } from './operations_defaults.js';
8
8
  import { GITOPS_MAX_ITERATIONS_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT, } from './gitops_constants.js';
9
9
  import { install_with_cache_healing } from './npm_install_helpers.js';
10
+ import { summarize_events, } from './publishing_event.js';
11
+ import { capture_handler, multi_handler, } from './publishing_event_handler.js';
10
12
  export const publish_repos = async (repos, options) => {
11
13
  const start_time = Date.now();
12
14
  const { wetrun, update_deps, log, ops = default_gitops_operations } = options;
15
+ // Capture every event for the result; also forward to the caller's sink if provided.
16
+ const capture = capture_handler();
17
+ const events_handler = options.events ? multi_handler([options.events, capture]) : capture;
18
+ const emit = (event) => {
19
+ events_handler.emit(event);
20
+ };
13
21
  // Preflight checks (skip for dry runs since we're not actually publishing)
14
22
  if (wetrun) {
15
23
  const preflight_options = {
@@ -39,16 +47,19 @@ export const publish_repos = async (repos, options) => {
39
47
  log_cycles: true,
40
48
  log_order: true,
41
49
  });
50
+ emit({ event: 'run_started', wetrun, total: order.length });
42
51
  const published = new Map();
43
52
  const failed = new Map();
44
53
  const changed_repos = new Set(); // Track repos with any changes for selective deployment
54
+ const skipped_packages = new Set(); // dedupe the package_skipped event across iterations
45
55
  // Fixed-point iteration: keep publishing until no new changesets are created
46
56
  // This handles transitive dependency updates (auto-generated changesets)
47
57
  let iteration = 0;
48
58
  let converged = false;
49
59
  while (!converged && iteration < GITOPS_MAX_ITERATIONS_DEFAULT) {
50
60
  iteration++;
51
- log?.info(st('cyan', `\n🚀 Publishing iteration ${iteration}/${GITOPS_MAX_ITERATIONS_DEFAULT}...\n`));
61
+ emit({ event: 'iteration_started', iteration, max: GITOPS_MAX_ITERATIONS_DEFAULT });
62
+ log?.info(st('cyan', `\n🚀 ${wetrun ? 'Publishing' : 'Dry run'} iteration ${iteration}/${GITOPS_MAX_ITERATIONS_DEFAULT}...\n`));
52
63
  // Track if any packages were published in this iteration
53
64
  let published_in_iteration = false;
54
65
  let published_count = 0;
@@ -70,6 +81,7 @@ export const publish_repos = async (repos, options) => {
70
81
  // Failed to check changesets
71
82
  const err = new Error(`Failed to check changesets: ${has_result.message}`);
72
83
  failed.set(pkg_name, err);
84
+ emit({ event: 'package_failed', name: pkg_name, error: err.message, code: 'dependency' });
73
85
  log?.error(st('red', ` ❌ ${err.message}`));
74
86
  break;
75
87
  }
@@ -77,6 +89,11 @@ export const publish_repos = async (repos, options) => {
77
89
  // Skip packages without changesets
78
90
  // In real publish: They might get auto-changesets during dependency updates
79
91
  // In dry run: We can't simulate auto-changesets, so just skip
92
+ // Emit once per package — the loop revisits no-changeset packages each iteration
93
+ if (!skipped_packages.has(pkg_name)) {
94
+ skipped_packages.add(pkg_name);
95
+ emit({ event: 'package_skipped', name: pkg_name, reason: 'no changesets' });
96
+ }
80
97
  if (!wetrun) {
81
98
  // Silent skip in dry run - plan shows which packages get auto-changesets
82
99
  continue;
@@ -88,7 +105,7 @@ export const publish_repos = async (repos, options) => {
88
105
  }
89
106
  try {
90
107
  // 1. Publish this package
91
- log?.info(st('dim', ` [${i + 1}/${order.length}] Publishing ${pkg_name}...`));
108
+ log?.info(st('dim', ` [${i + 1}/${order.length}] ${wetrun ? 'Publishing' : 'Would publish'} ${pkg_name}...`));
92
109
  const version = await publish_single_repo(repo, options, ops);
93
110
  published.set(pkg_name, version);
94
111
  changed_repos.add(pkg_name); // Mark as changed for deployment
@@ -96,7 +113,19 @@ export const publish_repos = async (repos, options) => {
96
113
  // (their dependencies didn't change, only their version)
97
114
  published_in_iteration = true;
98
115
  published_count++;
99
- log?.info(st('green', ` ✅ Published ${pkg_name}@${version.new_version}`));
116
+ emit({
117
+ event: 'package_completed',
118
+ name: pkg_name,
119
+ old_version: version.old_version,
120
+ new_version: version.new_version,
121
+ bump_type: version.bump_type,
122
+ breaking: version.breaking,
123
+ commit: version.commit,
124
+ tag: version.tag,
125
+ });
126
+ log?.info(wetrun
127
+ ? st('green', ` ✅ Published ${pkg_name}@${version.new_version}`)
128
+ : st('cyan', ` ◇ Would publish ${pkg_name}@${version.new_version}`));
100
129
  if (wetrun) {
101
130
  // 2. Wait for this package to be available on NPM
102
131
  log?.info(` ⏳ Waiting for ${pkg_name}@${version.new_version} on NPM...`);
@@ -112,7 +141,14 @@ export const publish_repos = async (repos, options) => {
112
141
  log,
113
142
  });
114
143
  if (!wait_result.ok) {
115
- throw new Error(`Failed to wait for package: ${wait_result.message}${wait_result.timeout ? ' (timeout)' : ''}`);
144
+ // Handle inline (don't throw into the generic catch): the npm-wait failure
145
+ // carries a typed `timeout` signal, so we know this is a network failure
146
+ // without sniffing the message.
147
+ const err = new Error(`Failed to wait for package: ${wait_result.message}${wait_result.timeout ? ' (timeout)' : ''}`);
148
+ failed.set(pkg_name, err);
149
+ emit({ event: 'package_failed', name: pkg_name, error: err.message, code: 'network' });
150
+ log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
151
+ break; // fail fast
116
152
  }
117
153
  // 3. Update all repos that have prod/peer deps on this package
118
154
  if (update_deps) {
@@ -135,6 +171,12 @@ export const publish_repos = async (repos, options) => {
135
171
  // Apply updates if any
136
172
  if (updates.size > 0) {
137
173
  log?.info(` Updating ${dependent_repo.library.name}'s dependency on ${pkg_name}`);
174
+ emit({
175
+ event: 'dependency_updated',
176
+ dependent: dependent_repo.library.name,
177
+ dependency: pkg_name,
178
+ version: version.new_version,
179
+ });
138
180
  changed_repos.add(dependent_repo.library.name); // Mark as changed for deployment
139
181
  changed_in_iteration.add(dependent_repo.library.name); // Track for batch install
140
182
  await update_package_json(dependent_repo, updates, {
@@ -151,6 +193,15 @@ export const publish_repos = async (repos, options) => {
151
193
  catch (error) {
152
194
  const err = error instanceof Error ? error : new Error(String(error));
153
195
  failed.set(pkg_name, err);
196
+ emit({
197
+ event: 'package_failed',
198
+ name: pkg_name,
199
+ error: err.message,
200
+ // TODO: emit a precise code once the npm/process ops return typed errors —
201
+ // today a publish-step cause lives in unstructured stderr, so use the honest
202
+ // coarse bucket rather than guessing 'auth'/'network'/'build' from the message.
203
+ code: 'publish',
204
+ });
154
205
  log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
155
206
  break; // Always fail fast on error
156
207
  }
@@ -159,31 +210,26 @@ export const publish_repos = async (repos, options) => {
159
210
  // This ensures workspace stays consistent before next iteration
160
211
  if (wetrun && !options.skip_install && changed_in_iteration.size > 0) {
161
212
  log?.info(st('cyan', '\n📦 Installing dependencies for updated repos...\n'));
162
- for (const pkg_name of changed_in_iteration) {
163
- const repo = repos.find((r) => r.library.name === pkg_name);
164
- if (!repo)
165
- continue;
166
- try {
167
- log?.info(` Installing ${pkg_name}...`);
168
- await install_with_cache_healing(repo, ops, log);
169
- log?.info(st('green', ` ✅ Installed ${pkg_name}`));
170
- }
171
- catch (error) {
172
- const err = error instanceof Error ? error : new Error(String(error));
173
- failed.set(pkg_name, err);
174
- log?.error(st('red', ` ❌ Failed to install ${pkg_name}: ${err.message}`));
175
- // Continue with other installs instead of breaking
176
- }
213
+ for (const [name, err] of await install_repos(changed_in_iteration, repos, ops, emit, log)) {
214
+ failed.set(name, err);
177
215
  }
178
216
  }
179
217
  // Log iteration summary
180
218
  if (published_count > 0) {
181
- log?.info(st('dim', `\nIteration ${iteration}: ${published_count} package(s) published\n`));
219
+ log?.info(st('dim', `\nIteration ${iteration}: ${published_count} package(s) ${wetrun ? 'published' : 'would be published'}\n`));
182
220
  }
221
+ emit({
222
+ event: 'iteration_finished',
223
+ iteration,
224
+ published_count,
225
+ converged: !published_in_iteration,
226
+ });
183
227
  // Check for convergence: no packages published in this iteration
184
228
  if (!published_in_iteration) {
185
229
  converged = true;
186
- log?.info(st('green', `\n✓ Converged after ${iteration} iteration(s) - no new changesets\n`));
230
+ log?.info(st('green', wetrun
231
+ ? `\n✓ Converged after ${iteration} iteration(s) - no new changesets\n`
232
+ : `\n✓ Dry run complete after ${iteration} iteration(s)\n`));
187
233
  }
188
234
  else if (iteration === GITOPS_MAX_ITERATIONS_DEFAULT) {
189
235
  // Count packages that still have changesets (not yet published)
@@ -212,6 +258,14 @@ export const publish_repos = async (repos, options) => {
212
258
  }
213
259
  if (dev_updates.size > 0) {
214
260
  log?.info(` Updating ${dev_updates.size} dev dependencies in ${repo.library.name}`);
261
+ for (const [dep_name, dep_version] of dev_updates) {
262
+ emit({
263
+ event: 'dependency_updated',
264
+ dependent: repo.library.name,
265
+ dependency: dep_name,
266
+ version: dep_version,
267
+ });
268
+ }
215
269
  changed_repos.add(repo.library.name); // Mark as changed for deployment
216
270
  dev_updated_repos.add(repo.library.name); // Track for batch install
217
271
  await update_package_json(repo, dev_updates, {
@@ -226,21 +280,8 @@ export const publish_repos = async (repos, options) => {
226
280
  // Phase 2b: Install dev dependencies for repos with dev dep updates
227
281
  if (wetrun && !options.skip_install && dev_updated_repos.size > 0) {
228
282
  log?.info(st('cyan', '\n📦 Installing dev dependencies for updated repos...\n'));
229
- for (const pkg_name of dev_updated_repos) {
230
- const repo = repos.find((r) => r.library.name === pkg_name);
231
- if (!repo)
232
- continue;
233
- try {
234
- log?.info(` Installing ${pkg_name}...`);
235
- await install_with_cache_healing(repo, ops, log);
236
- log?.info(st('green', ` ✅ Installed ${pkg_name}`));
237
- }
238
- catch (error) {
239
- const err = error instanceof Error ? error : new Error(String(error));
240
- failed.set(pkg_name, err);
241
- log?.error(st('red', ` ❌ Failed to install ${pkg_name}: ${err.message}`));
242
- // Continue with other installs instead of breaking
243
- }
283
+ for (const [name, err] of await install_repos(dev_updated_repos, repos, ops, emit, log)) {
284
+ failed.set(name, err);
244
285
  }
245
286
  }
246
287
  // Phase 3: Deploy repos with changes (optional)
@@ -250,6 +291,7 @@ export const publish_repos = async (repos, options) => {
250
291
  log?.info(st('cyan', `\n🚢 Deploying ${repos_to_deploy.length}/${repos.length} repos with changes...\n`));
251
292
  for (const repo of repos_to_deploy) {
252
293
  try {
294
+ emit({ event: 'deploy_started', name: repo.library.name });
253
295
  log?.info(` Deploying ${repo.library.name}...`);
254
296
  const deploy_result = await ops.process.spawn({
255
297
  cmd: 'gro',
@@ -257,37 +299,49 @@ export const publish_repos = async (repos, options) => {
257
299
  cwd: repo.repo_dir,
258
300
  });
259
301
  if (deploy_result.ok) {
302
+ emit({ event: 'deploy_completed', name: repo.library.name });
260
303
  log?.info(st('green', ` ✅ Deployed ${repo.library.name}`));
261
304
  }
262
305
  else {
306
+ emit({ event: 'deploy_failed', name: repo.library.name, error: deploy_result.message });
263
307
  log?.warn(st('yellow', ` ⚠️ Failed to deploy ${repo.library.name}`));
264
308
  }
265
309
  }
266
310
  catch (error) {
267
- log?.error(st('red', ` ❌ Error deploying ${repo.library.name}: ${error}`));
311
+ const err = error instanceof Error ? error : new Error(String(error));
312
+ emit({ event: 'deploy_failed', name: repo.library.name, error: err.message });
313
+ log?.error(st('red', ` ❌ Error deploying ${repo.library.name}: ${err.message}`));
268
314
  }
269
315
  }
270
316
  }
271
317
  // Summary
272
318
  const duration = Date.now() - start_time;
273
319
  const ok = failed.size === 0;
274
- log?.info(st('cyan', '\n📋 Publishing Summary\n'));
320
+ const summary = summarize_events(capture.events, duration);
321
+ log?.info(st('cyan', `\n📋 ${wetrun ? 'Publishing' : 'Dry Run'} Summary\n`));
275
322
  log?.info(` Duration: ${(duration / 1000).toFixed(1)}s`);
276
- log?.info(` Published: ${published.size} packages`);
323
+ log?.info(` ${wetrun ? 'Published' : 'Would publish'}: ${published.size} packages`);
277
324
  if (failed.size > 0) {
278
325
  log?.info(` Failed: ${failed.size} packages`);
279
326
  }
280
327
  if (ok) {
281
- log?.info(st('green', '\n✨ All packages published successfully!\n'));
328
+ log?.info(st('green', wetrun
329
+ ? '\n✨ All packages published successfully!\n'
330
+ : `\n✨ Dry run complete — ${published.size} package(s) would be published. Re-run with --wetrun to publish.\n`));
282
331
  }
283
332
  else {
284
- log?.error(st('red', '\n❌ Some packages failed to publish\n'));
333
+ log?.error(st('red', wetrun
334
+ ? '\n❌ Some packages failed to publish\n'
335
+ : '\n❌ Some packages failed during dry run\n'));
285
336
  }
337
+ emit({ event: 'run_finished', summary });
286
338
  return {
287
339
  ok,
288
340
  published: Array.from(published.values()),
289
341
  failed: Array.from(failed.entries()).map(([name, error]) => ({ name, error })),
290
342
  duration,
343
+ events: capture.events,
344
+ summary,
291
345
  };
292
346
  };
293
347
  /**
@@ -361,3 +415,31 @@ const publish_single_repo = async (repo, options, ops = default_gitops_operation
361
415
  tag: `v${new_version}`,
362
416
  };
363
417
  };
418
+ /**
419
+ * Installs dependencies for each named repo (with cache healing), emitting install
420
+ * events and logging progress. A failed install doesn't stop the batch — failures are
421
+ * collected and returned so the caller can fold them into the run's failures.
422
+ */
423
+ const install_repos = async (names, repos, ops, emit, log) => {
424
+ const failures = new Map();
425
+ for (const name of names) {
426
+ const repo = repos.find((r) => r.library.name === name);
427
+ if (!repo)
428
+ continue;
429
+ try {
430
+ emit({ event: 'install_started', name });
431
+ log?.info(` Installing ${name}...`);
432
+ await install_with_cache_healing(repo, ops, log);
433
+ emit({ event: 'install_completed', name });
434
+ log?.info(st('green', ` ✅ Installed ${name}`));
435
+ }
436
+ catch (error) {
437
+ const err = error instanceof Error ? error : new Error(String(error));
438
+ failures.set(name, err);
439
+ emit({ event: 'install_failed', name, error: err.message });
440
+ log?.error(st('red', ` ❌ Failed to install ${name}: ${err.message}`));
441
+ // continue with other installs instead of breaking
442
+ }
443
+ }
444
+ return failures;
445
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Structured events for multi-repo publishing.
3
+ *
4
+ * Publishing emits a stream of tagged events alongside its human-readable logging,
5
+ * so machine consumers (CI, dashboards) can follow a run step by step. Every run
6
+ * opens with a `run_started` event carrying `wetrun`: when `false`, the run is a dry
7
+ * run and every `package_completed` is a prediction (its `commit` is `'simulated'`)
8
+ * rather than an applied change. A run's `run_finished` summary is derived from the
9
+ * same event list via `summarize_events`, so the stream and the summary never drift.
10
+ *
11
+ * Events are consumed through a `PublishingEventHandler` sink (see
12
+ * `publishing_event_handler.ts`).
13
+ *
14
+ * @module
15
+ */
16
+ import { z } from 'zod';
17
+ /**
18
+ * Coarse triage classification for a failed package. Lets consumers branch on
19
+ * failure kind without parsing the message.
20
+ */
21
+ export declare const PublishingErrorCode: z.ZodEnum<{
22
+ other: "other";
23
+ build: "build";
24
+ publish: "publish";
25
+ network: "network";
26
+ auth: "auth";
27
+ dependency: "dependency";
28
+ }>;
29
+ export type PublishingErrorCode = z.infer<typeof PublishingErrorCode>;
30
+ /** Tallied outcome of a publishing run, derived from its events via `summarize_events`. */
31
+ export declare const PublishingRunSummary: z.ZodObject<{
32
+ total: z.ZodNumber;
33
+ published: z.ZodNumber;
34
+ failed: z.ZodNumber;
35
+ skipped: z.ZodNumber;
36
+ duration: z.ZodNumber;
37
+ }, z.core.$strict>;
38
+ export type PublishingRunSummary = z.infer<typeof PublishingRunSummary>;
39
+ /**
40
+ * A single structured event emitted during a publishing run. Tagged on `event` so the
41
+ * union serializes as one self-describing JSON object per event (JSON-lines on the wire).
42
+ */
43
+ export declare const PublishingEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
+ event: z.ZodLiteral<"run_started">;
45
+ wetrun: z.ZodBoolean;
46
+ total: z.ZodNumber;
47
+ }, z.core.$strict>, z.ZodObject<{
48
+ event: z.ZodLiteral<"iteration_started">;
49
+ iteration: z.ZodNumber;
50
+ max: z.ZodNumber;
51
+ }, z.core.$strict>, z.ZodObject<{
52
+ event: z.ZodLiteral<"iteration_finished">;
53
+ iteration: z.ZodNumber;
54
+ published_count: z.ZodNumber;
55
+ converged: z.ZodBoolean;
56
+ }, z.core.$strict>, z.ZodObject<{
57
+ event: z.ZodLiteral<"package_skipped">;
58
+ name: z.ZodString;
59
+ reason: z.ZodString;
60
+ }, z.core.$strict>, z.ZodObject<{
61
+ event: z.ZodLiteral<"package_completed">;
62
+ name: z.ZodString;
63
+ old_version: z.ZodString;
64
+ new_version: z.ZodString;
65
+ bump_type: z.ZodEnum<{
66
+ major: "major";
67
+ minor: "minor";
68
+ patch: "patch";
69
+ }>;
70
+ breaking: z.ZodBoolean;
71
+ commit: z.ZodString;
72
+ tag: z.ZodString;
73
+ }, z.core.$strict>, z.ZodObject<{
74
+ event: z.ZodLiteral<"package_failed">;
75
+ name: z.ZodString;
76
+ error: z.ZodString;
77
+ code: z.ZodEnum<{
78
+ other: "other";
79
+ build: "build";
80
+ publish: "publish";
81
+ network: "network";
82
+ auth: "auth";
83
+ dependency: "dependency";
84
+ }>;
85
+ }, z.core.$strict>, z.ZodObject<{
86
+ event: z.ZodLiteral<"dependency_updated">;
87
+ dependent: z.ZodString;
88
+ dependency: z.ZodString;
89
+ version: z.ZodString;
90
+ }, z.core.$strict>, z.ZodObject<{
91
+ event: z.ZodLiteral<"install_started">;
92
+ name: z.ZodString;
93
+ }, z.core.$strict>, z.ZodObject<{
94
+ event: z.ZodLiteral<"install_completed">;
95
+ name: z.ZodString;
96
+ }, z.core.$strict>, z.ZodObject<{
97
+ event: z.ZodLiteral<"install_failed">;
98
+ name: z.ZodString;
99
+ error: z.ZodString;
100
+ }, z.core.$strict>, z.ZodObject<{
101
+ event: z.ZodLiteral<"deploy_started">;
102
+ name: z.ZodString;
103
+ }, z.core.$strict>, z.ZodObject<{
104
+ event: z.ZodLiteral<"deploy_completed">;
105
+ name: z.ZodString;
106
+ }, z.core.$strict>, z.ZodObject<{
107
+ event: z.ZodLiteral<"deploy_failed">;
108
+ name: z.ZodString;
109
+ error: z.ZodString;
110
+ }, z.core.$strict>, z.ZodObject<{
111
+ event: z.ZodLiteral<"run_finished">;
112
+ summary: z.ZodObject<{
113
+ total: z.ZodNumber;
114
+ published: z.ZodNumber;
115
+ failed: z.ZodNumber;
116
+ skipped: z.ZodNumber;
117
+ duration: z.ZodNumber;
118
+ }, z.core.$strict>;
119
+ }, z.core.$strict>], "event">;
120
+ export type PublishingEvent = z.infer<typeof PublishingEvent>;
121
+ /**
122
+ * Derives a run summary from the captured event list — the single canonical path from
123
+ * events to summary, so the `run_finished` summary always agrees with the stream.
124
+ * Call before emitting `run_finished` (which is not itself counted).
125
+ *
126
+ * @param events - the events captured so far this run
127
+ * @param duration - wall-clock duration in milliseconds
128
+ */
129
+ export declare const summarize_events: (events: Array<PublishingEvent>, duration: number) => PublishingRunSummary;
130
+ //# sourceMappingURL=publishing_event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publishing_event.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/publishing_event.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;EAO9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB;;;;;;kBAM/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BA2E1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAC5B,QAAQ,KAAK,CAAC,eAAe,CAAC,EAC9B,UAAU,MAAM,KACd,oBAwBF,CAAC"}