@topogram/cli 0.3.47 → 0.3.48

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +187 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.47",
3
+ "version": "0.3.48",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -191,8 +191,8 @@ function printUsage(options = {}) {
191
191
  console.log("Usage: topogram version [--json]");
192
192
  console.log("Usage: topogram doctor [--json] [--catalog <path-or-source>]");
193
193
  console.log("Usage: topogram setup package-auth|catalog-auth");
194
- console.log("Usage: topogram release status [--json] [--strict]");
195
- console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push]");
194
+ console.log("Usage: topogram release status [--json] [--strict] [--markdown|--write-report <path>]");
195
+ console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push] [--watch]");
196
196
  console.log("Usage: topogram check [path] [--json]");
197
197
  console.log(" or: topogram component check [path] [--projection <id>] [--component <id>] [--json]");
198
198
  console.log(" or: topogram component behavior [path] [--projection <id>] [--component <id>] [--json]");
@@ -759,18 +759,19 @@ function printPackageHelp() {
759
759
  }
760
760
 
761
761
  function printReleaseHelp() {
762
- console.log("Usage: topogram release status [--json] [--strict]");
763
- console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push]");
762
+ console.log("Usage: topogram release status [--json] [--strict] [--markdown|--write-report <path>]");
763
+ console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push] [--watch]");
764
764
  console.log("");
765
765
  console.log("Checks the local CLI version, latest published package version, release tag, first-party consumer pins, and strict consumer CI state.");
766
- console.log("Rolls first-party consumers to a published CLI version, runs their checks, commits, pushes, and prints latest workflow URLs.");
766
+ console.log("Rolls first-party consumers to a published CLI version, runs their checks, commits, pushes, and can wait for current workflow runs.");
767
767
  console.log("");
768
768
  console.log("Examples:");
769
769
  console.log(" topogram release status");
770
770
  console.log(" topogram release status --json");
771
771
  console.log(" topogram release status --strict");
772
- console.log(" topogram release roll-consumers 0.3.46");
773
- console.log(" topogram release roll-consumers --latest");
772
+ console.log(" topogram release status --strict --write-report ./release-baseline.md");
773
+ console.log(" topogram release roll-consumers 0.3.46 --watch");
774
+ console.log(" topogram release roll-consumers --latest --watch");
774
775
  console.log("");
775
776
  console.log("Release preparation and publishing are repo-level tasks in the Topogram source checkout:");
776
777
  console.log(" npm run release:prepare -- <version>");
