@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
|
@@ -5,26 +5,36 @@ import {styleText as st} from 'node:util';
|
|
|
5
5
|
|
|
6
6
|
import type {LocalRepo} from './local_repo.js';
|
|
7
7
|
import {update_package_json, type VersionStrategy} from './dependency_updater.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
generate_publishing_plan,
|
|
10
|
+
type VersionChange,
|
|
11
|
+
type DependencyUpdate,
|
|
12
|
+
type PublishingPlan,
|
|
13
|
+
} from './publishing_plan.js';
|
|
9
14
|
import type {PreflightOptions} from './preflight_checks.js';
|
|
10
|
-
import {needs_update, is_breaking_change, detect_bump_type} from './version_utils.js';
|
|
11
15
|
import type {GitopsOperations} from './operations.js';
|
|
12
16
|
import {default_gitops_operations} from './operations_defaults.js';
|
|
17
|
+
import {GITOPS_NPM_WAIT_TIMEOUT_DEFAULT} from './gitops_constants.js';
|
|
18
|
+
import {
|
|
19
|
+
type PublishingEvent,
|
|
20
|
+
type PublishingRunSummary,
|
|
21
|
+
summarize_events,
|
|
22
|
+
} from './publishing_event.js';
|
|
13
23
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
type PublishingEventHandler,
|
|
25
|
+
capture_handler,
|
|
26
|
+
multi_handler,
|
|
27
|
+
} from './publishing_event_handler.js';
|
|
18
28
|
|
|
19
29
|
export interface PublishingOptions {
|
|
20
30
|
wetrun: boolean;
|
|
21
|
-
update_deps: boolean;
|
|
22
31
|
version_strategy?: VersionStrategy;
|
|
23
32
|
deploy?: boolean;
|
|
24
33
|
max_wait?: number;
|
|
25
|
-
skip_install?: boolean;
|
|
26
34
|
log?: Logger;
|
|
27
35
|
ops?: GitopsOperations;
|
|
36
|
+
/** Structured event sink; defaults to capture-only (events surface on the result). */
|
|
37
|
+
events?: PublishingEventHandler;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export interface PublishedVersion {
|
|
@@ -42,14 +52,59 @@ export interface PublishingResult {
|
|
|
42
52
|
published: Array<PublishedVersion>;
|
|
43
53
|
failed: Array<{name: string; error: Error}>;
|
|
44
54
|
duration: number;
|
|
55
|
+
/** The structured event stream for this run, in emission order. */
|
|
56
|
+
events: Array<PublishingEvent>;
|
|
57
|
+
/** Tallied outcome, derived from `events`. */
|
|
58
|
+
summary: PublishingRunSummary;
|
|
59
|
+
/** Plan errors that blocked (wetrun) or would block (dry run) publishing; empty when clean. */
|
|
60
|
+
plan_errors: Array<string>;
|
|
61
|
+
/** Non-blocking plan warnings (e.g. the fixed-point iteration limit). */
|
|
62
|
+
plan_warnings: Array<string>;
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
export const publish_repos = async (
|
|
48
66
|
repos: Array<LocalRepo>,
|
|
49
67
|
options: PublishingOptions,
|
|
68
|
+
): Promise<PublishingResult> => {
|
|
69
|
+
const {log, ops = default_gitops_operations} = options;
|
|
70
|
+
// Convenience entry: generate the plan, then execute it. Callers that already hold a
|
|
71
|
+
// plan (e.g. to display and confirm it first) call `execute_publishing_plan` directly,
|
|
72
|
+
// so the plan is generated exactly once per command.
|
|
73
|
+
const plan = await generate_publishing_plan(repos, {log, ops: ops.changeset});
|
|
74
|
+
return execute_publishing_plan(repos, plan, options);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Executes a frozen publishing plan in a single linear pass — the "dumb executor" half of
|
|
79
|
+
* the zap model. The plan is the single source of truth; this re-derives nothing.
|
|
80
|
+
*
|
|
81
|
+
* Fails loud when the plan couldn't be fully computed (`plan.errors`): a wetrun aborts
|
|
82
|
+
* before any side effect, a dry run still reports the partial cascade but returns
|
|
83
|
+
* `ok: false`. This is the single error gate — `--no-plan` can't bypass it.
|
|
84
|
+
*/
|
|
85
|
+
export const execute_publishing_plan = async (
|
|
86
|
+
repos: Array<LocalRepo>,
|
|
87
|
+
plan: PublishingPlan,
|
|
88
|
+
options: PublishingOptions,
|
|
50
89
|
): Promise<PublishingResult> => {
|
|
51
90
|
const start_time = Date.now();
|
|
52
|
-
const {wetrun,
|
|
91
|
+
const {wetrun, log, ops = default_gitops_operations} = options;
|
|
92
|
+
|
|
93
|
+
// Fail loud on an incomplete plan. Executing one with errors would silently skip the
|
|
94
|
+
// affected packages. A wetrun aborts before touching npm or git; a dry run proceeds
|
|
95
|
+
// (no side effects) but the result is not ok (see the summary below).
|
|
96
|
+
if (wetrun && plan.errors.length > 0) {
|
|
97
|
+
throw new TaskError(
|
|
98
|
+
`Cannot publish — the plan has ${plan.errors.length} error(s):\n ${plan.errors.join('\n ')}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Capture every event for the result; also forward to the caller's sink if provided.
|
|
103
|
+
const capture = capture_handler();
|
|
104
|
+
const events_handler = options.events ? multi_handler([options.events, capture]) : capture;
|
|
105
|
+
const emit = (event: PublishingEvent): void => {
|
|
106
|
+
events_handler.emit(event);
|
|
107
|
+
};
|
|
53
108
|
|
|
54
109
|
// Preflight checks (skip for dry runs since we're not actually publishing)
|
|
55
110
|
if (wetrun) {
|
|
@@ -74,254 +129,243 @@ export const publish_repos = async (
|
|
|
74
129
|
log?.info('⏭️ Skipping preflight checks (dry run)');
|
|
75
130
|
}
|
|
76
131
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
132
|
+
// The plan already validated the dependency graph and resolved the full cascade
|
|
133
|
+
// (explicit changesets, bump escalations, and auto-generated changesets) — execute its
|
|
134
|
+
// frozen topological order directly, re-deriving nothing.
|
|
135
|
+
const order = plan.publishing_order;
|
|
136
|
+
const plan_changes: Map<string, VersionChange> = new Map(
|
|
137
|
+
plan.version_changes.map((vc) => [vc.package_name, vc]),
|
|
138
|
+
);
|
|
139
|
+
if (order.length > 0) {
|
|
140
|
+
log?.info(` Publishing order: ${order.join(' → ')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
emit({event: 'run_started', wetrun, total: order.length});
|
|
84
144
|
|
|
85
145
|
const published: Map<string, PublishedVersion> = new Map();
|
|
86
146
|
const failed: Map<string, Error> = new Map();
|
|
87
147
|
const changed_repos: Set<string> = new Set(); // Track repos with any changes for selective deployment
|
|
148
|
+
// Name → repo index, built once to avoid repeated linear scans of `repos`.
|
|
149
|
+
const repo_by_name: Map<string, LocalRepo> = new Map(repos.map((r) => [r.library.name, r]));
|
|
150
|
+
|
|
151
|
+
log?.info(st('cyan', `\n🚀 ${wetrun ? 'Publishing' : 'Dry run'}...\n`));
|
|
152
|
+
|
|
153
|
+
// Phase 1: one linear pass over the plan's topological order. The plan already
|
|
154
|
+
// resolved the full cascade, and publishing a package immediately rewrites every
|
|
155
|
+
// dependent's package.json and creates its auto-changeset — so by the time the pass
|
|
156
|
+
// reaches any package, all its dependencies have published and its changeset exists.
|
|
157
|
+
// No fixed-point loop is needed: a single pass converges by construction.
|
|
158
|
+
for (let i = 0; i < order.length; i++) {
|
|
159
|
+
const pkg_name = order[i]!;
|
|
160
|
+
const planned = plan_changes.get(pkg_name);
|
|
161
|
+
|
|
162
|
+
// Not in the plan = no changesets and no dependency updates = nothing to publish.
|
|
163
|
+
if (!planned) {
|
|
164
|
+
emit({event: 'package_skipped', name: pkg_name, reason: 'no changesets'});
|
|
165
|
+
log?.info(st('yellow', ` ⚠️ Skipping ${pkg_name} - no changesets`));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
88
168
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let iteration = 0;
|
|
92
|
-
let converged = false;
|
|
93
|
-
|
|
94
|
-
while (!converged && iteration < GITOPS_MAX_ITERATIONS_DEFAULT) {
|
|
95
|
-
iteration++;
|
|
96
|
-
log?.info(
|
|
97
|
-
st('cyan', `\n🚀 Publishing iteration ${iteration}/${GITOPS_MAX_ITERATIONS_DEFAULT}...\n`),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Track if any packages were published in this iteration
|
|
101
|
-
let published_in_iteration = false;
|
|
102
|
-
let published_count = 0;
|
|
103
|
-
|
|
104
|
-
// Track repos changed in THIS iteration only (for batch install)
|
|
105
|
-
const changed_in_iteration: Set<string> = new Set();
|
|
106
|
-
|
|
107
|
-
// Phase 1: Publish each package and immediately update dependents
|
|
108
|
-
for (let i = 0; i < order.length; i++) {
|
|
109
|
-
const pkg_name = order[i]!;
|
|
110
|
-
const repo = repos.find((r) => r.library.name === pkg_name);
|
|
111
|
-
if (!repo) continue;
|
|
112
|
-
|
|
113
|
-
// Skip if already published in a previous iteration
|
|
114
|
-
if (published.has(pkg_name)) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
169
|
+
const repo = repo_by_name.get(pkg_name);
|
|
170
|
+
if (!repo) continue;
|
|
117
171
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
172
|
+
// An earlier publish may have rewritten this package's dependency ranges. No install is
|
|
173
|
+
// needed here: `gro publish` runs its own install (which self-heals npm's stale-cache
|
|
174
|
+
// ETARGET), so the package's freshly-rewritten deps are installed and healed as part of
|
|
175
|
+
// publishing it.
|
|
176
|
+
try {
|
|
177
|
+
// 1. Publish this package (real publish or dry-run prediction)
|
|
178
|
+
log?.info(
|
|
179
|
+
st(
|
|
180
|
+
'dim',
|
|
181
|
+
` [${i + 1}/${order.length}] ${wetrun ? 'Publishing' : 'Would publish'} ${pkg_name}...`,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
const version = await publish_single_repo(repo, options, ops, planned);
|
|
185
|
+
|
|
186
|
+
// Fail loud on drift: a real publish that lands a version the plan didn't predict
|
|
187
|
+
// is an invariant violation, not a routine failure. Abort and leave the dirty
|
|
188
|
+
// state in place — re-running re-plans from the current state (the just-published
|
|
189
|
+
// package no longer has changesets, so it drops out of the new plan).
|
|
190
|
+
if (wetrun && version.new_version !== planned.to) {
|
|
191
|
+
const err = new Error(
|
|
192
|
+
`Plan drift for ${pkg_name}: published ${version.new_version} but the plan predicted ${planned.to}. ` +
|
|
193
|
+
`Aborting — re-run 'gro gitops_publish --wetrun' to re-plan from the current state.`,
|
|
194
|
+
);
|
|
123
195
|
failed.set(pkg_name, err);
|
|
196
|
+
emit({event: 'package_failed', name: pkg_name, error: err.message, code: 'drift'});
|
|
124
197
|
log?.error(st('red', ` ❌ ${err.message}`));
|
|
125
198
|
break;
|
|
126
199
|
}
|
|
127
200
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
published.set(pkg_name, version);
|
|
146
|
-
changed_repos.add(pkg_name); // Mark as changed for deployment
|
|
147
|
-
// Note: don't add to changed_in_iteration - published packages don't need install
|
|
148
|
-
// (their dependencies didn't change, only their version)
|
|
149
|
-
published_in_iteration = true;
|
|
150
|
-
published_count++;
|
|
151
|
-
log?.info(st('green', ` ✅ Published ${pkg_name}@${version.new_version}`));
|
|
152
|
-
|
|
153
|
-
if (wetrun) {
|
|
154
|
-
// 2. Wait for this package to be available on NPM
|
|
155
|
-
log?.info(` ⏳ Waiting for ${pkg_name}@${version.new_version} on NPM...`);
|
|
156
|
-
const wait_result = await ops.npm.wait_for_package({
|
|
157
|
-
pkg: pkg_name,
|
|
158
|
-
version: version.new_version,
|
|
159
|
-
wait_options: {
|
|
160
|
-
max_attempts: 30,
|
|
161
|
-
initial_delay: 1000,
|
|
162
|
-
max_delay: 60000,
|
|
163
|
-
timeout: options.max_wait ?? GITOPS_NPM_WAIT_TIMEOUT_DEFAULT,
|
|
164
|
-
},
|
|
165
|
-
log,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (!wait_result.ok) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`Failed to wait for package: ${wait_result.message}${wait_result.timeout ? ' (timeout)' : ''}`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
201
|
+
published.set(pkg_name, version);
|
|
202
|
+
changed_repos.add(pkg_name); // Mark as changed for deployment
|
|
203
|
+
emit({
|
|
204
|
+
event: 'package_completed',
|
|
205
|
+
name: pkg_name,
|
|
206
|
+
old_version: version.old_version,
|
|
207
|
+
new_version: version.new_version,
|
|
208
|
+
bump_type: version.bump_type,
|
|
209
|
+
breaking: version.breaking,
|
|
210
|
+
commit: version.commit,
|
|
211
|
+
tag: version.tag,
|
|
212
|
+
});
|
|
213
|
+
log?.info(
|
|
214
|
+
wetrun
|
|
215
|
+
? st('green', ` ✅ Published ${pkg_name}@${version.new_version}`)
|
|
216
|
+
: st('cyan', ` ◇ Would publish ${pkg_name}@${version.new_version}`),
|
|
217
|
+
);
|
|
173
218
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (dependent_repo.peer_dependencies?.has(pkg_name)) {
|
|
189
|
-
const current = dependent_repo.peer_dependencies.get(pkg_name)!;
|
|
190
|
-
if (needs_update(current, version.new_version)) {
|
|
191
|
-
updates.set(pkg_name, version.new_version);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Apply updates if any
|
|
196
|
-
if (updates.size > 0) {
|
|
197
|
-
log?.info(
|
|
198
|
-
` Updating ${dependent_repo.library.name}'s dependency on ${pkg_name}`,
|
|
199
|
-
);
|
|
200
|
-
changed_repos.add(dependent_repo.library.name); // Mark as changed for deployment
|
|
201
|
-
changed_in_iteration.add(dependent_repo.library.name); // Track for batch install
|
|
202
|
-
await update_package_json(dependent_repo, updates, {
|
|
203
|
-
strategy: options.version_strategy || 'caret',
|
|
204
|
-
published_versions: published,
|
|
205
|
-
log,
|
|
206
|
-
git_ops: ops.git,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
214
|
-
failed.set(pkg_name, err);
|
|
215
|
-
log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
|
|
216
|
-
break; // Always fail fast on error
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
+
if (wetrun) {
|
|
220
|
+
// 2. Wait for this package to be available on NPM
|
|
221
|
+
log?.info(` ⏳ Waiting for ${pkg_name}@${version.new_version} on NPM...`);
|
|
222
|
+
const wait_result = await ops.npm.wait_for_package({
|
|
223
|
+
pkg: pkg_name,
|
|
224
|
+
version: version.new_version,
|
|
225
|
+
wait_options: {
|
|
226
|
+
max_attempts: 30,
|
|
227
|
+
initial_delay: 1000,
|
|
228
|
+
max_delay: 60000,
|
|
229
|
+
timeout: options.max_wait ?? GITOPS_NPM_WAIT_TIMEOUT_DEFAULT,
|
|
230
|
+
},
|
|
231
|
+
log,
|
|
232
|
+
});
|
|
219
233
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!repo) continue;
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
log?.info(` Installing ${pkg_name}...`);
|
|
231
|
-
await install_with_cache_healing(repo, ops, log);
|
|
232
|
-
log?.info(st('green', ` ✅ Installed ${pkg_name}`));
|
|
233
|
-
} catch (error) {
|
|
234
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
234
|
+
if (!wait_result.ok) {
|
|
235
|
+
// Handle inline (don't throw into the generic catch): the npm-wait failure
|
|
236
|
+
// carries a typed `timeout` signal, so we know this is a network failure
|
|
237
|
+
// without sniffing the message.
|
|
238
|
+
const err = new Error(
|
|
239
|
+
`Failed to wait for package: ${wait_result.message}${wait_result.timeout ? ' (timeout)' : ''}`,
|
|
240
|
+
);
|
|
235
241
|
failed.set(pkg_name, err);
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
emit({event: 'package_failed', name: pkg_name, error: err.message, code: 'network'});
|
|
243
|
+
log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
|
|
244
|
+
break; // fail fast
|
|
245
|
+
}
|
|
246
|
+
emit({event: 'npm_waited', name: pkg_name, version: version.new_version});
|
|
247
|
+
|
|
248
|
+
// 3. Update every dependent the plan says has a prod/peer dep on this package.
|
|
249
|
+
// This rewrites their package.json ranges and creates their auto-changeset,
|
|
250
|
+
// which a later step of this same pass publishes. The dependent is queued for
|
|
251
|
+
// a single install just before it publishes (see the top of the loop).
|
|
252
|
+
const dependent_updates = group_dependency_updates(
|
|
253
|
+
plan.dependency_updates,
|
|
254
|
+
published,
|
|
255
|
+
(update) =>
|
|
256
|
+
update.updated_dependency === pkg_name &&
|
|
257
|
+
(update.type === 'dependencies' || update.type === 'peerDependencies'),
|
|
258
|
+
);
|
|
259
|
+
for (const [dependent_name, updates] of dependent_updates) {
|
|
260
|
+
const dependent_repo = repo_by_name.get(dependent_name);
|
|
261
|
+
if (!dependent_repo) continue;
|
|
262
|
+
// A dependent republishes iff the plan gave it a version change. Private packages
|
|
263
|
+
// are excluded from the plan's version changes, so they take the update-only-leaf
|
|
264
|
+
// path: rewrite + commit their dependency ranges with NO changeset and NO
|
|
265
|
+
// publish/npm-wait. Publishable dependents get an auto-changeset and republish in
|
|
266
|
+
// turn — `gro publish` installs + heals their rewritten deps when it reaches them.
|
|
267
|
+
const republishes = plan_changes.has(dependent_name);
|
|
268
|
+
for (const [dep_name, dep_version] of updates) {
|
|
269
|
+
log?.info(` Updating ${dependent_name}'s dependency on ${dep_name}`);
|
|
270
|
+
emit({
|
|
271
|
+
event: 'dependency_updated',
|
|
272
|
+
dependent: dependent_name,
|
|
273
|
+
dependency: dep_name,
|
|
274
|
+
version: dep_version,
|
|
275
|
+
dep_type: dependency_update_type(plan, dependent_name, dep_name),
|
|
276
|
+
creates_changeset: republishes,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
changed_repos.add(dependent_name); // Mark as changed for deployment
|
|
280
|
+
if (republishes) {
|
|
281
|
+
await update_package_json(dependent_repo, updates, {
|
|
282
|
+
strategy: options.version_strategy || 'caret',
|
|
283
|
+
published_versions: published, // creates the auto-changeset
|
|
284
|
+
log,
|
|
285
|
+
git_ops: ops.git,
|
|
286
|
+
fs_ops: ops.fs,
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
// update-only leaf: rewrite ranges + commit, no changeset (it won't republish)
|
|
290
|
+
await update_package_json(dependent_repo, updates, {
|
|
291
|
+
strategy: options.version_strategy || 'caret',
|
|
292
|
+
log,
|
|
293
|
+
git_ops: ops.git,
|
|
294
|
+
fs_ops: ops.fs,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
238
297
|
}
|
|
239
298
|
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const estimated_iterations = Math.ceil(pending_count / 2); // Rough estimate
|
|
255
|
-
|
|
256
|
-
log?.warn(
|
|
257
|
-
st(
|
|
258
|
-
'yellow',
|
|
259
|
-
`\n⚠️ Reached maximum iterations (${GITOPS_MAX_ITERATIONS_DEFAULT}) without full convergence\n` +
|
|
260
|
-
` ${pending_count} package(s) may still have changesets to process\n` +
|
|
261
|
-
` Estimated ${estimated_iterations} more iteration(s) needed - run 'gro gitops_publish' again\n`,
|
|
262
|
-
),
|
|
263
|
-
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
301
|
+
failed.set(pkg_name, err);
|
|
302
|
+
emit({
|
|
303
|
+
event: 'package_failed',
|
|
304
|
+
name: pkg_name,
|
|
305
|
+
error: err.message,
|
|
306
|
+
// TODO: emit a precise code once the npm/process ops return typed errors —
|
|
307
|
+
// today a publish-step cause lives in unstructured stderr, so use the honest
|
|
308
|
+
// coarse bucket rather than guessing 'auth'/'network'/'build' from the message.
|
|
309
|
+
code: 'publish',
|
|
310
|
+
});
|
|
311
|
+
log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
|
|
312
|
+
break; // Always fail fast on error
|
|
264
313
|
}
|
|
265
314
|
}
|
|
266
315
|
|
|
267
316
|
// Phase 2: Update all dev dependencies (can have cycles)
|
|
268
|
-
// Dev dep changes require deployment even without version bumps (rebuild needed)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const published_version = published.get(dep_name);
|
|
280
|
-
if (published_version && needs_update(current_version, published_version.new_version)) {
|
|
281
|
-
dev_updates.set(dep_name, published_version.new_version);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
317
|
+
// Dev dep changes require deployment even without version bumps (rebuild needed).
|
|
318
|
+
// Sourced from the plan's dependency updates so the publisher derives nothing itself.
|
|
319
|
+
// Only package.json is rewritten + committed here — no install. These repos don't run
|
|
320
|
+
// `gro publish`; their `node_modules` is refreshed (and ETARGET-healed) by gro the next
|
|
321
|
+
// time they build/deploy/sync, so the executor never runs a bare `npm install` itself.
|
|
322
|
+
if (published.size > 0 && wetrun) {
|
|
323
|
+
const dev_updates_by_repo = group_dependency_updates(
|
|
324
|
+
plan.dependency_updates,
|
|
325
|
+
published,
|
|
326
|
+
(update) => update.type === 'devDependencies',
|
|
327
|
+
);
|
|
285
328
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
changed_repos.add(repo.library.name); // Mark as changed for deployment
|
|
289
|
-
dev_updated_repos.add(repo.library.name); // Track for batch install
|
|
290
|
-
await update_package_json(repo, dev_updates, {
|
|
291
|
-
strategy: options.version_strategy || 'caret',
|
|
292
|
-
published_versions: published,
|
|
293
|
-
log,
|
|
294
|
-
git_ops: ops.git,
|
|
295
|
-
});
|
|
296
|
-
}
|
|
329
|
+
if (dev_updates_by_repo.size > 0) {
|
|
330
|
+
log?.info(st('cyan', '\n🔄 Updating dev dependencies...\n'));
|
|
297
331
|
}
|
|
298
|
-
}
|
|
299
332
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
log?.info(st('cyan', '\n📦 Installing dev dependencies for updated repos...\n'));
|
|
303
|
-
|
|
304
|
-
for (const pkg_name of dev_updated_repos) {
|
|
305
|
-
const repo = repos.find((r) => r.library.name === pkg_name);
|
|
333
|
+
for (const [repo_name, dev_updates] of dev_updates_by_repo) {
|
|
334
|
+
const repo = repo_by_name.get(repo_name);
|
|
306
335
|
if (!repo) continue;
|
|
307
336
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
337
|
+
log?.info(` Updating ${dev_updates.size} dev dependencies in ${repo_name}`);
|
|
338
|
+
for (const [dep_name, dep_version] of dev_updates) {
|
|
339
|
+
emit({
|
|
340
|
+
event: 'dependency_updated',
|
|
341
|
+
dependent: repo_name,
|
|
342
|
+
dependency: dep_name,
|
|
343
|
+
version: dep_version,
|
|
344
|
+
dep_type: 'dev',
|
|
345
|
+
creates_changeset: false, // dev-dep updates redeploy without republishing
|
|
346
|
+
});
|
|
317
347
|
}
|
|
348
|
+
changed_repos.add(repo_name); // Mark as changed for deployment
|
|
349
|
+
// No `published_versions` here on purpose: a dev-dep bump updates and commits
|
|
350
|
+
// package.json but must NOT generate a changeset — dev-only changes redeploy
|
|
351
|
+
// (rebuild) without republishing, so they shouldn't bump the next release.
|
|
352
|
+
await update_package_json(repo, dev_updates, {
|
|
353
|
+
strategy: options.version_strategy || 'caret',
|
|
354
|
+
log,
|
|
355
|
+
git_ops: ops.git,
|
|
356
|
+
fs_ops: ops.fs,
|
|
357
|
+
});
|
|
318
358
|
}
|
|
319
359
|
}
|
|
320
360
|
|
|
321
361
|
// Phase 3: Deploy repos with changes (optional)
|
|
322
|
-
// Deploys only repos that were: published, had prod/peer deps updated, or had dev deps updated
|
|
362
|
+
// Deploys only repos that were: published, had prod/peer deps updated, or had dev deps updated.
|
|
363
|
+
// Iterate `changed_repos` (insertion order) rather than `repos` order so deploys run in
|
|
364
|
+
// dependency order — and so the `--preview` side-effect list matches this exactly.
|
|
323
365
|
if (options.deploy && wetrun) {
|
|
324
|
-
const repos_to_deploy =
|
|
366
|
+
const repos_to_deploy = Array.from(changed_repos)
|
|
367
|
+
.map((name) => repo_by_name.get(name))
|
|
368
|
+
.filter((r): r is LocalRepo => r !== undefined);
|
|
325
369
|
log?.info(
|
|
326
370
|
st(
|
|
327
371
|
'cyan',
|
|
@@ -331,92 +375,125 @@ export const publish_repos = async (
|
|
|
331
375
|
|
|
332
376
|
for (const repo of repos_to_deploy) {
|
|
333
377
|
try {
|
|
378
|
+
emit({event: 'deploy_started', name: repo.library.name});
|
|
334
379
|
log?.info(` Deploying ${repo.library.name}...`);
|
|
380
|
+
// Build fresh (no --no-build): a deployed site bundles its dependencies, so it
|
|
381
|
+
// must be rebuilt against the versions this run just published — the preflight
|
|
382
|
+
// build ran against the old versions, before the cascade rewrote package.json.
|
|
335
383
|
const deploy_result = await ops.process.spawn({
|
|
336
384
|
cmd: 'gro',
|
|
337
|
-
args: ['deploy'
|
|
385
|
+
args: ['deploy'],
|
|
338
386
|
cwd: repo.repo_dir,
|
|
339
387
|
});
|
|
340
388
|
|
|
341
389
|
if (deploy_result.ok) {
|
|
390
|
+
emit({event: 'deploy_completed', name: repo.library.name});
|
|
342
391
|
log?.info(st('green', ` ✅ Deployed ${repo.library.name}`));
|
|
343
392
|
} else {
|
|
393
|
+
emit({event: 'deploy_failed', name: repo.library.name, error: deploy_result.message});
|
|
344
394
|
log?.warn(st('yellow', ` ⚠️ Failed to deploy ${repo.library.name}`));
|
|
345
395
|
}
|
|
346
396
|
} catch (error) {
|
|
347
|
-
|
|
397
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
398
|
+
emit({event: 'deploy_failed', name: repo.library.name, error: err.message});
|
|
399
|
+
log?.error(st('red', ` ❌ Error deploying ${repo.library.name}: ${err.message}`));
|
|
348
400
|
}
|
|
349
401
|
}
|
|
350
402
|
}
|
|
351
403
|
|
|
352
404
|
// Summary
|
|
353
405
|
const duration = Date.now() - start_time;
|
|
354
|
-
const ok = failed.size === 0;
|
|
406
|
+
const ok = failed.size === 0 && plan.errors.length === 0;
|
|
407
|
+
const summary = summarize_events(capture.events, duration);
|
|
355
408
|
|
|
356
|
-
log?.info(st('cyan',
|
|
409
|
+
log?.info(st('cyan', `\n📋 ${wetrun ? 'Publishing' : 'Dry Run'} Summary\n`));
|
|
357
410
|
log?.info(` Duration: ${(duration / 1000).toFixed(1)}s`);
|
|
358
|
-
log?.info(` Published: ${published.size} packages`);
|
|
411
|
+
log?.info(` ${wetrun ? 'Published' : 'Would publish'}: ${published.size} packages`);
|
|
359
412
|
if (failed.size > 0) {
|
|
360
413
|
log?.info(` Failed: ${failed.size} packages`);
|
|
361
414
|
}
|
|
362
415
|
|
|
416
|
+
// Surface plan diagnostics (a wetrun with errors threw above; this is mainly the dry run
|
|
417
|
+
// and non-blocking warnings) so an audit of the cascade sees them.
|
|
418
|
+
if (plan.warnings.length > 0) {
|
|
419
|
+
log?.warn(st('yellow', ` ⚠️ Plan warnings: ${plan.warnings.length}`));
|
|
420
|
+
for (const warning of plan.warnings) log?.warn(st('yellow', ` - ${warning}`));
|
|
421
|
+
}
|
|
422
|
+
if (plan.errors.length > 0) {
|
|
423
|
+
log?.error(st('red', ` ❌ Plan errors: ${plan.errors.length}`));
|
|
424
|
+
for (const plan_error of plan.errors) log?.error(st('red', ` - ${plan_error}`));
|
|
425
|
+
}
|
|
426
|
+
|
|
363
427
|
if (ok) {
|
|
364
|
-
|
|
428
|
+
if (wetrun) {
|
|
429
|
+
log?.info(st('green', '\n✨ All packages published successfully!\n'));
|
|
430
|
+
} else {
|
|
431
|
+
log?.info(
|
|
432
|
+
st(
|
|
433
|
+
'green',
|
|
434
|
+
`\n✨ Dry run complete — ${published.size} package(s) would be published. Re-run with --wetrun to publish.`,
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
// The dry run is driven by the same plan as `gro gitops_plan`, so this
|
|
438
|
+
// count includes bump escalations and auto-generated changesets — it
|
|
439
|
+
// matches `gro gitops_plan` exactly (the full cascade).
|
|
440
|
+
log?.info(st('dim', 'This matches `gro gitops_plan` (the full cascade).\n'));
|
|
441
|
+
}
|
|
365
442
|
} else {
|
|
366
|
-
log?.error(
|
|
443
|
+
log?.error(
|
|
444
|
+
st(
|
|
445
|
+
'red',
|
|
446
|
+
wetrun
|
|
447
|
+
? '\n❌ Some packages failed to publish\n'
|
|
448
|
+
: '\n❌ Some packages failed during dry run\n',
|
|
449
|
+
),
|
|
450
|
+
);
|
|
367
451
|
}
|
|
368
452
|
|
|
453
|
+
emit({event: 'run_finished', summary});
|
|
454
|
+
|
|
369
455
|
return {
|
|
370
456
|
ok,
|
|
371
457
|
published: Array.from(published.values()),
|
|
372
458
|
failed: Array.from(failed.entries()).map(([name, error]) => ({name, error})),
|
|
373
459
|
duration,
|
|
460
|
+
events: capture.events,
|
|
461
|
+
summary,
|
|
462
|
+
plan_errors: plan.errors,
|
|
463
|
+
plan_warnings: plan.warnings,
|
|
374
464
|
};
|
|
375
465
|
};
|
|
376
466
|
|
|
377
467
|
/**
|
|
378
468
|
* Publishes a single repo using `gro publish`.
|
|
379
469
|
*
|
|
380
|
-
* Dry run mode:
|
|
381
|
-
* Real mode:
|
|
382
|
-
* reads new version from `package.json`, and returns
|
|
470
|
+
* Dry run mode: reports the precomputed plan entry without side effects.
|
|
471
|
+
* Real mode: runs `gro publish --no-build` (builds already validated in preflight),
|
|
472
|
+
* reads the new version from `package.json`, and returns it alongside the plan's
|
|
473
|
+
* predicted bump metadata. The caller compares the read-back version to the plan to
|
|
474
|
+
* detect drift.
|
|
383
475
|
*
|
|
384
|
-
* @throws {Error} if
|
|
476
|
+
* @throws {Error} if the publish, version read-back, or commit-hash lookup fails
|
|
385
477
|
*/
|
|
386
478
|
const publish_single_repo = async (
|
|
387
479
|
repo: LocalRepo,
|
|
388
480
|
options: PublishingOptions,
|
|
389
|
-
ops: GitopsOperations
|
|
481
|
+
ops: GitopsOperations,
|
|
482
|
+
planned: VersionChange,
|
|
390
483
|
): Promise<PublishedVersion> => {
|
|
391
|
-
const {wetrun
|
|
392
|
-
|
|
393
|
-
const old_version = repo.library.package_json.version || '0.0.0';
|
|
484
|
+
const {wetrun} = options;
|
|
394
485
|
|
|
395
486
|
if (!wetrun) {
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (!prediction) {
|
|
400
|
-
// No changesets found, skip this repo
|
|
401
|
-
throw new Error(`No changesets found for ${repo.library.name}`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (!prediction.ok) {
|
|
405
|
-
// Error reading changesets
|
|
406
|
-
throw new Error(`Failed to predict version: ${prediction.message}`);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const {version: new_version, bump_type} = prediction;
|
|
410
|
-
const breaking = is_breaking_change(old_version, bump_type);
|
|
411
|
-
|
|
487
|
+
// Dry run reports the precomputed plan — the single source of truth for the
|
|
488
|
+
// cascade (explicit changesets, bump escalations, and auto-generated changesets).
|
|
412
489
|
return {
|
|
413
490
|
name: repo.library.name,
|
|
414
|
-
old_version,
|
|
415
|
-
new_version,
|
|
416
|
-
bump_type,
|
|
417
|
-
breaking,
|
|
491
|
+
old_version: planned.from,
|
|
492
|
+
new_version: planned.to,
|
|
493
|
+
bump_type: planned.bump_type,
|
|
494
|
+
breaking: planned.breaking,
|
|
418
495
|
commit: 'simulated',
|
|
419
|
-
tag: `v${
|
|
496
|
+
tag: `v${planned.to}`,
|
|
420
497
|
};
|
|
421
498
|
}
|
|
422
499
|
|
|
@@ -442,10 +519,6 @@ const publish_single_repo = async (
|
|
|
442
519
|
const package_json = JSON.parse(content_result.value);
|
|
443
520
|
const new_version = package_json.version;
|
|
444
521
|
|
|
445
|
-
// Determine bump type and if it's breaking
|
|
446
|
-
const bump_type = detect_bump_type(old_version, new_version);
|
|
447
|
-
const breaking = is_breaking_change(old_version, bump_type);
|
|
448
|
-
|
|
449
522
|
// Get actual commit hash
|
|
450
523
|
const commit_result = await ops.git.current_commit_hash({cwd: repo.repo_dir});
|
|
451
524
|
|
|
@@ -455,13 +528,58 @@ const publish_single_repo = async (
|
|
|
455
528
|
|
|
456
529
|
const commit = commit_result.value;
|
|
457
530
|
|
|
531
|
+
// Bump metadata comes from the plan (the single source of truth); the caller
|
|
532
|
+
// fail-louds if `new_version` diverges from the plan's prediction.
|
|
458
533
|
return {
|
|
459
534
|
name: repo.library.name,
|
|
460
|
-
old_version,
|
|
535
|
+
old_version: planned.from,
|
|
461
536
|
new_version,
|
|
462
|
-
bump_type,
|
|
463
|
-
breaking,
|
|
537
|
+
bump_type: planned.bump_type,
|
|
538
|
+
breaking: planned.breaking,
|
|
464
539
|
commit,
|
|
465
540
|
tag: `v${new_version}`,
|
|
466
541
|
};
|
|
467
542
|
};
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Groups dependency updates by dependent package — `dependent → (dependency → new
|
|
546
|
+
* version)`, the shape `update_package_json` consumes. Restricted by `predicate` (e.g.
|
|
547
|
+
* prod/peer for a given package, or all dev deps) and to dependencies that actually
|
|
548
|
+
* published this run, so a failed/aborted publish never propagates to its dependents.
|
|
549
|
+
*/
|
|
550
|
+
export const group_dependency_updates = (
|
|
551
|
+
updates: Array<DependencyUpdate>,
|
|
552
|
+
published: Map<string, PublishedVersion>,
|
|
553
|
+
predicate: (update: DependencyUpdate) => boolean,
|
|
554
|
+
): Map<string, Map<string, string>> => {
|
|
555
|
+
const by_repo: Map<string, Map<string, string>> = new Map();
|
|
556
|
+
for (const update of updates) {
|
|
557
|
+
if (!predicate(update)) continue;
|
|
558
|
+
const published_dep = published.get(update.updated_dependency);
|
|
559
|
+
if (!published_dep) continue;
|
|
560
|
+
let repo_updates = by_repo.get(update.dependent_package);
|
|
561
|
+
if (!repo_updates) {
|
|
562
|
+
repo_updates = new Map();
|
|
563
|
+
by_repo.set(update.dependent_package, repo_updates);
|
|
564
|
+
}
|
|
565
|
+
repo_updates.set(update.updated_dependency, published_dep.new_version);
|
|
566
|
+
}
|
|
567
|
+
return by_repo;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/** The dep_type tag for a prod/peer dependency-update event — `peer` if the edge is a peer
|
|
571
|
+
* dependency, else `prod`. Mirrors `derive_publish_steps`' classification so the executor's
|
|
572
|
+
* event stream and the preview agree. */
|
|
573
|
+
const dependency_update_type = (
|
|
574
|
+
plan: PublishingPlan,
|
|
575
|
+
dependent: string,
|
|
576
|
+
dependency: string,
|
|
577
|
+
): 'prod' | 'peer' =>
|
|
578
|
+
plan.dependency_updates.some(
|
|
579
|
+
(u) =>
|
|
580
|
+
u.dependent_package === dependent &&
|
|
581
|
+
u.updated_dependency === dependency &&
|
|
582
|
+
u.type === 'peerDependencies',
|
|
583
|
+
)
|
|
584
|
+
? 'peer'
|
|
585
|
+
: 'prod';
|