@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.
Files changed (179) hide show
  1. package/README.md +78 -24
  2. package/dist/ModulesDetail.svelte +15 -22
  3. package/dist/ModulesDetail.svelte.d.ts.map +1 -1
  4. package/dist/ModulesNav.svelte +4 -1
  5. package/dist/ModulesPage.svelte +1 -3
  6. package/dist/ModulesPage.svelte.d.ts.map +1 -1
  7. package/dist/PageHeader.svelte +2 -2
  8. package/dist/PageHeader.svelte.d.ts +0 -3
  9. package/dist/PageHeader.svelte.d.ts.map +1 -1
  10. package/dist/PullRequestsDetail.svelte +10 -9
  11. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
  12. package/dist/PullRequestsPage.svelte +1 -3
  13. package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
  14. package/dist/ReposTable.svelte +27 -48
  15. package/dist/ReposTable.svelte.d.ts.map +1 -1
  16. package/dist/ReposTree.svelte +5 -19
  17. package/dist/ReposTree.svelte.d.ts.map +1 -1
  18. package/dist/ReposTreeNav.svelte +5 -7
  19. package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
  20. package/dist/TablePage.svelte +1 -3
  21. package/dist/TablePage.svelte.d.ts.map +1 -1
  22. package/dist/TreeItemPage.svelte +1 -3
  23. package/dist/TreeItemPage.svelte.d.ts.map +1 -1
  24. package/dist/TreePage.svelte +1 -3
  25. package/dist/TreePage.svelte.d.ts.map +1 -1
  26. package/dist/ci_reconcile.d.ts +6 -10
  27. package/dist/ci_reconcile.d.ts.map +1 -1
  28. package/dist/ci_reconcile.js +6 -8
  29. package/dist/dependency_graph.d.ts +26 -49
  30. package/dist/dependency_graph.d.ts.map +1 -1
  31. package/dist/dependency_graph.js +37 -80
  32. package/dist/dependency_updater.d.ts +1 -1
  33. package/dist/dependency_updater.d.ts.map +1 -1
  34. package/dist/dependency_updater.js +6 -1
  35. package/dist/fetch_repo_data.d.ts +34 -8
  36. package/dist/fetch_repo_data.d.ts.map +1 -1
  37. package/dist/fetch_repo_data.js +44 -22
  38. package/dist/fs_fetch_value_cache.d.ts +1 -1
  39. package/dist/fs_fetch_value_cache.js +1 -1
  40. package/dist/git_operations.d.ts +6 -49
  41. package/dist/git_operations.d.ts.map +1 -1
  42. package/dist/git_operations.js +13 -126
  43. package/dist/github.d.ts +17 -2
  44. package/dist/github.d.ts.map +1 -1
  45. package/dist/github.js +15 -6
  46. package/dist/github_helpers.d.ts.map +1 -1
  47. package/dist/github_helpers.js +3 -11
  48. package/dist/gitops_analyze.task.d.ts +24 -2
  49. package/dist/gitops_analyze.task.d.ts.map +1 -1
  50. package/dist/gitops_analyze.task.js +43 -30
  51. package/dist/gitops_config.d.ts +34 -79
  52. package/dist/gitops_config.d.ts.map +1 -1
  53. package/dist/gitops_config.js +76 -53
  54. package/dist/gitops_constants.d.ts +6 -4
  55. package/dist/gitops_constants.d.ts.map +1 -1
  56. package/dist/gitops_constants.js +6 -4
  57. package/dist/gitops_plan.task.d.ts +29 -3
  58. package/dist/gitops_plan.task.d.ts.map +1 -1
  59. package/dist/gitops_plan.task.js +80 -58
  60. package/dist/gitops_publish.task.d.ts +62 -2
  61. package/dist/gitops_publish.task.d.ts.map +1 -1
  62. package/dist/gitops_publish.task.js +187 -102
  63. package/dist/gitops_run.task.d.ts +74 -0
  64. package/dist/gitops_run.task.d.ts.map +1 -1
  65. package/dist/gitops_run.task.js +174 -160
  66. package/dist/gitops_sync.task.d.ts +65 -3
  67. package/dist/gitops_sync.task.d.ts.map +1 -1
  68. package/dist/gitops_sync.task.js +154 -30
  69. package/dist/gitops_task_helpers.d.ts +86 -59
  70. package/dist/gitops_task_helpers.d.ts.map +1 -1
  71. package/dist/gitops_task_helpers.js +120 -79
  72. package/dist/gitops_validate.task.d.ts +1 -2
  73. package/dist/gitops_validate.task.d.ts.map +1 -1
  74. package/dist/gitops_validate.task.js +15 -22
  75. package/dist/graph_validation.d.ts +16 -27
  76. package/dist/graph_validation.d.ts.map +1 -1
  77. package/dist/graph_validation.js +23 -64
  78. package/dist/local_repo.d.ts +50 -62
  79. package/dist/local_repo.d.ts.map +1 -1
  80. package/dist/local_repo.js +80 -193
  81. package/dist/log_helpers.d.ts +8 -8
  82. package/dist/log_helpers.d.ts.map +1 -1
  83. package/dist/multi_repo_publisher.d.ts +10 -0
  84. package/dist/multi_repo_publisher.d.ts.map +1 -1
  85. package/dist/multi_repo_publisher.js +115 -51
  86. package/dist/npm_registry.d.ts +23 -19
  87. package/dist/npm_registry.d.ts.map +1 -1
  88. package/dist/npm_registry.js +20 -47
  89. package/dist/operations.d.ts +75 -180
  90. package/dist/operations.d.ts.map +1 -1
  91. package/dist/operations.js +15 -7
  92. package/dist/operations_defaults.d.ts +17 -2
  93. package/dist/operations_defaults.d.ts.map +1 -1
  94. package/dist/operations_defaults.js +104 -121
  95. package/dist/output_helpers.d.ts +36 -0
  96. package/dist/output_helpers.d.ts.map +1 -1
  97. package/dist/output_helpers.js +37 -3
  98. package/dist/preflight_checks.d.ts +20 -27
  99. package/dist/preflight_checks.d.ts.map +1 -1
  100. package/dist/preflight_checks.js +37 -126
  101. package/dist/publish_steps.d.ts +2 -4
  102. package/dist/publish_steps.d.ts.map +1 -1
  103. package/dist/publish_steps.js +2 -6
  104. package/dist/publishing_event.d.ts +2 -8
  105. package/dist/publishing_event.d.ts.map +1 -1
  106. package/dist/publishing_event.js +5 -4
  107. package/dist/publishing_event_handler.d.ts +10 -8
  108. package/dist/publishing_event_handler.d.ts.map +1 -1
  109. package/dist/publishing_event_handler.js +14 -12
  110. package/dist/publishing_plan.d.ts +16 -1
  111. package/dist/publishing_plan.d.ts.map +1 -1
  112. package/dist/publishing_plan.js +328 -250
  113. package/dist/publishing_plan_logging.d.ts +1 -1
  114. package/dist/publishing_plan_logging.d.ts.map +1 -1
  115. package/dist/publishing_plan_logging.js +21 -14
  116. package/dist/repo.svelte.d.ts +7 -0
  117. package/dist/repo.svelte.d.ts.map +1 -1
  118. package/dist/repo.svelte.js +3 -0
  119. package/dist/repo_readiness.d.ts +210 -0
  120. package/dist/repo_readiness.d.ts.map +1 -0
  121. package/dist/repo_readiness.js +516 -0
  122. package/dist/repos_status.d.ts +3340 -0
  123. package/dist/repos_status.d.ts.map +1 -0
  124. package/dist/repos_status.js +555 -0
  125. package/dist/repos_status_load.d.ts +65 -0
  126. package/dist/repos_status_load.d.ts.map +1 -0
  127. package/dist/repos_status_load.js +131 -0
  128. package/package.json +7 -27
  129. package/src/lib/ci_reconcile.ts +7 -11
  130. package/src/lib/dependency_graph.ts +45 -97
  131. package/src/lib/dependency_updater.ts +7 -1
  132. package/src/lib/fetch_repo_data.ts +66 -29
  133. package/src/lib/fs_fetch_value_cache.ts +1 -1
  134. package/src/lib/git_operations.ts +15 -187
  135. package/src/lib/github.ts +20 -8
  136. package/src/lib/github_helpers.ts +5 -12
  137. package/src/lib/gitops_analyze.task.ts +79 -41
  138. package/src/lib/gitops_config.ts +88 -137
  139. package/src/lib/gitops_constants.ts +6 -4
  140. package/src/lib/gitops_plan.task.ts +113 -61
  141. package/src/lib/gitops_publish.task.ts +255 -122
  142. package/src/lib/gitops_run.task.ts +250 -178
  143. package/src/lib/gitops_sync.task.ts +208 -33
  144. package/src/lib/gitops_task_helpers.ts +166 -110
  145. package/src/lib/gitops_validate.task.ts +17 -27
  146. package/src/lib/graph_validation.ts +25 -86
  147. package/src/lib/local_repo.ts +109 -316
  148. package/src/lib/log_helpers.ts +8 -18
  149. package/src/lib/multi_repo_publisher.ts +138 -53
  150. package/src/lib/npm_registry.ts +37 -62
  151. package/src/lib/operations.ts +78 -150
  152. package/src/lib/operations_defaults.ts +108 -154
  153. package/src/lib/output_helpers.ts +55 -3
  154. package/src/lib/preflight_checks.ts +47 -183
  155. package/src/lib/publish_steps.ts +15 -13
  156. package/src/lib/publishing_event.ts +5 -4
  157. package/src/lib/publishing_event_handler.ts +18 -16
  158. package/src/lib/publishing_plan.ts +451 -287
  159. package/src/lib/publishing_plan_logging.ts +32 -23
  160. package/src/lib/repo.svelte.ts +8 -0
  161. package/src/lib/repo_readiness.ts +647 -0
  162. package/src/lib/repos_status.ts +671 -0
  163. package/src/lib/repos_status_load.ts +173 -0
  164. package/dist/config_reconcile.d.ts +0 -46
  165. package/dist/config_reconcile.d.ts.map +0 -1
  166. package/dist/config_reconcile.js +0 -64
  167. package/dist/paths.d.ts +0 -7
  168. package/dist/paths.d.ts.map +0 -1
  169. package/dist/paths.js +0 -6
  170. package/dist/repo_ops.d.ts +0 -59
  171. package/dist/repo_ops.d.ts.map +0 -1
  172. package/dist/repo_ops.js +0 -170
  173. package/dist/resolved_gitops_config.d.ts +0 -9
  174. package/dist/resolved_gitops_config.d.ts.map +0 -1
  175. package/dist/resolved_gitops_config.js +0 -12
  176. package/src/lib/config_reconcile.ts +0 -98
  177. package/src/lib/paths.ts +0 -6
  178. package/src/lib/repo_ops.ts +0 -216
  179. package/src/lib/resolved_gitops_config.ts +0 -27
