@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.
- package/package.json +1 -1
- package/src/cli.js +187 -14
package/package.json
CHANGED
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
|
|
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
|
|
773
|
-
console.log(" topogram release roll-consumers --
|
|
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 =
|
|
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 =
|
|
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
|
|
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));
|