@@ -2514,14 +2515,35 @@ function printPackageUpdateCli(payload) {
2514
2515
 
2515
2516
  /**
2516
2517
  * @param {string} requested
2517
- * @param {{ cwd?: string, push?: boolean }} [options]
2518
- * @returns {{ ok: boolean, packageName: string, requestedVersion: string, requestedLatest: boolean, pushed: boolean, consumers: Array<Record<string, any>>, diagnostics: Array<Record<string, any>>, errors: string[] }}
2518
+ * @param {{ cwd?: string, push?: boolean, watch?: boolean }} [options]
2519
+ * @returns {{ ok: boolean, packageName: string, requestedVersion: string, requestedLatest: boolean, pushed: boolean, watched: boolean, consumers: Array<Record<string, any>>, diagnostics: Array<Record<string, any>>, errors: string[] }}
2519
2520
  */
2520
2521
  function buildReleaseRollConsumersPayload(requested, options = {}) {
2521
2522
  const cwd = options.cwd || process.cwd();
2522
2523
  const push = options.push !== false;
2524
+ const watch = Boolean(options.watch);
2523
2525
  const requestedLatest = requested === "latest" || requested === "--latest";
2524
2526
  const diagnostics = [];
2527
+ if (watch && !push) {
2528
+ diagnostics.push({
2529
+ code: "release_roll_watch_requires_push",
2530
+ severity: "error",
2531
+ message: "`topogram release roll-consumers --watch` requires pushing consumer commits.",
2532
+ path: "release roll-consumers",
2533
+ suggestedFix: "Remove --no-push or run without --watch and verify consumer CI separately."
2534
+ });
2535
+ return {
2536
+ ok: false,
2537
+ packageName: CLI_PACKAGE_NAME,
2538
+ requestedVersion: requestedLatest ? "latest" : requested,
2539
+ requestedLatest,
2540
+ pushed: push,
2541
+ watched: watch,
2542
+ consumers: [],
2543
+ diagnostics,
2544
+ errors: diagnostics.map((diagnostic) => diagnostic.message)
2545
+ };
2546
+ }
2525
2547
  const version = requestedLatest
2526
2548
  ? resolveLatestTopogramCliVersionForPackageUpdate(cwd, diagnostics)
2527
2549
  : requested;
@@ -2622,7 +2644,9 @@ function buildReleaseRollConsumersPayload(requested, options = {}) {
2622
2644
  continue;
2623
2645
  }
2624
2646
  if (!staged.changed) {
2625
- item.ci = inspectConsumerCi(consumer, { strict: false });
2647
+ item.ci = watch
2648
+ ? waitForConsumerCi(consumer)
2649
+ : inspectConsumerCi(consumer, { strict: false });
2626
2650
  item.diagnostics.push(...item.ci.diagnostics);
2627
2651
  diagnostics.push(...item.ci.diagnostics);
2628
2652
  continue;
@@ -2658,7 +2682,9 @@ function buildReleaseRollConsumersPayload(requested, options = {}) {
2658
2682
  }
2659
2683
  item.pushed = true;
2660
2684
  }
2661
- item.ci = inspectConsumerCi(consumer, { strict: false });
2685
+ item.ci = watch
2686
+ ? waitForConsumerCi(consumer)
2687
+ : inspectConsumerCi(consumer, { strict: false });
2662
2688
  item.diagnostics.push(...item.ci.diagnostics);
2663
2689
  diagnostics.push(...item.ci.diagnostics);
2664
2690
  }
@@ -2671,6 +2697,7 @@ function buildReleaseRollConsumersPayload(requested, options = {}) {
2671
2697
  requestedVersion: version,
2672
2698
  requestedLatest,
2673
2699
  pushed: push,
2700
+ watched: watch,
2674
2701
  consumers,
2675
2702
  diagnostics,
2676
2703
  errors
@@ -2688,6 +2715,7 @@ function printReleaseRollConsumers(payload) {
2688
2715
  }
2689
2716
  console.log(`Package: ${payload.packageName}@${payload.requestedVersion}`);
2690
2717
  console.log(`Push: ${payload.pushed ? "enabled" : "disabled"}`);
2718
+ console.log(`Watch: ${payload.watched ? "enabled" : "disabled"}`);
2691
2719
  for (const consumer of payload.consumers) {
2692
2720
  const state = consumer.committed
2693
2721
  ? consumer.pushed ? "pushed" : "committed"
@@ -2805,13 +2833,13 @@ function releaseStatusStrictDiagnostics(release) {
2805
2833
  suggestedFix: "Publish the current CLI package version or fix npm package registry auth, then rerun `topogram release status --strict`."
2806
2834
  });
2807
2835
  }
2808
- if (release.git.local !== true) {
2836
+ if (release.git.local !== true && release.git.remote !== true) {
2809
2837
  diagnostics.push({
2810
2838
  code: "release_local_tag_missing",
2811
2839
  severity: "error",
2812
2840
  message: `Release tag ${release.git.tag} is missing locally.`,
2813
2841
  path: release.git.tag,
2814
- suggestedFix: `Fetch or create the local ${release.git.tag} tag before treating this release as complete.`
2842
+ suggestedFix: `Fetch, create, or push ${release.git.tag} before treating this release as complete.`
2815
2843
  });
2816
2844
  }
2817
2845
  if (release.git.remote !== true) {
@@ -2899,6 +2927,62 @@ function printReleaseStatus(payload) {
2899
2927
  }
2900
2928
  }
2901
2929
 
2930
+ /**
2931
+ * @param {ReturnType<typeof buildReleaseStatusPayload>} payload
2932
+ * @returns {string}
2933
+ */
2934
+ function renderReleaseStatusMarkdown(payload) {
2935
+ const lines = [
2936
+ `# Topogram CLI release ${payload.localVersion}`,
2937
+ "",
2938
+ `Date checked: ${new Date().toISOString().slice(0, 10)}`,
2939
+ "",
2940
+ "## Summary",
2941
+ "",
2942
+ `- Package: \`${payload.packageName}@${payload.localVersion}\``,
2943
+ `- Latest published: \`${payload.latestVersion || "unknown"}\`${payload.currentPublished === true ? " (current)" : payload.currentPublished === false ? " (differs)" : ""}`,
2944
+ `- Release tag: \`${payload.git.tag}\` (local=${labelBoolean(payload.git.local)}, remote=${labelBoolean(payload.git.remote)})`,
2945
+ `- Consumer pins: ${payload.consumerPins.matching}/${payload.consumerPins.known} matching`,
2946
+ `- Consumer CI: ${payload.consumerCi.passing}/${payload.consumerCi.checked} passing`,
2947
+ `- Strict status: ${payload.ok ? "passed" : "failed"}`,
2948
+ "",
2949
+ "## Consumers",
2950
+ "",
2951
+ "| Repo | Pin | Workflow | Status | Run |",
2952
+ "| --- | --- | --- | --- | --- |"
2953
+ ];
2954
+ for (const consumer of payload.consumers) {
2955
+ const workflow = consumer.workflow || consumer.ci?.expectedWorkflow || "";
2956
+ const run = consumer.ci?.run;
2957
+ const status = run ? `${run.status || "unknown"}/${run.conclusion || "unknown"}` : consumer.ci?.checked ? "unavailable" : "not checked";
2958
+ const url = run?.url ? `[${run.databaseId || "run"}](${run.url})` : "";
2959
+ lines.push(`| \`${consumer.name}\` | \`${consumer.version || "missing"}\` | ${escapeMarkdownTableCell(workflow)} | ${escapeMarkdownTableCell(status)} | ${url} |`);
2960
+ }
2961
+ if (payload.diagnostics.length > 0) {
2962
+ lines.push("", "## Diagnostics", "");
2963
+ for (const diagnostic of payload.diagnostics) {
2964
+ const label = diagnostic.severity === "warning"
2965
+ ? "Warning"
2966
+ : diagnostic.severity === "info"
2967
+ ? "Note"
2968
+ : "Error";
2969
+ lines.push(`- **${label}** \`${diagnostic.code}\`: ${diagnostic.message}`);
2970
+ if (diagnostic.suggestedFix) {
2971
+ lines.push(` Fix: ${diagnostic.suggestedFix}`);
2972
+ }
2973
+ }
2974
+ }
2975
+ return `${lines.join("\n")}\n`;
2976
+ }
2977
+
2978
+ /**
2979
+ * @param {string|null|undefined} value
2980
+ * @returns {string}
2981
+ */
2982
+ function escapeMarkdownTableCell(value) {
2983
+ return String(value || "").replace(/\|/g, "\\|").replace(/\n/g, "<br>");
2984
+ }
2985
+
2902
2986
  /**
2903
2987
  * @param {boolean|null} value
2904
2988
  * @returns {string}
@@ -3041,6 +3125,64 @@ function commandOutput(result) {
3041
3125
  return [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
3042
3126
  }
3043
3127
 
3128
+ /**
3129
+ * @param {number} ms
3130
+ * @returns {void}
3131
+ */
3132
+ function sleepSync(ms) {
3133
+ if (ms <= 0) {
3134
+ return;
3135
+ }
3136
+ const buffer = new SharedArrayBuffer(4);
3137
+ const view = new Int32Array(buffer);
3138
+ Atomics.wait(view, 0, 0, ms);
3139
+ }
3140
+
3141
+ /**
3142
+ * @param {string} name
3143
+ * @param {number} fallback
3144
+ * @returns {number}
3145
+ */
3146
+ function positiveIntegerEnv(name, fallback) {
3147
+ const value = Number.parseInt(process.env[name] || "", 10);
3148
+ return Number.isFinite(value) && value > 0 ? value : fallback;
3149
+ }
3150
+
3151
+ /**
3152
+ * @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
3153
+ * @param {{ timeoutMs?: number, intervalMs?: number }} [options]
3154
+ * @returns {ReturnType<typeof inspectConsumerCi>}
3155
+ */
3156
+ function waitForConsumerCi(consumer, options = {}) {
3157
+ const timeoutMs = options.timeoutMs || positiveIntegerEnv("TOPOGRAM_RELEASE_WATCH_TIMEOUT_MS", 20 * 60 * 1000);
3158
+ const intervalMs = options.intervalMs || positiveIntegerEnv("TOPOGRAM_RELEASE_WATCH_INTERVAL_MS", 5000);
3159
+ const startedAt = Date.now();
3160
+ let latest = inspectConsumerCi(consumer, { strict: false });
3161
+ while (true) {
3162
+ const currentRun = latest.run &&
3163
+ latest.headSha &&
3164
+ latest.run.headSha &&
3165
+ latest.run.headSha === latest.headSha;
3166
+ if (currentRun && latest.run.status === "completed") {
3167
+ return inspectConsumerCi(consumer, { strict: true });
3168
+ }
3169
+ if (Date.now() - startedAt >= timeoutMs) {
3170
+ const strictLatest = inspectConsumerCi(consumer, { strict: true });
3171
+ strictLatest.diagnostics.push({
3172
+ code: "release_consumer_ci_watch_timeout",
3173
+ severity: "error",
3174
+ message: `${consumer.name} verification workflow did not complete on the current commit before the watch timeout.`,
3175
+ path: strictLatest.run?.url || `attebury/${consumer.name}`,
3176
+ suggestedFix: "Open the consumer workflow, fix failures if needed, then rerun release status."
3177
+ });
3178
+ strictLatest.ok = false;
3179
+ return strictLatest;
3180
+ }
3181
+ sleepSync(intervalMs);
3182
+ latest = inspectConsumerCi(consumer, { strict: false });
3183
+ }
3184
+ }
3185
+
3044
3186
  /**
3045
3187
  * @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
3046
3188
  * @param {{ strict?: boolean }} [options]
@@ -8337,6 +8479,14 @@ if (commandArgs && Object.prototype.hasOwnProperty.call(commandArgs, "inputPath"
8337
8479
  const emitJson = args.includes("--json");
8338
8480
  const strictReleaseStatus = args.includes("--strict");
8339
8481
  const shouldPushReleaseConsumers = !args.includes("--no-push");
8482
+ const shouldWatchReleaseConsumers = args.includes("--watch");
8483
+ const shouldPrintReleaseMarkdown = args.includes("--markdown");
8484
+ const releaseReportIndex = args.indexOf("--write-report");
8485
+ const releaseReportPath = releaseReportIndex >= 0 &&
8486
+ args[releaseReportIndex + 1] &&
8487
+ !args[releaseReportIndex + 1].startsWith("-")
8488
+ ? args[releaseReportIndex + 1]
8489
+ : null;
8340
8490
  const shouldVersion = Boolean(commandArgs?.version);
8341
8491
  const shouldDoctor = Boolean(commandArgs?.doctor);
8342
8492
  const shouldReleaseStatus = Boolean(commandArgs?.releaseStatus);
@@ -8515,6 +8665,18 @@ if (shouldPackageUpdateCli && !inputPath) {
8515
8665
  process.exit(1);
8516
8666
  }
8517
8667
 
8668
+ if (shouldReleaseStatus && args.includes("--write-report") && !releaseReportPath) {
8669
+ console.error("Missing required --write-report <path>.");
8670
+ printReleaseHelp();
8671
+ process.exit(1);
8672
+ }
8673
+
8674
+ if (shouldReleaseRollConsumers && shouldWatchReleaseConsumers && !shouldPushReleaseConsumers) {
8675
+ console.error("Use either --watch or --no-push, not both.");
8676
+ printReleaseHelp();
8677
+ process.exit(1);
8678
+ }
8679
+
8518
8680
  if (shouldImportWorkspace && !outPath) {
8519
8681
  console.error("Missing required --out <target>.");
8520
8682
  printImportHelp();
@@ -8566,17 +8728,28 @@ try {
8566
8728
 
8567
8729
  if (shouldReleaseStatus) {
8568
8730
  const payload = buildReleaseStatusPayload({ strict: strictReleaseStatus });
8731
+ if (releaseReportPath) {
8732
+ const target = path.resolve(releaseReportPath);
8733
+ fs.mkdirSync(path.dirname(target), { recursive: true });
8734
+ fs.writeFileSync(target, renderReleaseStatusMarkdown(payload), "utf8");
8735
+ }
8569
8736
  if (emitJson) {
8570
8737
  console.log(stableStringify(payload));
8738
+ } else if (shouldPrintReleaseMarkdown) {
8739
+ console.log(renderReleaseStatusMarkdown(payload).trimEnd());
8571
8740
  } else {
8572
8741
  printReleaseStatus(payload);
8742
+ if (releaseReportPath) {
8743
+ console.log(`Report: ${path.resolve(releaseReportPath)}`);
8744
+ }
8573
8745
  }
8574
8746
  process.exit(payload.ok ? 0 : 1);
8575
8747
  }
8576
8748
 
8577
8749
  if (shouldReleaseRollConsumers) {
8578
8750
  const payload = buildReleaseRollConsumersPayload(commandArgs.releaseRollVersion, {
8579
- push: shouldPushReleaseConsumers
8751
+ push: shouldPushReleaseConsumers,
8752
+ watch: shouldWatchReleaseConsumers
8580
8753
  });
8581
8754
  if (emitJson) {
8582
8755
  console.log(stableStringify(payload));