@@ -4,10 +4,12 @@ import { styleText as st } from 'node:util';
4
4
  import { repo_is_npm } from './local_repo.js';
5
5
  import { update_package_json } from './dependency_updater.js';
6
6
  import { generate_publishing_plan } from './publishing_plan.js';
7
+ import { load_repos_status, to_repos_command } from './repos_status_load.js';
8
+ import { check_publish_readiness } from './repo_readiness.js';
7
9
  import { default_gitops_operations } from './operations_defaults.js';
8
10
  import { GITOPS_NPM_WAIT_TIMEOUT_DEFAULT } from './gitops_constants.js';
9
11
  import { summarize_events } from './publishing_event.js';
10
- import { capture_handler, multi_handler } from './publishing_event_handler.js';
12
+ import { capture_handler, multi_handler, redact_secrets } from './publishing_event_handler.js';
11
13
  export const publish_repos = async (repos, options) => {
12
14
  const { log, ops = default_gitops_operations } = options;
13
15
  // Convenience entry: generate the plan, then execute it. Callers that already hold a
@@ -27,6 +29,7 @@ export const publish_repos = async (repos, options) => {
27
29
  export const execute_publishing_plan = async (all_repos, plan, options) => {
28
30
  const start_time = Date.now();
29
31
  const { wetrun, log, ops = default_gitops_operations } = options;
32
+ const version_strategy = options.version_strategy ?? 'caret';
30
33
  // Only npm repos publish; drop any non-npm repos (e.g. cargo) so preflight and the
31
34
  // executor's name lookup never touch them. The plan already excludes them too.
32
35
  const repos = all_repos.filter(repo_is_npm);
@@ -42,20 +45,16 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
42
45
  const emit = (event) => {
43
46
  events_handler.emit(event);
44
47
  };
45
- // Preflight checks (skip for dry runs since we're not actually publishing)
48
+ // Preflight checks (skip for dry runs since we're not actually publishing): it builds
49
+ // exactly the packages the plan publishes. Repo git state isn't preflight's:
50
+ // `gitops_publish --wetrun` gates on it before its confirmation prompt.
46
51
  if (wetrun) {
47
- const preflight_options = {
48
- skip_changesets: false, // Always check for changesets
49
- required_branch: 'main',
50
- log
51
- };
52
52
  const preflight = await ops.preflight.run_preflight_checks({
53
53
  repos,
54
- preflight_options,
55
- git_ops: ops.git,
54
+ version_changes: plan.version_changes,
55
+ log,
56
56
  npm_ops: ops.npm,
57
- build_ops: ops.build,
58
- changeset_ops: ops.changeset
57
+ build_ops: ops.build
59
58
  });
60
59
  if (!preflight.ok) {
61
60
  throw new TaskError(`Preflight checks failed: ${preflight.errors.join(', ')}`);
@@ -96,6 +95,23 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
96
95
  const repo = repo_by_name.get(pkg_name);
97
96
  if (!repo)
98
97
  continue;
98
+ // Re-check the repo right before its `gro publish`: the gate ran before the prompt, and a
99
+ // cascade's npm waits leave a long window. `--no-pull` means gro won't notice origin
100
+ // moving until its push is rejected — after `changeset publish` put the version on npm —
101
+ // and its `commit -a` would sweep a tracked edit made since into the release commit.
102
+ // Aborting here, before any npm side effect, leaves the dirty state resumption expects.
103
+ if (wetrun) {
104
+ const recheck = await recheck_publish_readiness(repo, options.registry, ops);
105
+ if (!recheck.ok) {
106
+ const err = new Error(`${pkg_name} isn't ready to publish, re-checked right before \`gro publish\`: ` +
107
+ `${recheck.message}. Aborting before it touches npm — fix it, then re-run ` +
108
+ `'gro gitops_publish --wetrun' to re-plan from the current state.`);
109
+ failed.set(pkg_name, err);
110
+ emit({ event: 'package_failed', name: pkg_name, error: err.message, code: 'not_ready' });
111
+ log?.error(st('red', ` ❌ ${err.message}`));
112
+ break;
113
+ }
114
+ }
99
115
  // An earlier publish may have rewritten this package's dependency ranges. No install is
100
116
  // needed here: `gro publish` runs its own install (which self-heals npm's stale-cache
101
117
  // ETARGET), so the package's freshly-rewritten deps are installed and healed as part of
@@ -146,10 +162,9 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
146
162
  log
147
163
  });
148
164
  if (!wait_result.ok) {
149
- // Handle inline (don't throw into the generic catch): the npm-wait failure
150
- // carries a typed `timeout` signal, so we know this is a network failure
151
- // without sniffing the message.
152
- const err = new Error(`Failed to wait for package: ${wait_result.message}${wait_result.timeout ? ' (timeout)' : ''}`);
165
+ // Handle inline (don't throw into the generic catch): a failed npm wait is a
166
+ // network failure, known by where it happened rather than by sniffing the message.
167
+ const err = new Error(`Failed to wait for package: ${wait_result.message}`);
153
168
  failed.set(pkg_name, err);
154
169
  emit({ event: 'package_failed', name: pkg_name, error: err.message, code: 'network' });
155
170
  log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
@@ -158,8 +173,8 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
158
173
  emit({ event: 'npm_waited', name: pkg_name, version: version.new_version });
159
174
  // 3. Update every dependent the plan says has a prod/peer dep on this package.
160
175
  // This rewrites their package.json ranges and creates their auto-changeset,
161
- // which a later step of this same pass publishes. The dependent is queued for
162
- // a single install just before it publishes (see the top of the loop).
176
+ // which a later step of this same pass publishes; its `gro publish` installs the
177
+ // rewritten deps then (see the top of the loop).
163
178
  const dependent_updates = group_dependency_updates(plan.dependency_updates, published, (update) => update.updated_dependency === pkg_name &&
164
179
  (update.type === 'dependencies' || update.type === 'peerDependencies'));
165
180
  for (const [dependent_name, updates] of dependent_updates) {
@@ -184,24 +199,15 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
184
199
  });
185
200
  }
186
201
  changed_repos.add(dependent_name); // Mark as changed for deployment
187
- if (republishes) {
188
- await update_package_json(dependent_repo, updates, {
189
- strategy: options.version_strategy || 'caret',
190
- published_versions: published, // creates the auto-changeset
191
- log,
192
- git_ops: ops.git,
193
- fs_ops: ops.fs
194
- });
195
- }
196
- else {
197
- // update-only leaf: rewrite ranges + commit, no changeset (it won't republish)
198
- await update_package_json(dependent_repo, updates, {
199
- strategy: options.version_strategy || 'caret',
200
- log,
201
- git_ops: ops.git,
202
- fs_ops: ops.fs
203
- });
204
- }
202
+ await update_package_json(dependent_repo, updates, {
203
+ strategy: version_strategy,
204
+ // `published_versions` creates the auto-changeset; an update-only leaf gets
205
+ // its ranges rewritten and committed with no changeset (it won't republish)
206
+ published_versions: republishes ? published : undefined,
207
+ log,
208
+ git_ops: ops.git,
209
+ fs_ops: ops.fs
210
+ });
205
211
  }
206
212
  }
