@fuzdev/fuz_gitops 0.72.0 → 0.74.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 +15 -8
- package/dist/PageHeader.svelte +3 -3
- package/dist/PageHeader.svelte.d.ts +3 -3
- package/dist/PageHeader.svelte.d.ts.map +1 -1
- 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/changeset_reader.js +1 -1
- package/dist/dependency_graph.d.ts.map +1 -1
- package/dist/dependency_graph.js +6 -6
- package/dist/fetch_repo_data.d.ts.map +1 -1
- package/dist/fetch_repo_data.js +3 -2
- 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 +25 -12
- package/dist/local_repo.d.ts.map +1 -1
- package/dist/local_repo.js +99 -79
- 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 +4 -20
- 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 +21 -4
- 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.svelte.d.ts +10 -6
- package/dist/repo.svelte.d.ts.map +1 -1
- package/dist/repo.svelte.js +6 -14
- 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 +14 -15
- package/src/lib/changeset_generator.ts +10 -19
- package/src/lib/changeset_reader.ts +2 -3
- package/src/lib/dependency_graph.ts +6 -8
- package/src/lib/fetch_repo_data.ts +3 -2
- 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 +124 -88
- 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 +4 -20
- 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 +26 -6
- package/src/lib/publishing_plan_helpers.ts +5 -18
- package/src/lib/publishing_plan_logging.ts +19 -39
- package/src/lib/repo.svelte.ts +12 -15
- 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,
|
|
@@ -119,9 +119,11 @@ export const default_git_operations: GitOperations = {
|
|
|
119
119
|
pull: async (options) => {
|
|
120
120
|
const {origin, branch, cwd} = options ?? EMPTY_OBJECT;
|
|
121
121
|
try {
|
|
122
|
+
// Omit the branch arg when absent: a trailing `''` makes git pull the remote's
|
|
123
|
+
// default branch (origin/HEAD) instead of the current branch's upstream.
|
|
122
124
|
const spawned = await spawn_out(
|
|
123
125
|
'git',
|
|
124
|
-
['pull', origin || 'origin', branch
|
|
126
|
+
['pull', origin || 'origin', ...(branch ? [branch] : [])],
|
|
125
127
|
cwd ? {cwd} : undefined,
|
|
126
128
|
);
|
|
127
129
|
if (spawned.result.ok) {
|
|
@@ -236,11 +238,6 @@ export const default_npm_operations: NpmOperations = {
|
|
|
236
238
|
}
|
|
237
239
|
},
|
|
238
240
|
|
|
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
241
|
check_auth: async () => {
|
|
245
242
|
try {
|
|
246
243
|
const result = await spawn_out('npm', ['whoami']);
|
|
@@ -281,19 +278,6 @@ export const default_npm_operations: NpmOperations = {
|
|
|
281
278
|
return {ok: false, message: String(error)};
|
|
282
279
|
}
|
|
283
280
|
},
|
|
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
281
|
};
|
|
298
282
|
|
|
299
283
|
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.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
|
|
|
@@ -221,7 +236,7 @@ export const generate_publishing_plan = async (
|
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
{
|
|
224
|
-
const old_version = repo.
|
|
239
|
+
const old_version = repo.package_json.version || '0.0.0';
|
|
225
240
|
const is_breaking = is_breaking_change(old_version, prediction.bump_type);
|
|
226
241
|
|
|
227
242
|
predicted_versions.set(pkg_name, prediction.version);
|
|
@@ -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.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,
|
|
@@ -295,7 +314,7 @@ export const generate_publishing_plan = async (
|
|
|
295
314
|
// Package has changesets - check if it needs bump escalation
|
|
296
315
|
if (required_bump && compare_bump_types(required_bump, existing_entry.bump_type) > 0) {
|
|
297
316
|
// Dependencies require a larger bump than existing changesets provide
|
|
298
|
-
const old_version = repo.
|
|
317
|
+
const old_version = repo.package_json.version || '0.0.0';
|
|
299
318
|
const new_version = calculate_next_version(old_version, required_bump);
|
|
300
319
|
|
|
301
320
|
// Only mark as changed if version actually changed
|
|
@@ -337,7 +356,7 @@ export const generate_publishing_plan = async (
|
|
|
337
356
|
}
|
|
338
357
|
} else if (required_bump) {
|
|
339
358
|
// No existing changesets but needs changeset for dependency updates
|
|
340
|
-
const old_version = repo.
|
|
359
|
+
const old_version = repo.package_json.version || '0.0.0';
|
|
341
360
|
const new_version = calculate_next_version(old_version, required_bump);
|
|
342
361
|
|
|
343
362
|
// Check if this is a new version (not already in version_changes)
|
|
@@ -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.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.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('');
|