@fuzdev/fuz_gitops 0.72.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 +2 -2
- package/dist/gitops_publish.task.d.ts.map +1 -1
- package/dist/gitops_publish.task.js +83 -28
- 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 +21 -2
- package/dist/multi_repo_publisher.d.ts.map +1 -1
- package/dist/multi_repo_publisher.js +279 -287
- 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 +12 -19
- package/dist/publishing_event.d.ts.map +1 -1
- package/dist/publishing_event.js +16 -25
- 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/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 +5 -6
- 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 +92 -27
- 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 +336 -353
- 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 +16 -25
- 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/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
|
+
};
|
|
@@ -26,6 +26,10 @@ export const PublishingErrorCode = z.enum([
|
|
|
26
26
|
'auth',
|
|
27
27
|
'dependency',
|
|
28
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',
|
|
29
33
|
'other',
|
|
30
34
|
]);
|
|
31
35
|
export type PublishingErrorCode = z.infer<typeof PublishingErrorCode>;
|
|
@@ -50,17 +54,6 @@ export const PublishingEvent = z.discriminatedUnion('event', [
|
|
|
50
54
|
wetrun: z.boolean().meta({description: 'false means every package_completed is a prediction'}),
|
|
51
55
|
total: z.number(),
|
|
52
56
|
}),
|
|
53
|
-
z.strictObject({
|
|
54
|
-
event: z.literal('iteration_started'),
|
|
55
|
-
iteration: z.number(),
|
|
56
|
-
max: z.number(),
|
|
57
|
-
}),
|
|
58
|
-
z.strictObject({
|
|
59
|
-
event: z.literal('iteration_finished'),
|
|
60
|
-
iteration: z.number(),
|
|
61
|
-
published_count: z.number(),
|
|
62
|
-
converged: z.boolean(),
|
|
63
|
-
}),
|
|
64
57
|
z.strictObject({
|
|
65
58
|
event: z.literal('package_skipped'),
|
|
66
59
|
name: z.string(),
|
|
@@ -71,12 +64,17 @@ export const PublishingEvent = z.discriminatedUnion('event', [
|
|
|
71
64
|
name: z.string(),
|
|
72
65
|
old_version: z.string(),
|
|
73
66
|
new_version: z.string(),
|
|
74
|
-
// mirrors `BumpType` from `
|
|
67
|
+
// mirrors `BumpType` from `version_utils.ts`; inline so the event schema is self-contained
|
|
75
68
|
bump_type: z.enum(['major', 'minor', 'patch']),
|
|
76
69
|
breaking: z.boolean(),
|
|
77
70
|
commit: z.string().meta({description: "'simulated' in a dry run, otherwise the commit hash"}),
|
|
78
71
|
tag: z.string(),
|
|
79
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
|
+
}),
|
|
80
78
|
z.strictObject({
|
|
81
79
|
event: z.literal('package_failed'),
|
|
82
80
|
name: z.string(),
|
|
@@ -88,19 +86,12 @@ export const PublishingEvent = z.discriminatedUnion('event', [
|
|
|
88
86
|
dependent: z.string(),
|
|
89
87
|
dependency: z.string(),
|
|
90
88
|
version: z.string(),
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
event: z.literal('install_completed'),
|
|
98
|
-
name: z.string(),
|
|
99
|
-
}),
|
|
100
|
-
z.strictObject({
|
|
101
|
-
event: z.literal('install_failed'),
|
|
102
|
-
name: z.string(),
|
|
103
|
-
error: 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(),
|
|
104
95
|
}),
|
|
105
96
|
z.strictObject({
|
|
106
97
|
event: z.literal('deploy_started'),
|
|
@@ -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,
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type {LocalRepo} from './local_repo.js';
|
|
10
|
-
import type
|
|
11
|
-
import {needs_update} from './version_utils.js';
|
|
10
|
+
import {needs_update, required_bump_for_dependency_update, type BumpType} from './version_utils.js';
|
|
12
11
|
import type {DependencyUpdate} from './publishing_plan.js';
|
|
13
12
|
|
|
14
13
|
/**
|
|
@@ -42,7 +41,6 @@ export const calculate_dependency_updates = (
|
|
|
42
41
|
current_version,
|
|
43
42
|
new_version,
|
|
44
43
|
type: 'dependencies',
|
|
45
|
-
causes_republish: true,
|
|
46
44
|
});
|
|
47
45
|
|
|
48
46
|
if (breaking_packages.has(dep_name)) {
|
|
@@ -67,7 +65,6 @@ export const calculate_dependency_updates = (
|
|
|
67
65
|
current_version,
|
|
68
66
|
new_version,
|
|
69
67
|
type: 'peerDependencies',
|
|
70
|
-
causes_republish: true,
|
|
71
68
|
});
|
|
72
69
|
|
|
73
70
|
if (breaking_packages.has(dep_name)) {
|
|
@@ -92,7 +89,6 @@ export const calculate_dependency_updates = (
|
|
|
92
89
|
current_version,
|
|
93
90
|
new_version,
|
|
94
91
|
type: 'devDependencies',
|
|
95
|
-
causes_republish: false,
|
|
96
92
|
});
|
|
97
93
|
}
|
|
98
94
|
}
|
|
@@ -131,17 +127,8 @@ export const get_required_bump_for_dependencies = (
|
|
|
131
127
|
breaking_packages.has(update.updated_dependency),
|
|
132
128
|
);
|
|
133
129
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (has_breaking_deps) {
|
|
139
|
-
// Breaking changes propagate
|
|
140
|
-
// Pre-1.0: use minor for breaking changes
|
|
141
|
-
// 1.0+: use major for breaking changes
|
|
142
|
-
return is_pre_1_0 ? 'minor' : 'major';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// For non-breaking dependency updates, use patch
|
|
146
|
-
return 'patch';
|
|
130
|
+
return required_bump_for_dependency_update(
|
|
131
|
+
repo.library.package_json.version || '0.0.0',
|
|
132
|
+
has_breaking_deps,
|
|
133
|
+
);
|
|
147
134
|
};
|
|
@@ -182,54 +182,34 @@ export const log_publishing_plan = (
|
|
|
182
182
|
const with_escalation = ordered_changes.filter((vc) => vc.needs_bump_escalation);
|
|
183
183
|
const with_auto_changesets = ordered_changes.filter((vc) => vc.will_generate_changeset);
|
|
184
184
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
// A single running counter across all groups so positions read 1..N with no
|
|
186
|
+
// gaps. (Numbering per-group index would make the first group skip the numbers
|
|
187
|
+
// that land in a later group, e.g. jump from `[9/12]` to `[11/12]`.)
|
|
188
|
+
const total = with_changesets.length + with_escalation.length + with_auto_changesets.length;
|
|
189
|
+
let position = 0;
|
|
190
|
+
const log_change_group = (
|
|
191
|
+
title: string,
|
|
192
|
+
color: 'cyan' | 'yellow',
|
|
193
|
+
changes: Array<VersionChange>,
|
|
194
|
+
): void => {
|
|
195
|
+
if (changes.length === 0) return;
|
|
196
|
+
log.info(st(color, title));
|
|
197
|
+
for (const change of changes) {
|
|
190
198
|
log_version_change_with_diffs(
|
|
191
199
|
change,
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
position++,
|
|
201
|
+
total,
|
|
194
202
|
dependency_updates,
|
|
195
203
|
breaking_cascades,
|
|
196
204
|
log,
|
|
197
205
|
);
|
|
198
206
|
}
|
|
199
207
|
log.info('');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (with_escalation.length > 0) {
|
|
203
|
-
log.info(st('yellow', 'Version Changes (bump escalation):'));
|
|
204
|
-
for (const change of with_escalation) {
|
|
205
|
-
const idx = ordered_changes.indexOf(change);
|
|
206
|
-
log_version_change_with_diffs(
|
|
207
|
-
change,
|
|
208
|
-
idx,
|
|
209
|
-
ordered_changes.length,
|
|
210
|
-
dependency_updates,
|
|
211
|
-
breaking_cascades,
|
|
212
|
-
log,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
log.info('');
|
|
216
|
-
}
|
|
208
|
+
};
|
|
217
209
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const idx = ordered_changes.indexOf(change);
|
|
222
|
-
log_version_change_with_diffs(
|
|
223
|
-
change,
|
|
224
|
-
idx,
|
|
225
|
-
ordered_changes.length,
|
|
226
|
-
dependency_updates,
|
|
227
|
-
breaking_cascades,
|
|
228
|
-
log,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
log.info('');
|
|
232
|
-
}
|
|
210
|
+
log_change_group('Version Changes (from changesets):', 'cyan', with_changesets);
|
|
211
|
+
log_change_group('Version Changes (bump escalation):', 'yellow', with_escalation);
|
|
212
|
+
log_change_group('Version Changes (auto-generated):', 'cyan', with_auto_changesets);
|
|
233
213
|
} else {
|
|
234
214
|
log.info(st('dim', 'No packages to publish'));
|
|
235
215
|
log.info('');
|
package/src/lib/version_utils.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// TODO: candidate for extraction to `@fuzdev/fuz_util`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** A semver bump kind: `major`, `minor`, or `patch`. */
|
|
4
|
+
export type BumpType = 'major' | 'minor' | 'patch';
|
|
4
5
|
|
|
5
6
|
export const is_wildcard = (version: string): boolean => {
|
|
6
7
|
return version === '*';
|
|
@@ -100,10 +101,7 @@ export const get_update_prefix = (
|
|
|
100
101
|
* Pre-1.0: minor bumps are breaking
|
|
101
102
|
* 1.0+: major bumps are breaking
|
|
102
103
|
*/
|
|
103
|
-
export const is_breaking_change = (
|
|
104
|
-
old_version: string,
|
|
105
|
-
bump_type: 'major' | 'minor' | 'patch',
|
|
106
|
-
): boolean => {
|
|
104
|
+
export const is_breaking_change = (old_version: string, bump_type: BumpType): boolean => {
|
|
107
105
|
const [major] = old_version.split('.').map(Number);
|
|
108
106
|
const is_pre_1_0 = major === 0;
|
|
109
107
|
|
|
@@ -116,10 +114,33 @@ export const is_breaking_change = (
|
|
|
116
114
|
}
|
|
117
115
|
};
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
/**
|
|
118
|
+
* The bump a package must take when one of its prod/peer dependencies updates.
|
|
119
|
+
* Pre-1.0: `minor` for a breaking dependency, otherwise `patch`.
|
|
120
|
+
* 1.0+: `major` for a breaking dependency, otherwise `patch`.
|
|
121
|
+
*
|
|
122
|
+
* Single source of truth for the dependency-driven bump rule, shared by the plan
|
|
123
|
+
* (`get_required_bump_for_dependencies`) and the auto-changeset generator
|
|
124
|
+
* (`calculate_required_bump`) so the two never drift.
|
|
125
|
+
*
|
|
126
|
+
* @param current_version - the package's current version, used to detect the pre-1.0 regime
|
|
127
|
+
* @param has_breaking_deps - whether any updated dependency is a breaking change
|
|
128
|
+
*/
|
|
129
|
+
export const required_bump_for_dependency_update = (
|
|
130
|
+
current_version: string,
|
|
131
|
+
has_breaking_deps: boolean,
|
|
132
|
+
): BumpType => {
|
|
133
|
+
const [major] = current_version.split('.').map(Number);
|
|
134
|
+
const is_pre_1_0 = major === 0;
|
|
135
|
+
if (has_breaking_deps) {
|
|
136
|
+
// Breaking changes propagate: pre-1.0 uses minor, 1.0+ uses major
|
|
137
|
+
return is_pre_1_0 ? 'minor' : 'major';
|
|
138
|
+
}
|
|
139
|
+
// Non-breaking dependency updates only need a patch
|
|
140
|
+
return 'patch';
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const detect_bump_type = (old_version: string, new_version: string): BumpType => {
|
|
123
144
|
const old_parts = old_version.split('.').map(Number);
|
|
124
145
|
const new_parts = new_version.split('.').map(Number);
|
|
125
146
|
|