207
213
  }
@@ -214,10 +220,11 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
214
220
  error: err.message,
215
221
  // TODO: emit a precise code once the npm/process ops return typed errors —
216
222
  // today a publish-step cause lives in unstructured stderr, so use the honest
217
- // coarse bucket rather than guessing 'auth'/'network'/'build' from the message.
223
+ // coarse bucket rather than guessing a cause (auth, build, …) from the message.
218
224
  code: 'publish'
219
225
  });
220
- log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message}`));
226
+ // the first line alone: the stderr tail the rest repeats just streamed live
227
+ log?.error(st('red', ` ❌ Failed to publish ${pkg_name}: ${err.message.split('\n')[0]}`));
221
228
  break; // Always fail fast on error
222
229
  }
223
230
  }
@@ -252,7 +259,7 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
252
259
  // package.json but must NOT generate a changeset — dev-only changes redeploy
253
260
  // (rebuild) without republishing, so they shouldn't bump the next release.
254
261
  await update_package_json(repo, dev_updates, {
255
- strategy: options.version_strategy || 'caret',
262
+ strategy: version_strategy,
256
263
  log,
257
264
  git_ops: ops.git,
258
265
  fs_ops: ops.fs
@@ -275,18 +282,24 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
275
282
  // Build fresh (no --no-build): a deployed site bundles its dependencies, so it
276
283
  // must be rebuilt against the versions this run just published — the preflight
277
284
  // build ran against the old versions, before the cascade rewrote package.json.
278
- const deploy_result = await ops.process.spawn({
285
+ const deploy_result = await ops.process.run_interactive({
279
286
  cmd: 'gro',
280
287
  args: ['deploy'],
281
- cwd: repo.repo_dir
288
+ cwd: repo.repo_dir,
289
+ stdout: options.child_stdout
282
290
  });
283
291
  if (deploy_result.ok) {
284
292
  emit({ event: 'deploy_completed', name: repo.library.name });
285
293
  log?.info(st('green', ` ✅ Deployed ${repo.library.name}`));
286
294
  }
287
295
  else {
288
- emit({ event: 'deploy_failed', name: repo.library.name, error: deploy_result.message });
289
- log?.warn(st('yellow', ` ⚠️ Failed to deploy ${repo.library.name}`));
296
+ emit({
297
+ event: 'deploy_failed',
298
+ name: repo.library.name,
299
+ error: format_run_failure(deploy_result)
300
+ });
301
+ // the stderr tail just streamed live, so the log names the failure alone
302
+ log?.warn(st('yellow', ` ⚠️ Failed to deploy ${repo.library.name}: ${deploy_result.message}`));
290
303
  }
291
304
  }
292
305
  catch (error) {
@@ -351,10 +364,13 @@ export const execute_publishing_plan = async (all_repos, plan, options) => {
351
364
  * Publishes a single repo using `gro publish`.
352
365
  *
353
366
  * Dry run mode: reports the precomputed plan entry without side effects.
354
- * Real mode: runs `gro publish --no-build` (builds already validated in preflight),
355
- * reads the new version from `package.json`, and returns it alongside the plan's
356
- * predicted bump metadata. The caller compares the read-back version to the plan to
357
- * detect drift.
367
+ * Real mode: runs `gro publish --no-build --no-pull --branch <entry branch>` (builds
368
+ * already validated in preflight; the readiness re-check just found the branch in sync
369
+ * with origin or ahead of it, so gro's own `git pull` is skipped, and gro checks out the branch the
370
+ * registry entry follows rather than its `main` default) in the foreground — its output
371
+ * live and stdin the terminal's, for npm's one-time-password prompt — reads the new version from
372
+ * `package.json`, and returns it alongside the plan's predicted bump metadata. The
373
+ * caller compares the read-back version to the plan to detect drift.
358
374
  *
359
375
  * @throws {Error} if the publish, version read-back, or commit-hash lookup fails
360
376
  */
@@ -373,14 +389,15 @@ const publish_single_repo = async (repo, options, ops, planned) => {
373
389
  tag: `v${planned.to}`
374
390
  };
375
391
  }
376
- // Run gro publish with --no-build (builds were validated in preflight checks)
377
- const publish_result = await ops.process.spawn({
392
+ const publish_result = await ops.process.run_interactive({
378
393
  cmd: 'gro',
379
- args: ['publish', '--no-build'],
380
- cwd: repo.repo_dir
394
+ args: gro_publish_args(repo),
395
+ cwd: repo.repo_dir,
396
+ stdout: options.child_stdout
381
397
  });
382
398
  if (!publish_result.ok) {
383
- throw new Error(`Failed to publish ${repo.library.name}: ${publish_result.message}`);
399
+ // no "Failed to publish" prefix: the caller's log and report name the package
400
+ throw new Error(format_run_failure(publish_result));
384
401
  }
385
402
  // Read the new version from package.json after gro publish
386
403
  const package_json_path = join(repo.repo_dir, 'package.json');
@@ -408,6 +425,53 @@ const publish_single_repo = async (repo, options, ops, planned) => {
408
425
  tag: `v${new_version}`
409
426
  };
410
427
  };
428
+ /**
429
+ * Re-checks one repo's readiness right before its `gro publish`: `repos status
430
+ * --fetch --json <key>` through `ops.repos`, then `check_publish_readiness` on
431
+ * that entry — the gate's predicate, so a branch ahead of origin (the
432
+ * executor's own dependency-rewrite commit leaves a dependent ahead) is ready.
433
+ *
434
+ * @returns ok, or the problems found (or why the check couldn't run), joined
435
+ */
436
+ const recheck_publish_readiness = async (repo, registry, ops) => {
437
+ const key = repo.entry.key;
438
+ const loaded = await load_repos_status({
439
+ keys: [key],
440
+ registry,
441
+ fetch: true,
442
+ repos_ops: ops.repos
443
+ });
444
+ if (!loaded.ok)
445
+ return { ok: false, message: `the check didn't run: ${loaded.message}` };
446
+ const checked = check_publish_readiness({
447
+ report: loaded.report,
448
+ keys: [key],
449
+ repos_command: to_repos_command(registry)
450
+ });
451
+ return checked.ok ? { ok: true } : { ok: false, message: checked.lines.join('; ') };
452
+ };
453
+ /**
454
+ * The `gro publish` arguments the executor runs in a repo: `--no-build`, since
455
+ * preflight validated the build; `--no-pull`, since the readiness re-check right
456
+ * before found the branch in sync with origin or ahead of it, so a pull would only
457
+ * move what the check vouched for; and `--branch` naming the branch the registry entry follows,
458
+ * which gro checks out (its default is `main`).
459
+ */
460
+ const gro_publish_args = (repo) => {
461
+ const args = ['publish', '--no-build', '--no-pull'];
462
+ if (repo.entry.branch !== null)
463
+ args.push('--branch', repo.entry.branch);
464
+ return args;
465
+ };
466
+ /**
467
+ * The failure message for a command the executor ran: why it failed on the first
468
+ * line, then the end of its stderr with known secret shapes redacted, since the
469
+ * message lands in the result, the event stream, and the JSON or markdown report.
470
+ * The live log shows the first line alone, the stderr having just streamed.
471
+ */
472
+ const format_run_failure = (failure) => failure.stderr_tail
473
+ ? `${failure.message}\nthe end of its stderr:\n${redact_secrets(failure.stderr_tail)}`
474
+ : failure.message;
411
475
  /**
412
476
  * Groups dependency updates by dependent package — `dependent → (dependency → new
413
477
  * version)`, the shape `update_package_json` consumes. Restricted by `predicate` (e.g.
@@ -6,13 +6,31 @@ export interface WaitOptions {
6
6
  max_delay?: number;
7
7
  timeout?: number;
8
8
  }
9
- export interface PackageInfo {
10
- name: string;
11
- version: string;
9
+ /**
10
+ * The side effects of the registry checks — running npm, sleeping, and reading
11
+ * the clock — injected so tests drive them with plain objects.
12
+ */
13
+ export interface NpmRegistryDeps {
14
+ /**
15
+ * Runs `npm` with `args` and resolves to what it wrote to stdout — empty when
16
+ * nothing, or when npm couldn't run. A throw counts as unavailable too.
17
+ */
18
+ run_npm: (args: Array<string>) => Promise<string>;
19
+ /** Sleeps for `ms` milliseconds. */
20
+ wait: (ms: number) => Promise<void>;
21
+ /** The current time in milliseconds, as `Date.now`. */
22
+ now: () => number;
12
23
  }
