@fuzdev/fuz_gitops 0.78.1 → 0.79.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 +78 -24
- package/dist/ModulesDetail.svelte +15 -22
- package/dist/ModulesDetail.svelte.d.ts.map +1 -1
- package/dist/ModulesNav.svelte +4 -1
- package/dist/ModulesPage.svelte +1 -3
- package/dist/ModulesPage.svelte.d.ts.map +1 -1
- package/dist/PageHeader.svelte +2 -2
- package/dist/PageHeader.svelte.d.ts +0 -3
- package/dist/PageHeader.svelte.d.ts.map +1 -1
- package/dist/PullRequestsDetail.svelte +10 -9
- package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
- package/dist/PullRequestsPage.svelte +1 -3
- package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
- package/dist/ReposTable.svelte +27 -48
- package/dist/ReposTable.svelte.d.ts.map +1 -1
- package/dist/ReposTree.svelte +5 -19
- package/dist/ReposTree.svelte.d.ts.map +1 -1
- package/dist/ReposTreeNav.svelte +5 -7
- package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
- package/dist/TablePage.svelte +1 -3
- package/dist/TablePage.svelte.d.ts.map +1 -1
- package/dist/TreeItemPage.svelte +1 -3
- package/dist/TreeItemPage.svelte.d.ts.map +1 -1
- package/dist/TreePage.svelte +1 -3
- package/dist/TreePage.svelte.d.ts.map +1 -1
- package/dist/ci_reconcile.d.ts +6 -10
- package/dist/ci_reconcile.d.ts.map +1 -1
- package/dist/ci_reconcile.js +6 -8
- package/dist/dependency_graph.d.ts +26 -49
- package/dist/dependency_graph.d.ts.map +1 -1
- package/dist/dependency_graph.js +37 -80
- package/dist/dependency_updater.d.ts +1 -1
- package/dist/dependency_updater.d.ts.map +1 -1
- package/dist/dependency_updater.js +6 -1
- package/dist/fetch_repo_data.d.ts +34 -8
- package/dist/fetch_repo_data.d.ts.map +1 -1
- package/dist/fetch_repo_data.js +44 -22
- package/dist/fs_fetch_value_cache.d.ts +1 -1
- package/dist/fs_fetch_value_cache.js +1 -1
- package/dist/git_operations.d.ts +6 -49
- package/dist/git_operations.d.ts.map +1 -1
- package/dist/git_operations.js +13 -126
- package/dist/github.d.ts +17 -2
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +15 -6
- package/dist/github_helpers.d.ts.map +1 -1
- package/dist/github_helpers.js +3 -11
- package/dist/gitops_analyze.task.d.ts +24 -2
- package/dist/gitops_analyze.task.d.ts.map +1 -1
- package/dist/gitops_analyze.task.js +43 -30
- package/dist/gitops_config.d.ts +34 -79
- package/dist/gitops_config.d.ts.map +1 -1
- package/dist/gitops_config.js +76 -53
- package/dist/gitops_constants.d.ts +6 -4
- package/dist/gitops_constants.d.ts.map +1 -1
- package/dist/gitops_constants.js +6 -4
- package/dist/gitops_plan.task.d.ts +29 -3
- package/dist/gitops_plan.task.d.ts.map +1 -1
- package/dist/gitops_plan.task.js +80 -58
- package/dist/gitops_publish.task.d.ts +62 -2
- package/dist/gitops_publish.task.d.ts.map +1 -1
- package/dist/gitops_publish.task.js +187 -102
- package/dist/gitops_run.task.d.ts +74 -0
- package/dist/gitops_run.task.d.ts.map +1 -1
- package/dist/gitops_run.task.js +174 -160
- package/dist/gitops_sync.task.d.ts +65 -3
- package/dist/gitops_sync.task.d.ts.map +1 -1
- package/dist/gitops_sync.task.js +154 -30
- package/dist/gitops_task_helpers.d.ts +86 -59
- package/dist/gitops_task_helpers.d.ts.map +1 -1
- package/dist/gitops_task_helpers.js +120 -79
- package/dist/gitops_validate.task.d.ts +1 -2
- package/dist/gitops_validate.task.d.ts.map +1 -1
- package/dist/gitops_validate.task.js +15 -22
- package/dist/graph_validation.d.ts +16 -27
- package/dist/graph_validation.d.ts.map +1 -1
- package/dist/graph_validation.js +23 -64
- package/dist/local_repo.d.ts +50 -62
- package/dist/local_repo.d.ts.map +1 -1
- package/dist/local_repo.js +80 -193
- package/dist/log_helpers.d.ts +8 -8
- package/dist/log_helpers.d.ts.map +1 -1
- package/dist/multi_repo_publisher.d.ts +10 -0
- package/dist/multi_repo_publisher.d.ts.map +1 -1
- package/dist/multi_repo_publisher.js +115 -51
- package/dist/npm_registry.d.ts +23 -19
- package/dist/npm_registry.d.ts.map +1 -1
- package/dist/npm_registry.js +20 -47
- package/dist/operations.d.ts +75 -180
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +15 -7
- package/dist/operations_defaults.d.ts +17 -2
- package/dist/operations_defaults.d.ts.map +1 -1
- package/dist/operations_defaults.js +104 -121
- package/dist/output_helpers.d.ts +36 -0
- package/dist/output_helpers.d.ts.map +1 -1
- package/dist/output_helpers.js +37 -3
- package/dist/preflight_checks.d.ts +20 -27
- package/dist/preflight_checks.d.ts.map +1 -1
- package/dist/preflight_checks.js +37 -126
- package/dist/publish_steps.d.ts +2 -4
- package/dist/publish_steps.d.ts.map +1 -1
- package/dist/publish_steps.js +2 -6
- package/dist/publishing_event.d.ts +2 -8
- package/dist/publishing_event.d.ts.map +1 -1
- package/dist/publishing_event.js +5 -4
- package/dist/publishing_event_handler.d.ts +10 -8
- package/dist/publishing_event_handler.d.ts.map +1 -1
- package/dist/publishing_event_handler.js +14 -12
- package/dist/publishing_plan.d.ts +16 -1
- package/dist/publishing_plan.d.ts.map +1 -1
- package/dist/publishing_plan.js +328 -250
- package/dist/publishing_plan_logging.d.ts +1 -1
- package/dist/publishing_plan_logging.d.ts.map +1 -1
- package/dist/publishing_plan_logging.js +21 -14
- package/dist/repo.svelte.d.ts +7 -0
- package/dist/repo.svelte.d.ts.map +1 -1
- package/dist/repo.svelte.js +3 -0
- package/dist/repo_readiness.d.ts +210 -0
- package/dist/repo_readiness.d.ts.map +1 -0
- package/dist/repo_readiness.js +516 -0
- package/dist/repos_status.d.ts +3340 -0
- package/dist/repos_status.d.ts.map +1 -0
- package/dist/repos_status.js +555 -0
- package/dist/repos_status_load.d.ts +65 -0
- package/dist/repos_status_load.d.ts.map +1 -0
- package/dist/repos_status_load.js +131 -0
- package/package.json +7 -27
- package/src/lib/ci_reconcile.ts +7 -11
- package/src/lib/dependency_graph.ts +45 -97
- package/src/lib/dependency_updater.ts +7 -1
- package/src/lib/fetch_repo_data.ts +66 -29
- package/src/lib/fs_fetch_value_cache.ts +1 -1
- package/src/lib/git_operations.ts +15 -187
- package/src/lib/github.ts +20 -8
- package/src/lib/github_helpers.ts +5 -12
- package/src/lib/gitops_analyze.task.ts +79 -41
- package/src/lib/gitops_config.ts +88 -137
- package/src/lib/gitops_constants.ts +6 -4
- package/src/lib/gitops_plan.task.ts +113 -61
- package/src/lib/gitops_publish.task.ts +255 -122
- package/src/lib/gitops_run.task.ts +250 -178
- package/src/lib/gitops_sync.task.ts +208 -33
- package/src/lib/gitops_task_helpers.ts +166 -110
- package/src/lib/gitops_validate.task.ts +17 -27
- package/src/lib/graph_validation.ts +25 -86
- package/src/lib/local_repo.ts +109 -316
- package/src/lib/log_helpers.ts +8 -18
- package/src/lib/multi_repo_publisher.ts +138 -53
- package/src/lib/npm_registry.ts +37 -62
- package/src/lib/operations.ts +78 -150
- package/src/lib/operations_defaults.ts +108 -154
- package/src/lib/output_helpers.ts +55 -3
- package/src/lib/preflight_checks.ts +47 -183
- package/src/lib/publish_steps.ts +15 -13
- package/src/lib/publishing_event.ts +5 -4
- package/src/lib/publishing_event_handler.ts +18 -16
- package/src/lib/publishing_plan.ts +451 -287
- package/src/lib/publishing_plan_logging.ts +32 -23
- package/src/lib/repo.svelte.ts +8 -0
- package/src/lib/repo_readiness.ts +647 -0
- package/src/lib/repos_status.ts +671 -0
- package/src/lib/repos_status_load.ts +173 -0
- package/dist/config_reconcile.d.ts +0 -46
- package/dist/config_reconcile.d.ts.map +0 -1
- package/dist/config_reconcile.js +0 -64
- package/dist/paths.d.ts +0 -7
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -6
- package/dist/repo_ops.d.ts +0 -59
- package/dist/repo_ops.d.ts.map +0 -1
- package/dist/repo_ops.js +0 -170
- package/dist/resolved_gitops_config.d.ts +0 -9
- package/dist/resolved_gitops_config.d.ts.map +0 -1
- package/dist/resolved_gitops_config.js +0 -12
- package/src/lib/config_reconcile.ts +0 -98
- package/src/lib/paths.ts +0 -6
- package/src/lib/repo_ops.ts +0 -216
- package/src/lib/resolved_gitops_config.ts +0 -27
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
type DependencyUpdate,
|
|
12
12
|
type PublishingPlan
|
|
13
13
|
} from './publishing_plan.ts';
|
|
14
|
-
import
|
|
14
|
+
import { load_repos_status, to_repos_command } from './repos_status_load.ts';
|
|
15
|
+
import { check_publish_readiness } from './repo_readiness.ts';
|
|
16
|
+
import type { Result } from '@fuzdev/fuz_util/result.ts';
|
|
15
17
|
import type { GitopsOperations } from './operations.ts';
|
|
16
18
|
import { default_gitops_operations } from './operations_defaults.ts';
|
|
17
19
|
import { GITOPS_NPM_WAIT_TIMEOUT_DEFAULT } from './gitops_constants.ts';
|
|
@@ -23,7 +25,8 @@ import {
|
|
|
23
25
|
import {
|
|
24
26
|
type PublishingEventHandler,
|
|
25
27
|
capture_handler,
|
|
26
|
-
multi_handler
|
|
28
|
+
multi_handler,
|
|
29
|
+
redact_secrets
|
|
27
30
|
} from './publishing_event_handler.ts';
|
|
28
31
|
|
|
29
32
|
export interface PublishingOptions {
|
|
@@ -33,8 +36,18 @@ export interface PublishingOptions {
|
|
|
33
36
|
max_wait?: number;
|
|
34
37
|
log?: Logger;
|
|
35
38
|
ops?: GitopsOperations;
|
|
39
|
+
/** A `repos.toml` for the per-repo readiness re-check, when `repos` wouldn't find it from the cwd. */
|
|
40
|
+
registry?: string;
|
|
36
41
|
/** Structured event sink; defaults to capture-only (events surface on the result). */
|
|
37
42
|
events?: PublishingEventHandler;
|
|
43
|
+
/**
|
|
44
|
+
* Where `gro publish` and `gro deploy` show their stdout: ours, or our stderr
|
|
45
|
+
* when our stdout carries a machine-readable stream (`--emit_json`, or a JSON
|
|
46
|
+
* or markdown report written to stdout).
|
|
47
|
+
*
|
|
48
|
+
* @default 'stdout'
|
|
49
|
+
*/
|
|
50
|
+
child_stdout?: 'stdout' | 'stderr';
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
export interface PublishedVersion {
|
|
@@ -89,6 +102,7 @@ export const execute_publishing_plan = async (
|
|
|
89
102
|
): Promise<PublishingResult> => {
|
|
90
103
|
const start_time = Date.now();
|
|
91
104
|
const { wetrun, log, ops = default_gitops_operations } = options;
|
|
105
|
+
const version_strategy = options.version_strategy ?? 'caret';
|
|
92
106
|
|
|
93
107
|
// Only npm repos publish; drop any non-npm repos (e.g. cargo) so preflight and the
|
|
94
108
|
// executor's name lookup never touch them. The plan already excludes them too.
|
|
@@ -110,20 +124,16 @@ export const execute_publishing_plan = async (
|
|
|
110
124
|
events_handler.emit(event);
|
|
111
125
|
};
|
|
112
126
|
|
|
113
|
-
// Preflight checks (skip for dry runs since we're not actually publishing)
|
|
127
|
+
// Preflight checks (skip for dry runs since we're not actually publishing): it builds
|
|
128
|
+
// exactly the packages the plan publishes. Repo git state isn't preflight's:
|
|
129
|
+
// `gitops_publish --wetrun` gates on it before its confirmation prompt.
|
|
114
130
|
if (wetrun) {
|
|
115
|
-
const preflight_options: PreflightOptions = {
|
|
116
|
-
skip_changesets: false, // Always check for changesets
|
|
117
|
-
required_branch: 'main',
|
|
118
|
-
log
|
|
119
|
-
};
|
|
120
131
|
const preflight = await ops.preflight.run_preflight_checks({
|
|
121
132
|
repos,
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
version_changes: plan.version_changes,
|
|
134
|
+
log,
|
|
124
135
|
npm_ops: ops.npm,
|
|
125
|
-
build_ops: ops.build
|
|
126
|
-
changeset_ops: ops.changeset
|
|
136
|
+
build_ops: ops.build
|
|
127
137
|
});
|
|
128
138
|
|
|
129
139
|
if (!preflight.ok) {
|
|
@@ -173,6 +183,26 @@ export const execute_publishing_plan = async (
|
|
|
173
183
|
const repo = repo_by_name.get(pkg_name);
|
|
174
184
|
if (!repo) continue;
|
|
175
185
|
|
|
186
|
+
// Re-check the repo right before its `gro publish`: the gate ran before the prompt, and a
|
|
187
|
+
// cascade's npm waits leave a long window. `--no-pull` means gro won't notice origin
|
|
188
|
+
// moving until its push is rejected — after `changeset publish` put the version on npm —
|
|
189
|
+
// and its `commit -a` would sweep a tracked edit made since into the release commit.
|
|
190
|
+
// Aborting here, before any npm side effect, leaves the dirty state resumption expects.
|
|
191
|
+
if (wetrun) {
|
|
192
|
+
const recheck = await recheck_publish_readiness(repo, options.registry, ops);
|
|
193
|
+
if (!recheck.ok) {
|
|
194
|
+
const err = new Error(
|
|
195
|
+
`${pkg_name} isn't ready to publish, re-checked right before \`gro publish\`: ` +
|
|
196
|
+
`${recheck.message}. Aborting before it touches npm — fix it, then re-run ` +
|
|
197
|
+
`'gro gitops_publish --wetrun' to re-plan from the current state.`
|
|
198
|
+
);
|
|
199
|
+
failed.set(pkg_name, err);
|
|
200
|
+
emit({ event: 'package_failed', name: pkg_name, error: err.message, code: 'not_ready' });
|
|
201
|
+
log?.error(st('red', ` ❌ ${err.message}`));
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
176
206
|
// An earlier publish may have rewritten this package's dependency ranges. No install is
|
|
177
207
|
// needed here: `gro publish` runs its own install (which self-heals npm's stale-cache
|
|
178
208
|
// ETARGET), so the package's freshly-rewritten deps are installed and healed as part of
|
|
@@ -236,12 +266,9 @@ export const execute_publishing_plan = async (
|
|
|
236
266
|
});
|
|
237
267
|
|
|
238
268
|
if (!wait_result.ok) {
|
|
239
|
-
// Handle inline (don't throw into the generic catch):
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
const err = new Error(
|
|
243
|
-
`Failed to wait for package: ${wait_result.message}${wait_result.timeout ? ' (timeout)' : ''}`
|
|
244
|
-
);
|
|
269
|
+
// Handle inline (don't throw into the generic catch): a failed npm wait is a
|
|
270
|
+
// network failure, known by where it happened rather than by sniffing the message.
|
|
271
|
+
const err = new Error(`Failed to wait for package: ${wait_result.message}`);
|
|
245
272
|
failed.set(pkg_name, err);
|
|
246
273
|
emit({ event: 'package_failed', name: pkg_name, error: err.message, code: 'network' });
|
|
247
274
|
log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
|
|
@@ -251,8 +278,8 @@ export const execute_publishing_plan = async (
|
|
|
251
278
|
|
|
252
279
|
// 3. Update every dependent the plan says has a prod/peer dep on this package.
|
|
253
280
|
// This rewrites their package.json ranges and creates their auto-changeset,
|
|
254
|
-
// which a later step of this same pass publishes
|
|
255
|
-
//
|
|
281
|
+
// which a later step of this same pass publishes; its `gro publish` installs the
|
|
282
|
+
// rewritten deps then (see the top of the loop).
|
|
256
283
|
const dependent_updates = group_dependency_updates(
|
|
257
284
|
plan.dependency_updates,
|
|
258
285
|
published,
|
|
@@ -281,23 +308,15 @@ export const execute_publishing_plan = async (
|
|
|
281
308
|
});
|
|
282
309
|
}
|
|
283
310
|
changed_repos.add(dependent_name); // Mark as changed for deployment
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
// update-only leaf: rewrite ranges + commit, no changeset (it won't republish)
|
|
294
|
-
await update_package_json(dependent_repo, updates, {
|
|
295
|
-
strategy: options.version_strategy || 'caret',
|
|
296
|
-
log,
|
|
297
|
-
git_ops: ops.git,
|
|
298
|
-
fs_ops: ops.fs
|
|
299
|
-
});
|
|
300
|
-
}
|
|
311
|
+
await update_package_json(dependent_repo, updates, {
|
|
312
|
+
strategy: version_strategy,
|
|
313
|
+
// `published_versions` creates the auto-changeset; an update-only leaf gets
|
|
314
|
+
// its ranges rewritten and committed with no changeset (it won't republish)
|
|
315
|
+
published_versions: republishes ? published : undefined,
|
|
316
|
+
log,
|
|
317
|
+
git_ops: ops.git,
|
|
318
|
+
fs_ops: ops.fs
|
|
319
|
+
});
|
|
301
320
|
}
|
|
302
321
|
}
|
|
303
322
|
} catch (error) {
|
|
@@ -309,10 +328,11 @@ export const execute_publishing_plan = async (
|
|
|
309
328
|
error: err.message,
|
|
310
329
|
// TODO: emit a precise code once the npm/process ops return typed errors —
|
|
311
330
|
// today a publish-step cause lives in unstructured stderr, so use the honest
|
|
312
|
-
// coarse bucket rather than guessing
|
|
331
|
+
// coarse bucket rather than guessing a cause (auth, build, …) from the message.
|
|
313
332
|
code: 'publish'
|
|
314
333
|
});
|
|
315
|
-
|
|
334
|
+
// the first line alone: the stderr tail the rest repeats just streamed live
|
|
335
|
+
log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message.split('\n')[0]}`));
|
|
316
336
|
break; // Always fail fast on error
|
|
317
337
|
}
|
|
318
338
|
}
|
|
@@ -354,7 +374,7 @@ export const execute_publishing_plan = async (
|
|
|
354
374
|
// package.json but must NOT generate a changeset — dev-only changes redeploy
|
|
355
375
|
// (rebuild) without republishing, so they shouldn't bump the next release.
|
|
356
376
|
await update_package_json(repo, dev_updates, {
|
|
357
|
-
strategy:
|
|
377
|
+
strategy: version_strategy,
|
|
358
378
|
log,
|
|
359
379
|
git_ops: ops.git,
|
|
360
380
|
fs_ops: ops.fs
|
|
@@ -381,18 +401,26 @@ export const execute_publishing_plan = async (
|
|
|
381
401
|
// Build fresh (no --no-build): a deployed site bundles its dependencies, so it
|
|
382
402
|
// must be rebuilt against the versions this run just published — the preflight
|
|
383
403
|
// build ran against the old versions, before the cascade rewrote package.json.
|
|
384
|
-
const deploy_result = await ops.process.
|
|
404
|
+
const deploy_result = await ops.process.run_interactive({
|
|
385
405
|
cmd: 'gro',
|
|
386
406
|
args: ['deploy'],
|
|
387
|
-
cwd: repo.repo_dir
|
|
407
|
+
cwd: repo.repo_dir,
|
|
408
|
+
stdout: options.child_stdout
|
|
388
409
|
});
|
|
389
410
|
|
|
390
411
|
if (deploy_result.ok) {
|
|
391
412
|
emit({ event: 'deploy_completed', name: repo.library.name });
|
|
392
413
|
log?.info(st('green', ` ✅ Deployed ${repo.library.name}`));
|
|
393
414
|
} else {
|
|
394
|
-
emit({
|
|
395
|
-
|
|
415
|
+
emit({
|
|
416
|
+
event: 'deploy_failed',
|
|
417
|
+
name: repo.library.name,
|
|
418
|
+
error: format_run_failure(deploy_result)
|
|
419
|
+
});
|
|
420
|
+
// the stderr tail just streamed live, so the log names the failure alone
|
|
421
|
+
log?.warn(
|
|
422
|
+
st('yellow', ` ⚠️ Failed to deploy ${repo.library.name}: ${deploy_result.message}`)
|
|
423
|
+
);
|
|
396
424
|
}
|
|
397
425
|
} catch (error) {
|
|
398
426
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -469,10 +497,13 @@ export const execute_publishing_plan = async (
|
|
|
469
497
|
* Publishes a single repo using `gro publish`.
|
|
470
498
|
*
|
|
471
499
|
* Dry run mode: reports the precomputed plan entry without side effects.
|
|
472
|
-
* Real mode: runs `gro publish --no-build
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
500
|
+
* Real mode: runs `gro publish --no-build --no-pull --branch <entry branch>` (builds
|
|
501
|
+
* already validated in preflight; the readiness re-check just found the branch in sync
|
|
502
|
+
* with origin or ahead of it, so gro's own `git pull` is skipped, and gro checks out the branch the
|
|
503
|
+
* registry entry follows rather than its `main` default) in the foreground — its output
|
|
504
|
+
* live and stdin the terminal's, for npm's one-time-password prompt — reads the new version from
|
|
505
|
+
* `package.json`, and returns it alongside the plan's predicted bump metadata. The
|
|
506
|
+
* caller compares the read-back version to the plan to detect drift.
|
|
476
507
|
*
|
|
477
508
|
* @throws {Error} if the publish, version read-back, or commit-hash lookup fails
|
|
478
509
|
*/
|
|
@@ -498,15 +529,16 @@ const publish_single_repo = async (
|
|
|
498
529
|
};
|
|
499
530
|
}
|
|
500
531
|
|
|
501
|
-
|
|
502
|
-
const publish_result = await ops.process.spawn({
|
|
532
|
+
const publish_result = await ops.process.run_interactive({
|
|
503
533
|
cmd: 'gro',
|
|
504
|
-
args:
|
|
505
|
-
cwd: repo.repo_dir
|
|
534
|
+
args: gro_publish_args(repo),
|
|
535
|
+
cwd: repo.repo_dir,
|
|
536
|
+
stdout: options.child_stdout
|
|
506
537
|
});
|
|
507
538
|
|
|
508
539
|
if (!publish_result.ok) {
|
|
509
|
-
|
|
540
|
+
// no "Failed to publish" prefix: the caller's log and report name the package
|
|
541
|
+
throw new Error(format_run_failure(publish_result));
|
|
510
542
|
}
|
|
511
543
|
|
|
512
544
|
// Read the new version from package.json after gro publish
|
|
@@ -542,6 +574,59 @@ const publish_single_repo = async (
|
|
|
542
574
|
};
|
|
543
575
|
};
|
|
544
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Re-checks one repo's readiness right before its `gro publish`: `repos status
|
|
579
|
+
* --fetch --json <key>` through `ops.repos`, then `check_publish_readiness` on
|
|
580
|
+
* that entry — the gate's predicate, so a branch ahead of origin (the
|
|
581
|
+
* executor's own dependency-rewrite commit leaves a dependent ahead) is ready.
|
|
582
|
+
*
|
|
583
|
+
* @returns ok, or the problems found (or why the check couldn't run), joined
|
|
584
|
+
*/
|
|
585
|
+
const recheck_publish_readiness = async (
|
|
586
|
+
repo: LocalRepo,
|
|
587
|
+
registry: string | undefined,
|
|
588
|
+
ops: GitopsOperations
|
|
589
|
+
): Promise<Result<object, { message: string }>> => {
|
|
590
|
+
const key = repo.entry.key;
|
|
591
|
+
const loaded = await load_repos_status({
|
|
592
|
+
keys: [key],
|
|
593
|
+
registry,
|
|
594
|
+
fetch: true,
|
|
595
|
+
repos_ops: ops.repos
|
|
596
|
+
});
|
|
597
|
+
if (!loaded.ok) return { ok: false, message: `the check didn't run: ${loaded.message}` };
|
|
598
|
+
const checked = check_publish_readiness({
|
|
599
|
+
report: loaded.report,
|
|
600
|
+
keys: [key],
|
|
601
|
+
repos_command: to_repos_command(registry)
|
|
602
|
+
});
|
|
603
|
+
return checked.ok ? { ok: true } : { ok: false, message: checked.lines.join('; ') };
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* The `gro publish` arguments the executor runs in a repo: `--no-build`, since
|
|
608
|
+
* preflight validated the build; `--no-pull`, since the readiness re-check right
|
|
609
|
+
* before found the branch in sync with origin or ahead of it, so a pull would only
|
|
610
|
+
* move what the check vouched for; and `--branch` naming the branch the registry entry follows,
|
|
611
|
+
* which gro checks out (its default is `main`).
|
|
612
|
+
*/
|
|
613
|
+
const gro_publish_args = (repo: Pick<LocalRepo, 'entry'>): Array<string> => {
|
|
614
|
+
const args = ['publish', '--no-build', '--no-pull'];
|
|
615
|
+
if (repo.entry.branch !== null) args.push('--branch', repo.entry.branch);
|
|
616
|
+
return args;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* The failure message for a command the executor ran: why it failed on the first
|
|
621
|
+
* line, then the end of its stderr with known secret shapes redacted, since the
|
|
622
|
+
* message lands in the result, the event stream, and the JSON or markdown report.
|
|
623
|
+
* The live log shows the first line alone, the stderr having just streamed.
|
|
624
|
+
*/
|
|
625
|
+
const format_run_failure = (failure: { message: string; stderr_tail?: string }): string =>
|
|
626
|
+
failure.stderr_tail
|
|
627
|
+
? `${failure.message}\nthe end of its stderr:\n${redact_secrets(failure.stderr_tail)}`
|
|
628
|
+
: failure.message;
|
|
629
|
+
|
|
545
630
|
/**
|
|
546
631
|
* Groups dependency updates by dependent package — `dependent → (dependency → new
|
|
547
632
|
* version)`, the shape `update_package_json` consumes. Restricted by `predicate` (e.g.
|
package/src/lib/npm_registry.ts
CHANGED
|
@@ -11,28 +11,44 @@ export interface WaitOptions {
|
|
|
11
11
|
timeout?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The side effects of the registry checks — running npm, sleeping, and reading
|
|
16
|
+
* the clock — injected so tests drive them with plain objects.
|
|
17
|
+
*/
|
|
18
|
+
export interface NpmRegistryDeps {
|
|
19
|
+
/**
|
|
20
|
+
* Runs `npm` with `args` and resolves to what it wrote to stdout — empty when
|
|
21
|
+
* nothing, or when npm couldn't run. A throw counts as unavailable too.
|
|
22
|
+
*/
|
|
23
|
+
run_npm: (args: Array<string>) => Promise<string>;
|
|
24
|
+
/** Sleeps for `ms` milliseconds. */
|
|
25
|
+
wait: (ms: number) => Promise<void>;
|
|
26
|
+
/** The current time in milliseconds, as `Date.now`. */
|
|
27
|
+
now: () => number;
|
|
17
28
|
}
|
|
18
29
|
|
|
30
|
+
export const default_npm_registry_deps: NpmRegistryDeps = {
|
|
31
|
+
run_npm: async (args) => (await spawn_out('npm', args)).stdout ?? '',
|
|
32
|
+
wait,
|
|
33
|
+
now: () => Date.now()
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks whether `pkg@version` is on the npm registry, by `npm view`.
|
|
38
|
+
*
|
|
39
|
+
* @returns `true` when the registry reports that exact version, `false` when it
|
|
40
|
+
* doesn't or npm couldn't run
|
|
41
|
+
*/
|
|
19
42
|
export const check_package_available = async (
|
|
20
43
|
pkg: string,
|
|
21
44
|
version: string,
|
|
22
|
-
options: { log?: Logger } = {}
|
|
45
|
+
options: { log?: Logger } = {},
|
|
46
|
+
deps: NpmRegistryDeps = default_npm_registry_deps
|
|
23
47
|
): Promise<boolean> => {
|
|
24
48
|
const { log } = options;
|
|
25
49
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (result.stdout) {
|
|
30
|
-
const output = result.stdout.trim();
|
|
31
|
-
// If we get a version back, it exists
|
|
32
|
-
return output === version;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return false;
|
|
50
|
+
const stdout = await deps.run_npm(['view', `${pkg}@${version}`, 'version']);
|
|
51
|
+
return stdout.trim() === version;
|
|
36
52
|
} catch (error) {
|
|
37
53
|
log?.debug(`Failed to check ${pkg}@${version}: ${error}`);
|
|
38
54
|
return false;
|
|
@@ -57,7 +73,8 @@ export const check_package_available = async (
|
|
|
57
73
|
export const wait_for_package = async (
|
|
58
74
|
pkg: string,
|
|
59
75
|
version: string,
|
|
60
|
-
options: WaitOptions = {}
|
|
76
|
+
options: WaitOptions = {},
|
|
77
|
+
deps: NpmRegistryDeps = default_npm_registry_deps
|
|
61
78
|
): Promise<void> => {
|
|
62
79
|
const {
|
|
63
80
|
log,
|
|
@@ -67,34 +84,31 @@ export const wait_for_package = async (
|
|
|
67
84
|
timeout = 300000 // 5 minutes default
|
|
68
85
|
} = options;
|
|
69
86
|
|
|
70
|
-
const start_time =
|
|
87
|
+
const start_time = deps.now();
|
|
71
88
|
let attempt = 0;
|
|
72
89
|
let delay = initial_delay;
|
|
73
90
|
|
|
74
91
|
while (attempt < max_attempts) {
|
|
75
92
|
attempt++;
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
if (Date.now() - start_time > timeout) {
|
|
94
|
+
if (deps.now() - start_time > timeout) {
|
|
79
95
|
throw new Error(`Timeout waiting for ${pkg}@${version} after ${timeout}ms`);
|
|
80
96
|
}
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (await check_package_available(pkg, version, { log })) {
|
|
98
|
+
if (await check_package_available(pkg, version, { log }, deps)) {
|
|
85
99
|
log?.info(st('green', ` ✓ ${pkg}@${version} is now available on NPM`));
|
|
86
100
|
return;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
// Log progress occasionally
|
|
90
|
-
if (attempt
|
|
104
|
+
if (attempt % 5 === 0) {
|
|
91
105
|
log?.info(st('dim', ` Still waiting... (attempt ${attempt}/${max_attempts})`));
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
// Wait with exponential backoff + jitter
|
|
95
109
|
const jitter = Math.random() * delay * 0.1; // 10% jitter
|
|
96
110
|
const actual_delay = Math.min(delay + jitter, max_delay);
|
|
97
|
-
await wait(actual_delay);
|
|
111
|
+
await deps.wait(actual_delay);
|
|
98
112
|
|
|
99
113
|
// Exponential backoff
|
|
100
114
|
delay = Math.min(delay * 1.5, max_delay);
|
|
@@ -102,42 +116,3 @@ export const wait_for_package = async (
|
|
|
102
116
|
|
|
103
117
|
throw new Error(`${pkg}@${version} not available after ${max_attempts} attempts`);
|
|
104
118
|
};
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Fetches package metadata from NPM registry.
|
|
108
|
-
*
|
|
109
|
-
* Returns name and latest version. Returns null if package doesn't exist
|
|
110
|
-
* or registry is unreachable.
|
|
111
|
-
*
|
|
112
|
-
* @returns package info or null on error/not found
|
|
113
|
-
*/
|
|
114
|
-
export const get_package_info = async (
|
|
115
|
-
pkg: string,
|
|
116
|
-
options: { log?: Logger } = {}
|
|
117
|
-
): Promise<PackageInfo | null> => {
|
|
118
|
-
const { log } = options;
|
|
119
|
-
try {
|
|
120
|
-
const result = await spawn_out('npm', ['view', pkg, '--json']);
|
|
121
|
-
|
|
122
|
-
if (result.stdout) {
|
|
123
|
-
const data = JSON.parse(result.stdout);
|
|
124
|
-
return {
|
|
125
|
-
name: data.name,
|
|
126
|
-
version: data.version
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return null;
|
|
131
|
-
} catch (error) {
|
|
132
|
-
log?.debug(`Failed to get package info for ${pkg}: ${error}`);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export const package_exists = async (
|
|
138
|
-
pkg: string,
|
|
139
|
-
options: { log?: Logger } = {}
|
|
140
|
-
): Promise<boolean> => {
|
|
141
|
-
const info = await get_package_info(pkg, options);
|
|
142
|
-
return info !== null;
|
|
143
|
-
};
|