@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.
@@ -0,0 +1,149 @@
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 const PublishingErrorCode = z.enum([
22
+ 'publish',
23
+ 'network',
24
+ 'auth',
25
+ 'dependency',
26
+ 'build',
27
+ 'other',
28
+ ]);
29
+ /** Tallied outcome of a publishing run, derived from its events via `summarize_events`. */
30
+ export const PublishingRunSummary = z.strictObject({
31
+ total: z.number().meta({ description: 'packages in the publishing order (the candidate set)' }),
32
+ published: z.number().meta({ description: 'packages published (or, in a dry run, predicted)' }),
33
+ failed: z.number(),
34
+ skipped: z.number(),
35
+ duration: z.number().meta({ description: 'wall-clock duration in milliseconds' }),
36
+ });
37
+ /**
38
+ * A single structured event emitted during a publishing run. Tagged on `event` so the
39
+ * union serializes as one self-describing JSON object per event (JSON-lines on the wire).
40
+ */
41
+ export const PublishingEvent = z.discriminatedUnion('event', [
42
+ z.strictObject({
43
+ event: z.literal('run_started'),
44
+ wetrun: z.boolean().meta({ description: 'false means every package_completed is a prediction' }),
45
+ total: z.number(),
46
+ }),
47
+ z.strictObject({
48
+ event: z.literal('iteration_started'),
49
+ iteration: z.number(),
50
+ max: z.number(),
51
+ }),
52
+ z.strictObject({
53
+ event: z.literal('iteration_finished'),
54
+ iteration: z.number(),
55
+ published_count: z.number(),
56
+ converged: z.boolean(),
57
+ }),
58
+ z.strictObject({
59
+ event: z.literal('package_skipped'),
60
+ name: z.string(),
61
+ reason: z.string(),
62
+ }),
63
+ z.strictObject({
64
+ event: z.literal('package_completed'),
65
+ name: z.string(),
66
+ old_version: z.string(),
67
+ new_version: z.string(),
68
+ // mirrors `BumpType` from `semver.ts`; inline so the event schema is self-contained
69
+ bump_type: z.enum(['major', 'minor', 'patch']),
70
+ breaking: z.boolean(),
71
+ commit: z.string().meta({ description: "'simulated' in a dry run, otherwise the commit hash" }),
72
+ tag: z.string(),
73
+ }),
74
+ z.strictObject({
75
+ event: z.literal('package_failed'),
76
+ name: z.string(),
77
+ error: z.string(),
78
+ code: PublishingErrorCode,
79
+ }),
80
+ z.strictObject({
81
+ event: z.literal('dependency_updated'),
82
+ dependent: z.string(),
83
+ dependency: z.string(),
84
+ version: z.string(),
85
+ }),
86
+ z.strictObject({
87
+ event: z.literal('install_started'),
88
+ name: z.string(),
89
+ }),
90
+ z.strictObject({
91
+ event: z.literal('install_completed'),
92
+ name: z.string(),
93
+ }),
94
+ z.strictObject({
95
+ event: z.literal('install_failed'),
96
+ name: z.string(),
97
+ error: z.string(),
98
+ }),
99
+ z.strictObject({
100
+ event: z.literal('deploy_started'),
101
+ name: z.string(),
102
+ }),
103
+ z.strictObject({
104
+ event: z.literal('deploy_completed'),
105
+ name: z.string(),
106
+ }),
107
+ z.strictObject({
108
+ event: z.literal('deploy_failed'),
109
+ name: z.string(),
110
+ error: z.string(),
111
+ }),
112
+ z.strictObject({
113
+ event: z.literal('run_finished'),
114
+ summary: PublishingRunSummary,
115
+ }),
116
+ ]);
117
+ /**
118
+ * Derives a run summary from the captured event list — the single canonical path from
119
+ * events to summary, so the `run_finished` summary always agrees with the stream.
120
+ * Call before emitting `run_finished` (which is not itself counted).
121
+ *
122
+ * @param events - the events captured so far this run
123
+ * @param duration - wall-clock duration in milliseconds
124
+ */
125
+ export const summarize_events = (events, duration) => {
126
+ let total = 0;
127
+ let published = 0;
128
+ let failed = 0;
129
+ let skipped = 0;
130
+ for (const event of events) {
131
+ switch (event.event) {
132
+ case 'run_started':
133
+ total = event.total;
134
+ break;
135
+ case 'package_completed':
136
+ published++;
137
+ break;
138
+ case 'package_failed':
139
+ failed++;
140
+ break;
141
+ case 'package_skipped':
142
+ skipped++;
143
+ break;
144
+ default:
145
+ break;
146
+ }
147
+ }
148
+ return { total, published, failed, skipped, duration };
149
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Composable sinks for the publishing event stream.
3
+ *
4
+ * A `PublishingEventHandler` is anything that can receive a `PublishingEvent`. Handlers
5
+ * compose: `multi_handler` fans out, `masking_handler` redacts secrets then forwards.
6
+ * Emission is best-effort and synchronous — an observability sink must never fail or
7
+ * slow a run. The default sink is `null_handler` (drops everything).
8
+ *
9
+ * @module
10
+ */
11
+ import type { PublishingEvent } from './publishing_event.js';
12
+ /** A sink for publishing events. */
13
+ export interface PublishingEventHandler {
14
+ emit: (event: PublishingEvent) => void;
15
+ }
16
+ /** A `capture_handler` also exposes the events it has collected. */
17
+ export interface CapturingEventHandler extends PublishingEventHandler {
18
+ readonly events: Array<PublishingEvent>;
19
+ }
20
+ /** Drops every event. The default when no handler is supplied. */
21
+ export declare const null_handler: () => PublishingEventHandler;
22
+ /** Collects events in memory. Used to build the run report and in tests. */
23
+ export declare const capture_handler: () => CapturingEventHandler;
24
+ /**
25
+ * Writes each event as one JSON object per line (JSON-lines) to `process.stdout`.
26
+ * Write failures are swallowed — the stream is observability, not control flow.
27
+ */
28
+ export declare const stdout_handler: () => PublishingEventHandler;
29
+ /** Fans an event out to every handler in order. */
30
+ export declare const multi_handler: (handlers: Array<PublishingEventHandler>) => PublishingEventHandler;
31
+ /**
32
+ * Wraps a handler, masking secrets in each event's string fields before forwarding.
33
+ *
34
+ * @param inner - the handler to forward masked events to
35
+ * @param mask - the masking function, defaults to `mask_secrets`
36
+ */
37
+ export declare const masking_handler: (inner: PublishingEventHandler, mask?: (event: PublishingEvent) => PublishingEvent) => PublishingEventHandler;
38
+ /** Redacts known secret shapes from a string. */
39
+ export declare const redact_secrets: (text: string) => string;
40
+ /** Returns a copy of the event with secrets redacted from its string-valued fields. */
41
+ export declare const mask_secrets: (event: PublishingEvent) => PublishingEvent;
42
+ //# sourceMappingURL=publishing_event_handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publishing_event_handler.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/publishing_event_handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAE3D,oCAAoC;AACpC,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CACvC;AAED,oEAAoE;AACpE,MAAM,WAAW,qBAAsB,SAAQ,sBAAsB;IACpE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;CACxC;AAED,kEAAkE;AAClE,eAAO,MAAM,YAAY,QAAO,sBAE9B,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAO,qBAQlC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAO,sBAQhC,CAAC;AAEH,mDAAmD;AACnD,eAAO,MAAM,aAAa,GAAI,UAAU,KAAK,CAAC,sBAAsB,CAAC,KAAG,sBAMtE,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAC3B,OAAO,sBAAsB,EAC7B,OAAM,CAAC,KAAK,EAAE,eAAe,KAAK,eAA8B,KAC9D,sBAID,CAAC;AAWH,iDAAiD;AACjD,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,MACgD,CAAC;AAE/F,uFAAuF;AACvF,eAAO,MAAM,YAAY,GAAI,OAAO,eAAe,KAAG,eAMrD,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Composable sinks for the publishing event stream.
3
+ *
4
+ * A `PublishingEventHandler` is anything that can receive a `PublishingEvent`. Handlers
5
+ * compose: `multi_handler` fans out, `masking_handler` redacts secrets then forwards.
6
+ * Emission is best-effort and synchronous — an observability sink must never fail or
7
+ * slow a run. The default sink is `null_handler` (drops everything).
8
+ *
9
+ * @module
10
+ */
11
+ /** Drops every event. The default when no handler is supplied. */
12
+ export const null_handler = () => ({
13
+ emit: () => { },
14
+ });
15
+ /** Collects events in memory. Used to build the run report and in tests. */
16
+ export const capture_handler = () => {
17
+ const events = [];
18
+ return {
19
+ events,
20
+ emit: (event) => {
21
+ events.push(event);
22
+ },
23
+ };
24
+ };
25
+ /**
26
+ * Writes each event as one JSON object per line (JSON-lines) to `process.stdout`.
27
+ * Write failures are swallowed — the stream is observability, not control flow.
28
+ */
29
+ export const stdout_handler = () => ({
30
+ emit: (event) => {
31
+ try {
32
+ process.stdout.write(JSON.stringify(event) + '\n');
33
+ }
34
+ catch {
35
+ // best-effort: a logging sink must never fail a run
36
+ }
37
+ },
38
+ });
39
+ /** Fans an event out to every handler in order. */
40
+ export const multi_handler = (handlers) => ({
41
+ emit: (event) => {
42
+ for (const handler of handlers) {
43
+ handler.emit(event);
44
+ }
45
+ },
46
+ });
47
+ /**
48
+ * Wraps a handler, masking secrets in each event's string fields before forwarding.
49
+ *
50
+ * @param inner - the handler to forward masked events to
51
+ * @param mask - the masking function, defaults to `mask_secrets`
52
+ */
53
+ export const masking_handler = (inner, mask = mask_secrets) => ({
54
+ emit: (event) => {
55
+ inner.emit(mask(event));
56
+ },
57
+ });
58
+ // Minimal redaction rules: npm auth tokens (bare or registry-scoped), `SECRET_*`
59
+ // env-style assignments, and `npm_`-prefixed tokens. Deliberately lean — error
60
+ // strings can carry npm/git output; a fuller secret catalog is deferred.
61
+ const SECRET_RULES = [
62
+ [/((?:\/\/[^\s:]+:)?_authToken\s*=\s*)\S+/gi, '$1[redacted]'],
63
+ [/(SECRET_[A-Z0-9_]+\s*[=:]\s*)\S+/g, '$1[redacted]'],
64
+ [/(npm_[A-Za-z0-9]{4})[A-Za-z0-9]{12,}/g, '$1[redacted]'],
65
+ ];
66
+ /** Redacts known secret shapes from a string. */
67
+ export const redact_secrets = (text) => SECRET_RULES.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), text);
68
+ /** Returns a copy of the event with secrets redacted from its string-valued fields. */
69
+ export const mask_secrets = (event) => {
70
+ const masked = {};
71
+ for (const [key, value] of Object.entries(event)) {
72
+ masked[key] = typeof value === 'string' ? redact_secrets(value) : value;
73
+ }
74
+ return masked;
75
+ };
@@ -35,7 +35,7 @@ export interface RepoPath {
35
35
  * Get repo paths from gitops config without full git sync.
36
36
  * Lighter weight than `get_gitops_ready()` - just resolves paths.
37
37
  *
38
- * @param config_path - path to `gitops.config.ts` (defaults to `./gitops.config.ts`)
38
+ * @param config_path - path to the gitops config file (defaults to `gitops.config.ts`)
39
39
  * @returns array of repo info with name, path, and url
40
40
  */
41
41
  export declare const get_repo_paths: (config_path?: string) => Promise<Array<RepoPath>>;
package/dist/repo_ops.js CHANGED
@@ -59,7 +59,7 @@ export const DEFAULT_EXCLUDE_EXTENSIONS = [
59
59
  * Get repo paths from gitops config without full git sync.
60
60
  * Lighter weight than `get_gitops_ready()` - just resolves paths.
61
61
  *
62
- * @param config_path - path to `gitops.config.ts` (defaults to `./gitops.config.ts`)
62
+ * @param config_path - path to the gitops config file (defaults to `gitops.config.ts`)
63
63
  * @returns array of repo info with name, path, and url
64
64
  */
65
65
  export const get_repo_paths = async (config_path) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_gitops",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "description": "a tool for managing many repos",
5
5
  "glyph": "🪄",
6
6
  "logo": "logo.svg",
@@ -31,7 +31,7 @@
31
31
  "peerDependencies": {
32
32
  "@fuzdev/fuz_css": ">=0.61.0",
33
33
  "@fuzdev/fuz_ui": ">=0.198.0",
34
- "@fuzdev/fuz_util": ">=0.63.0",
34
+ "@fuzdev/fuz_util": ">=0.63.1",
35
35
  "@fuzdev/gro": ">=0.200.0",
36
36
  "@sveltejs/kit": "^2",
37
37
  "svelte": "^5",
@@ -43,7 +43,7 @@
43
43
  "@fuzdev/fuz_code": "^0.45.1",
44
44
  "@fuzdev/fuz_css": "^0.61.1",
45
45
  "@fuzdev/fuz_ui": "^0.198.0",
46
- "@fuzdev/fuz_util": "^0.63.0",
46
+ "@fuzdev/fuz_util": "^0.63.1",
47
47
  "@fuzdev/gro": "^0.200.0",
48
48
  "@jridgewell/trace-mapping": "^0.3.31",
49
49
  "@ryanatkn/eslint-config": "^0.12.1",
@@ -9,6 +9,7 @@ import {
9
9
  type PublishingOptions,
10
10
  type PublishingResult,
11
11
  } from './multi_repo_publisher.js';
12
+ import {stdout_handler} from './publishing_event_handler.js';
12
13
  import {generate_publishing_plan, log_publishing_plan} from './publishing_plan.js';
13
14
  import {format_and_output, type OutputFormatters} from './output_helpers.js';
14
15
  import {GITOPS_CONFIG_PATH_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT} from './gitops_constants.js';
@@ -46,6 +47,10 @@ export const Args = z.strictObject({
46
47
  .boolean()
47
48
  .meta({description: 'skip npm install after dependency updates'})
48
49
  .default(false),
50
+ emit_json: z
51
+ .boolean()
52
+ .meta({description: 'stream structured publishing events as JSON-lines to stdout'})
53
+ .default(false),
49
54
  outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
50
55
  verbose: z.boolean().meta({description: 'show additional details in plan output'}).default(false),
51
56
  });
@@ -66,6 +71,7 @@ export const task: Task<Args> = {
66
71
  plan,
67
72
  max_wait,
68
73
  skip_install,
74
+ emit_json,
69
75
  outfile,
70
76
  verbose,
71
77
  } = args;
@@ -107,6 +113,8 @@ export const task: Task<Args> = {
107
113
  max_wait,
108
114
  skip_install,
109
115
  log,
116
+ // Live JSON-lines stream when requested; events also surface on the result.
117
+ events: emit_json ? stdout_handler() : undefined,
110
118
  };
111
119
 
112
120
  // Execute publishing (may throw on fatal errors like circular dependencies)
@@ -124,6 +132,8 @@ export const task: Task<Args> = {
124
132
  // Note: FATAL_ERROR is a placeholder - only fatal_error.message is displayed in output
125
133
  failed: [{name: 'FATAL_ERROR', error: fatal_error}],
126
134
  duration: 0,
135
+ events: [],
136
+ summary: {total: 0, published: 0, failed: 1, skipped: 0, duration: 0},
127
137
  };
128
138
  }
129
139