24
+ export declare const default_npm_registry_deps: NpmRegistryDeps;
25
+ /**
26
+ * Checks whether `pkg@version` is on the npm registry, by `npm view`.
27
+ *
28
+ * @returns `true` when the registry reports that exact version, `false` when it
29
+ * doesn't or npm couldn't run
30
+ */
13
31
  export declare const check_package_available: (pkg: string, version: string, options?: {
14
32
  log?: Logger;
15
- }) => Promise<boolean>;
33
+ }, deps?: NpmRegistryDeps) => Promise<boolean>;
16
34
  /**
17
35
  * Waits for package version to propagate to NPM registry.
18
36
  *
@@ -28,19 +46,5 @@ export declare const check_package_available: (pkg: string, version: string, opt
28
46
  * @param options.timeout - total timeout in ms (default 300000 = 5min)
29
47
  * @throws {Error} if timeout reached or max attempts exceeded
30
48
  */
31
- export declare const wait_for_package: (pkg: string, version: string, options?: WaitOptions) => Promise<void>;
32
- /**
33
- * Fetches package metadata from NPM registry.
34
- *
35
- * Returns name and latest version. Returns null if package doesn't exist
36
- * or registry is unreachable.
37
- *
38
- * @returns package info or null on error/not found
39
- */
40
- export declare const get_package_info: (pkg: string, options?: {
41
- log?: Logger;
42
- }) => Promise<PackageInfo | null>;
43
- export declare const package_exists: (pkg: string, options?: {
44
- log?: Logger;
45
- }) => Promise<boolean>;
49
+ export declare const wait_for_package: (pkg: string, version: string, options?: WaitOptions, deps?: NpmRegistryDeps) => Promise<void>;
46
50
  //# sourceMappingURL=npm_registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"npm_registry.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/npm_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAKtD,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,uBAAuB,GACnC,KAAK,MAAM,EACX,SAAS,MAAM,EACf,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,KAC5B,OAAO,CAAC,OAAO,CAiBjB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GAC5B,KAAK,MAAM,EACX,SAAS,MAAM,EACf,UAAS,WAAgB,KACvB,OAAO,CAAC,IAAI,CA2Cd,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAC5B,KAAK,MAAM,EACX,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,KAC5B,OAAO,CAAC,WAAW,GAAG,IAAI,CAkB5B,CAAC;AAEF,eAAO,MAAM,cAAc,GAC1B,KAAK,MAAM,EACX,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,KAC5B,OAAO,CAAC,OAAO,CAGjB,CAAC"}
