@fuzdev/fuz_gitops 0.71.0 → 0.73.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 +13 -6
- package/dist/changeset_generator.d.ts.map +1 -1
- package/dist/changeset_generator.js +2 -16
- package/dist/changeset_reader.d.ts +1 -1
- package/dist/changeset_reader.d.ts.map +1 -1
- package/dist/dependency_graph.js +1 -1
- package/dist/git_operations.d.ts +5 -0
- package/dist/git_operations.d.ts.map +1 -1
- package/dist/git_operations.js +5 -0
- package/dist/gitops_analyze.task.d.ts +1 -0
- package/dist/gitops_analyze.task.d.ts.map +1 -1
- package/dist/gitops_analyze.task.js +12 -17
- package/dist/gitops_plan.task.d.ts +1 -0
- package/dist/gitops_plan.task.d.ts.map +1 -1
- package/dist/gitops_plan.task.js +9 -2
- package/dist/gitops_publish.task.d.ts +3 -2
- package/dist/gitops_publish.task.d.ts.map +1 -1
- package/dist/gitops_publish.task.js +90 -26
- package/dist/gitops_run.task.d.ts +2 -1
- package/dist/gitops_run.task.d.ts.map +1 -1
- package/dist/gitops_run.task.js +40 -7
- package/dist/gitops_sync.task.d.ts +1 -0
- package/dist/gitops_sync.task.d.ts.map +1 -1
- package/dist/gitops_sync.task.js +16 -2
- package/dist/gitops_task_helpers.d.ts +16 -2
- package/dist/gitops_task_helpers.d.ts.map +1 -1
- package/dist/gitops_task_helpers.js +11 -3
- package/dist/gitops_validate.task.d.ts +1 -0
- package/dist/gitops_validate.task.d.ts.map +1 -1
- package/dist/gitops_validate.task.js +20 -20
- package/dist/graph_validation.d.ts +16 -1
- package/dist/graph_validation.d.ts.map +1 -1
- package/dist/graph_validation.js +15 -0
- package/dist/local_repo.d.ts +22 -10
- package/dist/local_repo.d.ts.map +1 -1
- package/dist/local_repo.js +92 -75
- package/dist/log_helpers.d.ts +0 -5
- package/dist/log_helpers.d.ts.map +1 -1
- package/dist/log_helpers.js +8 -19
- package/dist/multi_repo_publisher.d.ts +29 -2
- package/dist/multi_repo_publisher.d.ts.map +1 -1
- package/dist/multi_repo_publisher.js +307 -233
- package/dist/operations.d.ts +6 -26
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations_defaults.d.ts.map +1 -1
- package/dist/operations_defaults.js +1 -19
- package/dist/output_helpers.d.ts.map +1 -1
- package/dist/output_helpers.js +7 -6
- package/dist/paths.d.ts +0 -4
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +0 -4
- package/dist/publish_gate.d.ts +44 -0
- package/dist/publish_gate.d.ts.map +1 -0
- package/dist/publish_gate.js +32 -0
- package/dist/publish_steps.d.ts +60 -0
- package/dist/publish_steps.d.ts.map +1 -0
- package/dist/publish_steps.js +113 -0
- package/dist/publishing_event.d.ts +123 -0
- package/dist/publishing_event.d.ts.map +1 -0
- package/dist/publishing_event.js +140 -0
- package/dist/publishing_event_handler.d.ts +42 -0
- package/dist/publishing_event_handler.d.ts.map +1 -0
- package/dist/publishing_event_handler.js +75 -0
- package/dist/publishing_plan.d.ts +1 -2
- package/dist/publishing_plan.d.ts.map +1 -1
- package/dist/publishing_plan.js +18 -1
- package/dist/publishing_plan_helpers.d.ts +1 -1
- package/dist/publishing_plan_helpers.d.ts.map +1 -1
- package/dist/publishing_plan_helpers.js +2 -15
- package/dist/publishing_plan_logging.d.ts.map +1 -1
- package/dist/publishing_plan_logging.js +15 -23
- package/dist/repo_ops.d.ts +1 -1
- package/dist/repo_ops.js +1 -1
- package/dist/version_utils.d.ts +17 -3
- package/dist/version_utils.d.ts.map +1 -1
- package/dist/version_utils.js +22 -0
- package/package.json +6 -7
- package/src/lib/changeset_generator.ts +10 -19
- package/src/lib/changeset_reader.ts +1 -2
- package/src/lib/dependency_graph.ts +1 -1
- package/src/lib/git_operations.ts +5 -0
- package/src/lib/gitops_analyze.task.ts +18 -23
- package/src/lib/gitops_plan.task.ts +10 -2
- package/src/lib/gitops_publish.task.ts +100 -25
- package/src/lib/gitops_run.task.ts +39 -7
- package/src/lib/gitops_sync.task.ts +18 -3
- package/src/lib/gitops_task_helpers.ts +20 -3
- package/src/lib/gitops_validate.task.ts +30 -22
- package/src/lib/graph_validation.ts +26 -0
- package/src/lib/local_repo.ts +114 -83
- package/src/lib/log_helpers.ts +8 -26
- package/src/lib/multi_repo_publisher.ts +387 -269
- package/src/lib/operations.ts +6 -22
- package/src/lib/operations_defaults.ts +1 -19
- package/src/lib/output_helpers.ts +7 -6
- package/src/lib/paths.ts +0 -5
- package/src/lib/publish_gate.ts +53 -0
- package/src/lib/publish_steps.ts +149 -0
- package/src/lib/publishing_event.ts +151 -0
- package/src/lib/publishing_event_handler.ts +98 -0
- package/src/lib/publishing_plan.ts +23 -3
- package/src/lib/publishing_plan_helpers.ts +5 -18
- package/src/lib/publishing_plan_logging.ts +19 -39
- package/src/lib/repo_ops.ts +1 -1
- package/src/lib/version_utils.ts +30 -9
- package/dist/npm_install_helpers.d.ts +0 -23
- package/dist/npm_install_helpers.d.ts.map +0 -1
- package/dist/npm_install_helpers.js +0 -60
- package/dist/semver.d.ts +0 -26
- package/dist/semver.d.ts.map +0 -1
- package/dist/semver.js +0 -137
- package/dist/serialization_types.d.ts +0 -59
- package/dist/serialization_types.d.ts.map +0 -1
- package/dist/serialization_types.js +0 -42
- package/src/lib/npm_install_helpers.ts +0 -85
- package/src/lib/semver.ts +0 -170
- package/src/lib/serialization_types.ts +0 -92
package/src/lib/operations.ts
CHANGED
|
@@ -39,7 +39,7 @@ import type {FsError} from '@fuzdev/fuz_util/fs.js';
|
|
|
39
39
|
import type {Logger} from '@fuzdev/fuz_util/log.js';
|
|
40
40
|
import type {LocalRepo} from './local_repo.js';
|
|
41
41
|
import type {ChangesetInfo} from './changeset_reader.js';
|
|
42
|
-
import type {BumpType} from './
|
|
42
|
+
import type {BumpType} from './version_utils.js';
|
|
43
43
|
import type {PreflightOptions, PreflightResult} from './preflight_checks.js';
|
|
44
44
|
import type {WaitOptions} from './npm_registry.js';
|
|
45
45
|
|
|
@@ -156,16 +156,15 @@ export interface GitOperations {
|
|
|
156
156
|
}) => Promise<Result<object, {message: string}>>;
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
|
-
* Checks
|
|
159
|
+
* Checks whether the working tree has any changes — staged, unstaged, or
|
|
160
|
+
* untracked (`git status --porcelain`). Broader than `list_uncommitted_files`,
|
|
161
|
+
* which reports only tracked working-tree changes relative to HEAD.
|
|
160
162
|
*/
|
|
161
163
|
has_changes: (options?: {cwd?: string}) => Promise<Result<{value: boolean}, {message: string}>>;
|
|
162
164
|
|
|
163
165
|
/**
|
|
164
|
-
* Lists uncommitted files in the working tree (`git diff --name-only HEAD`)
|
|
165
|
-
*
|
|
166
|
-
* Renamed from `get_changed_files` in 2026-04 because "changed files" collided
|
|
167
|
-
* with mageguild's `get_changed_files` which diffs two refs. This one reports
|
|
168
|
-
* uncommitted working-tree changes relative to HEAD.
|
|
166
|
+
* Lists uncommitted files in the working tree (`git diff --name-only HEAD`),
|
|
167
|
+
* i.e. working-tree changes relative to HEAD (not a diff between two refs).
|
|
169
168
|
*/
|
|
170
169
|
list_uncommitted_files: (options?: {
|
|
171
170
|
cwd?: string;
|
|
@@ -253,15 +252,6 @@ export interface NpmOperations {
|
|
|
253
252
|
log?: Logger;
|
|
254
253
|
}) => Promise<Result<object, {message: string; timeout?: boolean}>>;
|
|
255
254
|
|
|
256
|
-
/**
|
|
257
|
-
* Checks if a package version is available on NPM.
|
|
258
|
-
*/
|
|
259
|
-
check_package_available: (options: {
|
|
260
|
-
pkg: string;
|
|
261
|
-
version: string;
|
|
262
|
-
log?: Logger;
|
|
263
|
-
}) => Promise<Result<{value: boolean}, {message: string}>>;
|
|
264
|
-
|
|
265
255
|
/**
|
|
266
256
|
* Checks npm authentication status.
|
|
267
257
|
*/
|
|
@@ -278,12 +268,6 @@ export interface NpmOperations {
|
|
|
278
268
|
install: (options?: {
|
|
279
269
|
cwd?: string;
|
|
280
270
|
}) => Promise<Result<object, {message: string; stderr?: string}>>;
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Cleans the npm cache.
|
|
284
|
-
* Uses `npm cache clean --force` to clear stale cache entries.
|
|
285
|
-
*/
|
|
286
|
-
cache_clean: () => Promise<Result<object, {message: string}>>;
|
|
287
271
|
}
|
|
288
272
|
|
|
289
273
|
/**
|
|
@@ -14,7 +14,7 @@ import {fs_classify_error} from '@fuzdev/fuz_util/fs.js';
|
|
|
14
14
|
import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
|
|
15
15
|
|
|
16
16
|
import {has_changesets, read_changesets, predict_next_version} from './changeset_reader.js';
|
|
17
|
-
import {wait_for_package
|
|
17
|
+
import {wait_for_package} from './npm_registry.js';
|
|
18
18
|
import {run_preflight_checks} from './preflight_checks.js';
|
|
19
19
|
import {
|
|
20
20
|
git_add,
|
|
@@ -236,11 +236,6 @@ export const default_npm_operations: NpmOperations = {
|
|
|
236
236
|
}
|
|
237
237
|
},
|
|
238
238
|
|
|
239
|
-
check_package_available: async (options) => {
|
|
240
|
-
const {pkg, version, log} = options;
|
|
241
|
-
return wrap_with_value(() => check_package_available(pkg, version, {log}));
|
|
242
|
-
},
|
|
243
|
-
|
|
244
239
|
check_auth: async () => {
|
|
245
240
|
try {
|
|
246
241
|
const result = await spawn_out('npm', ['whoami']);
|
|
@@ -281,19 +276,6 @@ export const default_npm_operations: NpmOperations = {
|
|
|
281
276
|
return {ok: false, message: String(error)};
|
|
282
277
|
}
|
|
283
278
|
},
|
|
284
|
-
|
|
285
|
-
cache_clean: async () => {
|
|
286
|
-
try {
|
|
287
|
-
const spawned = await spawn_out('npm', ['cache', 'clean', '--force']);
|
|
288
|
-
if (spawned.result.ok) {
|
|
289
|
-
return {ok: true};
|
|
290
|
-
} else {
|
|
291
|
-
return {ok: false, message: 'Cache clean failed'};
|
|
292
|
-
}
|
|
293
|
-
} catch (error) {
|
|
294
|
-
return {ok: false, message: String(error)};
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
279
|
};
|
|
298
280
|
|
|
299
281
|
export const default_preflight_operations: PreflightOperations = {
|
|
@@ -50,15 +50,16 @@ export const format_and_output = async <T>(
|
|
|
50
50
|
// Format data
|
|
51
51
|
const content = format === 'json' ? formatters.json(data) : formatters.markdown(data).join('\n');
|
|
52
52
|
|
|
53
|
-
// Output to file or
|
|
53
|
+
// Output to file or stdout
|
|
54
54
|
if (outfile) {
|
|
55
55
|
await writeFile(outfile, content);
|
|
56
56
|
log?.info(`Output written to ${outfile}`);
|
|
57
57
|
} else {
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
// Write raw to stdout in one shot. Routing through `log` would prefix every
|
|
59
|
+
// line with `[taskname]`, corrupting JSON and cluttering markdown — keep the
|
|
60
|
+
// machine-readable formats unprefixed and pipeable (use `--outfile` for output
|
|
61
|
+
// fully free of Gro's own preamble).
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.log(content);
|
|
63
64
|
}
|
|
64
65
|
};
|
package/src/lib/paths.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for the `gitops_publish` task's interactive gating.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `gitops_publish.task.ts` so the gating table is unit-testable without driving
|
|
5
|
+
* readline or `process.exit`. The task keeps the IO — prompt, exit, logging — at its edge and
|
|
6
|
+
* consults these for every branch.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {PublishingResult} from './multi_repo_publisher.js';
|
|
12
|
+
import type {PublishingPlan} from './publishing_plan.js';
|
|
13
|
+
|
|
14
|
+
/** What the task should do with a generated plan before executing the cascade. */
|
|
15
|
+
export type PublishGate =
|
|
16
|
+
| {action: 'blocked'; message: string}
|
|
17
|
+
| {action: 'confirm'}
|
|
18
|
+
| {action: 'proceed'};
|
|
19
|
+
|
|
20
|
+
export interface PublishGateOptions {
|
|
21
|
+
/** A real publish (`--wetrun`); a dry run never prompts. */
|
|
22
|
+
wetrun: boolean;
|
|
23
|
+
/** Show the plan and confirm (`plan`); `--no-plan` skips the prompt. */
|
|
24
|
+
show_plan: boolean;
|
|
25
|
+
plan: Pick<PublishingPlan, 'errors'>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decides whether a publish run must block, prompt for confirmation, or proceed without
|
|
30
|
+
* prompting, from the pre-execution inputs.
|
|
31
|
+
*
|
|
32
|
+
* - `blocked`: a real publish whose plan has errors — fail loud before prompting. The executor
|
|
33
|
+
* enforces this too, so `--no-plan` can't bypass the gate; this branch only avoids prompting
|
|
34
|
+
* for (and printing the "this will publish" banner of) a plan that can't run.
|
|
35
|
+
* - `confirm`: a real publish that shows its plan — the user must confirm interactively.
|
|
36
|
+
* - `proceed`: a dry run, or a `--no-plan` real publish — no prompt.
|
|
37
|
+
*/
|
|
38
|
+
export const decide_publish_gate = (options: PublishGateOptions): PublishGate => {
|
|
39
|
+
if (!options.wetrun || !options.show_plan) return {action: 'proceed'};
|
|
40
|
+
if (options.plan.errors.length > 0) {
|
|
41
|
+
return {action: 'blocked', message: 'Cannot proceed with publishing due to errors'};
|
|
42
|
+
}
|
|
43
|
+
return {action: 'confirm'};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a finished run should exit non-zero: an unsuccessful result, or a fatal error thrown
|
|
48
|
+
* out of the executor.
|
|
49
|
+
*/
|
|
50
|
+
export const publish_run_failed = (
|
|
51
|
+
result: Pick<PublishingResult, 'ok'>,
|
|
52
|
+
fatal_error: Error | null,
|
|
53
|
+
): boolean => !result.ok || fatal_error !== null;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect preview for a publishing plan.
|
|
3
|
+
*
|
|
4
|
+
* `derive_publish_steps` linearizes a frozen `PublishingPlan` into the ordered side-effects
|
|
5
|
+
* a `--wetrun` would perform, mirroring `execute_publishing_plan`'s pass so the preview and
|
|
6
|
+
* the executor read the same plan data and can't drift. Pure — no side effects.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {BumpType} from './version_utils.js';
|
|
12
|
+
import type {PublishingPlan, VersionChange} from './publishing_plan.js';
|
|
13
|
+
import {UnreachableError} from '@fuzdev/fuz_util/error.js';
|
|
14
|
+
|
|
15
|
+
/** How a package's version bump arises in the plan. */
|
|
16
|
+
export type PublishStepVia = 'changeset' | 'auto_changeset' | 'escalation';
|
|
17
|
+
|
|
18
|
+
/** One ordered side-effect a wetrun would perform. */
|
|
19
|
+
export type PublishStep =
|
|
20
|
+
| {kind: 'publish'; repo: string; from: string; to: string; bump: BumpType; via: PublishStepVia}
|
|
21
|
+
| {kind: 'npm_wait'; repo: string; version: string}
|
|
22
|
+
| {
|
|
23
|
+
kind: 'dependency_update';
|
|
24
|
+
dependent: string;
|
|
25
|
+
dependency: string;
|
|
26
|
+
to: string;
|
|
27
|
+
dep_type: 'prod' | 'peer';
|
|
28
|
+
creates_changeset: boolean;
|
|
29
|
+
}
|
|
30
|
+
| {kind: 'dev_dep_update'; repo: string; dependency: string; to: string}
|
|
31
|
+
| {kind: 'deploy'; repo: string; builds: boolean};
|
|
32
|
+
|
|
33
|
+
export interface DerivePublishStepsOptions {
|
|
34
|
+
/** Include the deploy phase (the publisher only deploys with `--deploy`). */
|
|
35
|
+
deploy?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const step_via = (change: VersionChange): PublishStepVia =>
|
|
39
|
+
change.needs_bump_escalation
|
|
40
|
+
? 'escalation'
|
|
41
|
+
: change.has_changesets
|
|
42
|
+
? 'changeset'
|
|
43
|
+
: 'auto_changeset';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derives the ordered side-effects a wetrun would perform from a frozen plan.
|
|
47
|
+
*
|
|
48
|
+
* Reads only `publishing_order`, `version_changes`, and `dependency_updates` — the same data
|
|
49
|
+
* `execute_publishing_plan` consumes, in the same order — so the preview reflects the real
|
|
50
|
+
* pass. A dependency is only propagated to its dependents if it actually publishes this run.
|
|
51
|
+
*/
|
|
52
|
+
export const derive_publish_steps = (
|
|
53
|
+
plan: PublishingPlan,
|
|
54
|
+
options: DerivePublishStepsOptions = {},
|
|
55
|
+
): Array<PublishStep> => {
|
|
56
|
+
const {deploy = false} = options;
|
|
57
|
+
const changes: Map<string, VersionChange> = new Map(
|
|
58
|
+
plan.version_changes.map((vc) => [vc.package_name, vc]),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const steps: Array<PublishStep> = [];
|
|
62
|
+
const changed: Set<string> = new Set(); // for the deploy phase: published + their dependents
|
|
63
|
+
|
|
64
|
+
// Phase 1: one pass over the topological order, mirroring `execute_publishing_plan`.
|
|
65
|
+
for (const repo of plan.publishing_order) {
|
|
66
|
+
const change = changes.get(repo);
|
|
67
|
+
if (!change) continue; // not in the plan = nothing to publish
|
|
68
|
+
|
|
69
|
+
steps.push({
|
|
70
|
+
kind: 'publish',
|
|
71
|
+
repo,
|
|
72
|
+
from: change.from,
|
|
73
|
+
to: change.to,
|
|
74
|
+
bump: change.bump_type,
|
|
75
|
+
via: step_via(change),
|
|
76
|
+
});
|
|
77
|
+
steps.push({kind: 'npm_wait', repo, version: change.to});
|
|
78
|
+
changed.add(repo);
|
|
79
|
+
|
|
80
|
+
// Prod/peer dependents are rewritten right after this publishes. A dependent that
|
|
81
|
+
// republishes (has its own plan version change) gets an auto-changeset and republishes in
|
|
82
|
+
// turn (its rewritten deps are installed + healed by its own `gro publish`); a private
|
|
83
|
+
// dependent has no version change, so it's an update-only leaf — range rewritten, no
|
|
84
|
+
// changeset.
|
|
85
|
+
for (const update of plan.dependency_updates) {
|
|
86
|
+
if (update.updated_dependency !== repo) continue;
|
|
87
|
+
if (update.type !== 'dependencies' && update.type !== 'peerDependencies') continue;
|
|
88
|
+
const republishes = changes.has(update.dependent_package);
|
|
89
|
+
steps.push({
|
|
90
|
+
kind: 'dependency_update',
|
|
91
|
+
dependent: update.dependent_package,
|
|
92
|
+
dependency: repo,
|
|
93
|
+
to: change.to,
|
|
94
|
+
dep_type: update.type === 'peerDependencies' ? 'peer' : 'prod',
|
|
95
|
+
creates_changeset: republishes,
|
|
96
|
+
});
|
|
97
|
+
changed.add(update.dependent_package);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Phase 2: dev-dependency updates for deps that published this run — committed without a
|
|
102
|
+
// changeset (dev-only changes redeploy, they don't republish). No install step: these repos
|
|
103
|
+
// don't run `gro publish`; gro installs + heals their deps when they next build/deploy/sync.
|
|
104
|
+
for (const update of plan.dependency_updates) {
|
|
105
|
+
if (update.type !== 'devDependencies') continue;
|
|
106
|
+
const dep_change = changes.get(update.updated_dependency);
|
|
107
|
+
if (!dep_change) continue;
|
|
108
|
+
steps.push({
|
|
109
|
+
kind: 'dev_dep_update',
|
|
110
|
+
repo: update.dependent_package,
|
|
111
|
+
dependency: update.updated_dependency,
|
|
112
|
+
to: dep_change.to,
|
|
113
|
+
});
|
|
114
|
+
changed.add(update.dependent_package);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Phase 3: deploy every changed repo (only with --deploy). Each builds fresh.
|
|
118
|
+
if (deploy) {
|
|
119
|
+
for (const repo of changed) {
|
|
120
|
+
steps.push({kind: 'deploy', repo, builds: true});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return steps;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Formats steps as human-readable lines (one per step) for stdout and markdown output.
|
|
129
|
+
* Returns a single placeholder line when there are no side effects.
|
|
130
|
+
*/
|
|
131
|
+
export const format_publish_steps = (steps: Array<PublishStep>): Array<string> => {
|
|
132
|
+
if (steps.length === 0) return ['(no side effects — nothing to publish)'];
|
|
133
|
+
return steps.map((step) => {
|
|
134
|
+
switch (step.kind) {
|
|
135
|
+
case 'publish':
|
|
136
|
+
return `publish ${step.repo} ${step.from} → ${step.to} (${step.bump}, ${step.via})`;
|
|
137
|
+
case 'npm_wait':
|
|
138
|
+
return `npm wait ${step.repo}@${step.version}`;
|
|
139
|
+
case 'dependency_update':
|
|
140
|
+
return `update ${step.dependent} ← ${step.dependency}@${step.to} (${step.dep_type}${step.creates_changeset ? ', changeset' : ''})`;
|
|
141
|
+
case 'dev_dep_update':
|
|
142
|
+
return `dev dep ${step.repo} ← ${step.dependency}@${step.to} (no changeset)`;
|
|
143
|
+
case 'deploy':
|
|
144
|
+
return `deploy ${step.repo}${step.builds ? ' (builds)' : ''}`;
|
|
145
|
+
default:
|
|
146
|
+
throw new UnreachableError(step);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
|
|
17
|
+
import {z} from 'zod';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Coarse triage classification for a failed package. Lets consumers branch on
|
|
21
|
+
* failure kind without parsing the message.
|
|
22
|
+
*/
|
|
23
|
+
export const PublishingErrorCode = z.enum([
|
|
24
|
+
'publish',
|
|
25
|
+
'network',
|
|
26
|
+
'auth',
|
|
27
|
+
'dependency',
|
|
28
|
+
'build',
|
|
29
|
+
// the real published version diverged from the frozen plan's prediction — an
|
|
30
|
+
// invariant violation, distinct from an ordinary publish failure (see fail-loud
|
|
31
|
+
// drift detection in `multi_repo_publisher.ts`)
|
|
32
|
+
'drift',
|
|
33
|
+
'other',
|
|
34
|
+
]);
|
|
35
|
+
export type PublishingErrorCode = z.infer<typeof PublishingErrorCode>;
|
|
36
|
+
|
|
37
|
+
/** Tallied outcome of a publishing run, derived from its events via `summarize_events`. */
|
|
38
|
+
export const PublishingRunSummary = z.strictObject({
|
|
39
|
+
total: z.number().meta({description: 'packages in the publishing order (the candidate set)'}),
|
|
40
|
+
published: z.number().meta({description: 'packages published (or, in a dry run, predicted)'}),
|
|
41
|
+
failed: z.number(),
|
|
42
|
+
skipped: z.number(),
|
|
43
|
+
duration: z.number().meta({description: 'wall-clock duration in milliseconds'}),
|
|
44
|
+
});
|
|
45
|
+
export type PublishingRunSummary = z.infer<typeof PublishingRunSummary>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A single structured event emitted during a publishing run. Tagged on `event` so the
|
|
49
|
+
* union serializes as one self-describing JSON object per event (JSON-lines on the wire).
|
|
50
|
+
*/
|
|
51
|
+
export const PublishingEvent = z.discriminatedUnion('event', [
|
|
52
|
+
z.strictObject({
|
|
53
|
+
event: z.literal('run_started'),
|
|
54
|
+
wetrun: z.boolean().meta({description: 'false means every package_completed is a prediction'}),
|
|
55
|
+
total: z.number(),
|
|
56
|
+
}),
|
|
57
|
+
z.strictObject({
|
|
58
|
+
event: z.literal('package_skipped'),
|
|
59
|
+
name: z.string(),
|
|
60
|
+
reason: z.string(),
|
|
61
|
+
}),
|
|
62
|
+
z.strictObject({
|
|
63
|
+
event: z.literal('package_completed'),
|
|
64
|
+
name: z.string(),
|
|
65
|
+
old_version: z.string(),
|
|
66
|
+
new_version: z.string(),
|
|
67
|
+
// mirrors `BumpType` from `version_utils.ts`; inline so the event schema is self-contained
|
|
68
|
+
bump_type: z.enum(['major', 'minor', 'patch']),
|
|
69
|
+
breaking: z.boolean(),
|
|
70
|
+
commit: z.string().meta({description: "'simulated' in a dry run, otherwise the commit hash"}),
|
|
71
|
+
tag: z.string(),
|
|
72
|
+
}),
|
|
73
|
+
z.strictObject({
|
|
74
|
+
event: z.literal('npm_waited'),
|
|
75
|
+
name: z.string(),
|
|
76
|
+
version: z.string().meta({description: 'the version waited on after publishing'}),
|
|
77
|
+
}),
|
|
78
|
+
z.strictObject({
|
|
79
|
+
event: z.literal('package_failed'),
|
|
80
|
+
name: z.string(),
|
|
81
|
+
error: z.string(),
|
|
82
|
+
code: PublishingErrorCode,
|
|
83
|
+
}),
|
|
84
|
+
z.strictObject({
|
|
85
|
+
event: z.literal('dependency_updated'),
|
|
86
|
+
dependent: z.string(),
|
|
87
|
+
dependency: z.string(),
|
|
88
|
+
version: z.string(),
|
|
89
|
+
// 'prod'/'peer' updates run inline in the publish pass; 'dev' updates run in the later
|
|
90
|
+
// dev-dependency pass. Lets a consumer reconstruct which side-effect phase this is.
|
|
91
|
+
dep_type: z.enum(['prod', 'peer', 'dev']),
|
|
92
|
+
// true when the update creates an auto-changeset (a publishable dependent republishes);
|
|
93
|
+
// false for dev-dep updates and update-only leaves (private dependents never publish).
|
|
94
|
+
creates_changeset: z.boolean(),
|
|
95
|
+
}),
|
|
96
|
+
z.strictObject({
|
|
97
|
+
event: z.literal('deploy_started'),
|
|
98
|
+
name: z.string(),
|
|
99
|
+
}),
|
|
100
|
+
z.strictObject({
|
|
101
|
+
event: z.literal('deploy_completed'),
|
|
102
|
+
name: z.string(),
|
|
103
|
+
}),
|
|
104
|
+
z.strictObject({
|
|
105
|
+
event: z.literal('deploy_failed'),
|
|
106
|
+
name: z.string(),
|
|
107
|
+
error: z.string(),
|
|
108
|
+
}),
|
|
109
|
+
z.strictObject({
|
|
110
|
+
event: z.literal('run_finished'),
|
|
111
|
+
summary: PublishingRunSummary,
|
|
112
|
+
}),
|
|
113
|
+
]);
|
|
114
|
+
export type PublishingEvent = z.infer<typeof PublishingEvent>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Derives a run summary from the captured event list — the single canonical path from
|
|
118
|
+
* events to summary, so the `run_finished` summary always agrees with the stream.
|
|
119
|
+
* Call before emitting `run_finished` (which is not itself counted).
|
|
120
|
+
*
|
|
121
|
+
* @param events - the events captured so far this run
|
|
122
|
+
* @param duration - wall-clock duration in milliseconds
|
|
123
|
+
*/
|
|
124
|
+
export const summarize_events = (
|
|
125
|
+
events: Array<PublishingEvent>,
|
|
126
|
+
duration: number,
|
|
127
|
+
): PublishingRunSummary => {
|
|
128
|
+
let total = 0;
|
|
129
|
+
let published = 0;
|
|
130
|
+
let failed = 0;
|
|
131
|
+
let skipped = 0;
|
|
132
|
+
for (const event of events) {
|
|
133
|
+
switch (event.event) {
|
|
134
|
+
case 'run_started':
|
|
135
|
+
total = event.total;
|
|
136
|
+
break;
|
|
137
|
+
case 'package_completed':
|
|
138
|
+
published++;
|
|
139
|
+
break;
|
|
140
|
+
case 'package_failed':
|
|
141
|
+
failed++;
|
|
142
|
+
break;
|
|
143
|
+
case 'package_skipped':
|
|
144
|
+
skipped++;
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {total, published, failed, skipped, duration};
|
|
151
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
|
|
12
|
+
import type {PublishingEvent} from './publishing_event.js';
|
|
13
|
+
|
|
14
|
+
/** A sink for publishing events. */
|
|
15
|
+
export interface PublishingEventHandler {
|
|
16
|
+
emit: (event: PublishingEvent) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A `capture_handler` also exposes the events it has collected. */
|
|
20
|
+
export interface CapturingEventHandler extends PublishingEventHandler {
|
|
21
|
+
readonly events: Array<PublishingEvent>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Drops every event. The default when no handler is supplied. */
|
|
25
|
+
export const null_handler = (): PublishingEventHandler => ({
|
|
26
|
+
emit: () => {},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Collects events in memory. Used to build the run report and in tests. */
|
|
30
|
+
export const capture_handler = (): CapturingEventHandler => {
|
|
31
|
+
const events: Array<PublishingEvent> = [];
|
|
32
|
+
return {
|
|
33
|
+
events,
|
|
34
|
+
emit: (event) => {
|
|
35
|
+
events.push(event);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Writes each event as one JSON object per line (JSON-lines) to `process.stdout`.
|
|
42
|
+
* Write failures are swallowed — the stream is observability, not control flow.
|
|
43
|
+
*/
|
|
44
|
+
export const stdout_handler = (): PublishingEventHandler => ({
|
|
45
|
+
emit: (event) => {
|
|
46
|
+
try {
|
|
47
|
+
process.stdout.write(JSON.stringify(event) + '\n');
|
|
48
|
+
} catch {
|
|
49
|
+
// best-effort: a logging sink must never fail a run
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Fans an event out to every handler in order. */
|
|
55
|
+
export const multi_handler = (handlers: Array<PublishingEventHandler>): PublishingEventHandler => ({
|
|
56
|
+
emit: (event) => {
|
|
57
|
+
for (const handler of handlers) {
|
|
58
|
+
handler.emit(event);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wraps a handler, masking secrets in each event's string fields before forwarding.
|
|
65
|
+
*
|
|
66
|
+
* @param inner - the handler to forward masked events to
|
|
67
|
+
* @param mask - the masking function, defaults to `mask_secrets`
|
|
68
|
+
*/
|
|
69
|
+
export const masking_handler = (
|
|
70
|
+
inner: PublishingEventHandler,
|
|
71
|
+
mask: (event: PublishingEvent) => PublishingEvent = mask_secrets,
|
|
72
|
+
): PublishingEventHandler => ({
|
|
73
|
+
emit: (event) => {
|
|
74
|
+
inner.emit(mask(event));
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Minimal redaction rules: npm auth tokens (bare or registry-scoped), `SECRET_*`
|
|
79
|
+
// env-style assignments, and `npm_`-prefixed tokens. Deliberately lean — error
|
|
80
|
+
// strings can carry npm/git output; a fuller secret catalog is deferred.
|
|
81
|
+
const SECRET_RULES: Array<readonly [RegExp, string]> = [
|
|
82
|
+
[/((?:\/\/[^\s:]+:)?_authToken\s*=\s*)\S+/gi, '$1[redacted]'],
|
|
83
|
+
[/(SECRET_[A-Z0-9_]+\s*[=:]\s*)\S+/g, '$1[redacted]'],
|
|
84
|
+
[/(npm_[A-Za-z0-9]{4})[A-Za-z0-9]{12,}/g, '$1[redacted]'],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/** Redacts known secret shapes from a string. */
|
|
88
|
+
export const redact_secrets = (text: string): string =>
|
|
89
|
+
SECRET_RULES.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), text);
|
|
90
|
+
|
|
91
|
+
/** Returns a copy of the event with secrets redacted from its string-valued fields. */
|
|
92
|
+
export const mask_secrets = (event: PublishingEvent): PublishingEvent => {
|
|
93
|
+
const masked: Record<string, unknown> = {};
|
|
94
|
+
for (const [key, value] of Object.entries(event)) {
|
|
95
|
+
masked[key] = typeof value === 'string' ? redact_secrets(value) : value;
|
|
96
|
+
}
|
|
97
|
+
return masked as PublishingEvent;
|
|
98
|
+
};
|
|
@@ -2,9 +2,13 @@ import type {Logger} from '@fuzdev/fuz_util/log.js';
|
|
|
2
2
|
import {styleText as st} from 'node:util';
|
|
3
3
|
|
|
4
4
|
import type {LocalRepo} from './local_repo.js';
|
|
5
|
-
import type {BumpType} from './semver.js';
|
|
6
5
|
import {validate_dependency_graph} from './graph_validation.js';
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type BumpType,
|
|
8
|
+
is_breaking_change,
|
|
9
|
+
compare_bump_types,
|
|
10
|
+
calculate_next_version,
|
|
11
|
+
} from './version_utils.js';
|
|
8
12
|
import type {ChangesetOperations} from './operations.js';
|
|
9
13
|
import {default_changeset_operations} from './operations_defaults.js';
|
|
10
14
|
import {GITOPS_MAX_ITERATIONS_DEFAULT} from './gitops_constants.js';
|
|
@@ -36,7 +40,6 @@ export interface DependencyUpdate {
|
|
|
36
40
|
current_version: string;
|
|
37
41
|
new_version: string;
|
|
38
42
|
type: 'dependencies' | 'devDependencies' | 'peerDependencies';
|
|
39
|
-
causes_republish: boolean;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
// Verbose data types for diagnostic output
|
|
@@ -178,6 +181,18 @@ export const generate_publishing_plan = async (
|
|
|
178
181
|
const repo = repos.find((r) => r.library.name === pkg_name);
|
|
179
182
|
if (!repo) continue;
|
|
180
183
|
|
|
184
|
+
// Private packages never publish — exclude them from version changes entirely (no
|
|
185
|
+
// publish step, npm-wait, bump escalation, or auto-changeset). They keep their slot in
|
|
186
|
+
// the topological order. Flag a private package that carries a changeset, since that
|
|
187
|
+
// changeset can't be published.
|
|
188
|
+
if (repo.library.package_json.private) {
|
|
189
|
+
const private_has_changesets = await ops.has_changesets({repo});
|
|
190
|
+
if (private_has_changesets.ok && private_has_changesets.value) {
|
|
191
|
+
warnings.push(`${pkg_name} is private — its changeset(s) will not be published`);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
181
196
|
// Check for changesets
|
|
182
197
|
const has_result = await ops.has_changesets({repo});
|
|
183
198
|
|
|
@@ -267,6 +282,10 @@ export const generate_publishing_plan = async (
|
|
|
267
282
|
for (const repo of repos) {
|
|
268
283
|
const pkg_name = repo.library.name;
|
|
269
284
|
|
|
285
|
+
// Private packages are excluded from version changes (they never publish), so they
|
|
286
|
+
// never escalate or auto-generate a changeset.
|
|
287
|
+
if (repo.library.package_json.private) continue;
|
|
288
|
+
|
|
270
289
|
// Get required bump from dependencies
|
|
271
290
|
const required_bump = get_required_bump_for_dependencies(
|
|
272
291
|
repo,
|
|
@@ -405,6 +424,7 @@ export const generate_publishing_plan = async (
|
|
|
405
424
|
|
|
406
425
|
for (const repo of repos) {
|
|
407
426
|
const pkg_name = repo.library.name;
|
|
427
|
+
if (repo.library.package_json.private) continue; // private packages never publish
|
|
408
428
|
const required_bump = get_required_bump_for_dependencies(
|
|
409
429
|
repo,
|
|
410
430
|
pending_updates,
|