1
+ {"version":3,"file":"npm_registry.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/npm_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAKtD,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B;;;OAGG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,oCAAoC;IACpC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,uDAAuD;IACvD,GAAG,EAAE,MAAM,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,yBAAyB,EAAE,eAIvC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,KAAK,MAAM,EACX,SAAS,MAAM,EACf,UAAS;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,EAC9B,OAAM,eAA2C,KAC/C,OAAO,CAAC,OAAO,CASjB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GAC5B,KAAK,MAAM,EACX,SAAS,MAAM,EACf,UAAS,WAAgB,EACzB,OAAM,eAA2C,KAC/C,OAAO,CAAC,IAAI,CAwCd,CAAC"}
@@ -1,17 +1,22 @@
1
1
  import { spawn_out } from '@fuzdev/fuz_util/process.ts';
2
2
  import { wait } from '@fuzdev/fuz_util/async.ts';
3
3
  import { styleText as st } from 'node:util';
4
- export const check_package_available = async (pkg, version, options = {}) => {
4
+ export const default_npm_registry_deps = {
5
+ run_npm: async (args) => (await spawn_out('npm', args)).stdout ?? '',
6
+ wait,
7
+ now: () => Date.now()
8
+ };
9
+ /**
10
+ * Checks whether `pkg@version` is on the npm registry, by `npm view`.
11
+ *
12
+ * @returns `true` when the registry reports that exact version, `false` when it
13
+ * doesn't or npm couldn't run
14
+ */
15
+ export const check_package_available = async (pkg, version, options = {}, deps = default_npm_registry_deps) => {
5
16
  const { log } = options;
6
17
  try {
7
- // Use npm view to check if the specific version exists
8
- const result = await spawn_out('npm', ['view', `${pkg}@${version}`, 'version']);
9
- if (result.stdout) {
10
- const output = result.stdout.trim();
11
- // If we get a version back, it exists
12
- return output === version;
13
- }
14
- return false;
18
+ const stdout = await deps.run_npm(['view', `${pkg}@${version}`, 'version']);
19
+ return stdout.trim() === version;
15
20
  }
16
21
  catch (error) {
17
22
  log?.debug(`Failed to check ${pkg}@${version}: ${error}`);
@@ -33,63 +38,31 @@ export const check_package_available = async (pkg, version, options = {}) => {
33
38
  * @param options.timeout - total timeout in ms (default 300000 = 5min)
34
39
  * @throws {Error} if timeout reached or max attempts exceeded
35
40
  */
36
- export const wait_for_package = async (pkg, version, options = {}) => {
41
+ export const wait_for_package = async (pkg, version, options = {}, deps = default_npm_registry_deps) => {
37
42
  const { log, max_attempts = 30, initial_delay = 1000, max_delay = 60000, timeout = 300000 // 5 minutes default
38
43
  } = options;
39
- const start_time = Date.now();
44
+ const start_time = deps.now();
40
45
  let attempt = 0;
41
46
  let delay = initial_delay;
42
47
  while (attempt < max_attempts) {
43
48
  attempt++;
44
- // Check timeout
45
- if (Date.now() - start_time > timeout) {
49
+ if (deps.now() - start_time > timeout) {
46
50
  throw new Error(`Timeout waiting for ${pkg}@${version} after ${timeout}ms`);
47
51
  }
48
- // Check if package is available
49
- if (await check_package_available(pkg, version, { log })) {
52
+ if (await check_package_available(pkg, version, { log }, deps)) {
50
53
  log?.info(st('green', ` ✓ ${pkg}@${version} is now available on NPM`));
51
54
  return;
52
55
  }
53
56
  // Log progress occasionally
54
- if (attempt > 0 && attempt % 5 === 0) {
57
+ if (attempt % 5 === 0) {
55
58
  log?.info(st('dim', ` Still waiting... (attempt ${attempt}/${max_attempts})`));
56
59
  }
57
60
  // Wait with exponential backoff + jitter
58
61
  const jitter = Math.random() * delay * 0.1; // 10% jitter
59
62
  const actual_delay = Math.min(delay + jitter, max_delay);
60
- await wait(actual_delay);
63
+ await deps.wait(actual_delay);
61
64
  // Exponential backoff
62
65
  delay = Math.min(delay * 1.5, max_delay);
63
66
  }
64
67
  throw new Error(`${pkg}@${version} not available after ${max_attempts} attempts`);
65
68
  };
66
- /**
67
- * Fetches package metadata from NPM registry.
68
- *
69
- * Returns name and latest version. Returns null if package doesn't exist
70
- * or registry is unreachable.
71
- *
72
- * @returns package info or null on error/not found
73
- */
74
- export const get_package_info = async (pkg, options = {}) => {
75
- const { log } = options;
76
- try {
77
- const result = await spawn_out('npm', ['view', pkg, '--json']);
78
- if (result.stdout) {
79
- const data = JSON.parse(result.stdout);
80
- return {
81
- name: data.name,
82
- version: data.version
83
- };
84
- }
85
- return null;
86
- }
87
- catch (error) {
88
- log?.debug(`Failed to get package info for ${pkg}: ${error}`);
89
- return null;
90
- }
91
- };
92
- export const package_exists = async (pkg, options = {}) => {
93
- const info = await get_package_info(pkg, options);
94
- return info !== null;
95
- };