@topogram/cli 0.3.46 → 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 +712 -12
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -137,6 +137,20 @@ const KNOWN_CLI_CONSUMER_REPOS = [
|
|
|
137
137
|
"topogram-demo-todo",
|
|
138
138
|
"topogram-hello"
|
|
139
139
|
];
|
|
140
|
+
const KNOWN_CLI_CONSUMER_WORKFLOWS = {
|
|
141
|
+
"topogram-generator-express-api": "Generator Verification",
|
|
142
|
+
"topogram-generator-hono-api": "Generator Verification",
|
|
143
|
+
"topogram-generator-postgres-db": "Generator Verification",
|
|
144
|
+
"topogram-generator-react-web": "Generator Verification",
|
|
145
|
+
"topogram-generator-sqlite-db": "Generator Verification",
|
|
146
|
+
"topogram-generator-sveltekit-web": "Generator Verification",
|
|
147
|
+
"topogram-generator-swiftui-native": "Generator Verification",
|
|
148
|
+
"topogram-generator-vanilla-web": "Generator Verification",
|
|
149
|
+
"topogram-starters": "Starter Verification",
|
|
150
|
+
"topogram-template-todo": "Template Verification",
|
|
151
|
+
"topogram-demo-todo": "Demo Verification",
|
|
152
|
+
"topogram-hello": "Topogram Package Verification"
|
|
153
|
+
};
|
|
140
154
|
const PACKAGE_UPDATE_CLI_CHECK_SCRIPTS = [
|
|
141
155
|
"cli:surface",
|
|
142
156
|
"doctor",
|
|
@@ -177,7 +191,8 @@ function printUsage(options = {}) {
|
|
|
177
191
|
console.log("Usage: topogram version [--json]");
|
|
178
192
|
console.log("Usage: topogram doctor [--json] [--catalog <path-or-source>]");
|
|
179
193
|
console.log("Usage: topogram setup package-auth|catalog-auth");
|
|
180
|
-
console.log("Usage: topogram release status [--json] [--strict]");
|
|
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]");
|
|
181
196
|
console.log("Usage: topogram check [path] [--json]");
|
|
182
197
|
console.log(" or: topogram component check [path] [--projection <id>] [--component <id>] [--json]");
|
|
183
198
|
console.log(" or: topogram component behavior [path] [--projection <id>] [--component <id>] [--json]");
|
|
@@ -231,6 +246,7 @@ function printUsage(options = {}) {
|
|
|
231
246
|
console.log(" topogram doctor");
|
|
232
247
|
console.log(" topogram setup package-auth");
|
|
233
248
|
console.log(" topogram release status");
|
|
249
|
+
console.log(" topogram release roll-consumers --latest");
|
|
234
250
|
console.log(" topogram new ./my-app");
|
|
235
251
|
console.log(" topogram new --list-templates");
|
|
236
252
|
console.log(" topogram new ./my-app --template todo");
|
|
@@ -743,14 +759,19 @@ function printPackageHelp() {
|
|
|
743
759
|
}
|
|
744
760
|
|
|
745
761
|
function printReleaseHelp() {
|
|
746
|
-
console.log("Usage: topogram release status [--json] [--strict]");
|
|
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]");
|
|
747
764
|
console.log("");
|
|
748
|
-
console.log("Checks the local CLI version, latest published package version, release tag,
|
|
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 can wait for current workflow runs.");
|
|
749
767
|
console.log("");
|
|
750
768
|
console.log("Examples:");
|
|
751
769
|
console.log(" topogram release status");
|
|
752
770
|
console.log(" topogram release status --json");
|
|
753
771
|
console.log(" topogram release status --strict");
|
|
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");
|
|
754
775
|
console.log("");
|
|
755
776
|
console.log("Release preparation and publishing are repo-level tasks in the Topogram source checkout:");
|
|
756
777
|
console.log(" npm run release:prepare -- <version>");
|
|
@@ -2493,8 +2514,235 @@ function printPackageUpdateCli(payload) {
|
|
|
2493
2514
|
}
|
|
2494
2515
|
|
|
2495
2516
|
/**
|
|
2496
|
-
* @param {
|
|
2497
|
-
* @
|
|
2517
|
+
* @param {string} requested
|
|
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[] }}
|
|
2520
|
+
*/
|
|
2521
|
+
function buildReleaseRollConsumersPayload(requested, options = {}) {
|
|
2522
|
+
const cwd = options.cwd || process.cwd();
|
|
2523
|
+
const push = options.push !== false;
|
|
2524
|
+
const watch = Boolean(options.watch);
|
|
2525
|
+
const requestedLatest = requested === "latest" || requested === "--latest";
|
|
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
|
+
}
|
|
2547
|
+
const version = requestedLatest
|
|
2548
|
+
? resolveLatestTopogramCliVersionForPackageUpdate(cwd, diagnostics)
|
|
2549
|
+
: requested;
|
|
2550
|
+
if (!isPackageVersion(version)) {
|
|
2551
|
+
throw new Error("topogram release roll-consumers requires <version> or --latest.");
|
|
2552
|
+
}
|
|
2553
|
+
const consumers = [];
|
|
2554
|
+
for (const consumer of discoverTopogramCliVersionConsumers(cwd)) {
|
|
2555
|
+
const workflow = expectedConsumerWorkflowName(consumer.name);
|
|
2556
|
+
const item = {
|
|
2557
|
+
name: consumer.name,
|
|
2558
|
+
root: consumer.root,
|
|
2559
|
+
workflow,
|
|
2560
|
+
updated: false,
|
|
2561
|
+
committed: false,
|
|
2562
|
+
pushed: false,
|
|
2563
|
+
commit: null,
|
|
2564
|
+
update: null,
|
|
2565
|
+
ci: null,
|
|
2566
|
+
diagnostics: []
|
|
2567
|
+
};
|
|
2568
|
+
consumers.push(item);
|
|
2569
|
+
if (!consumer.root || !fs.existsSync(consumer.root)) {
|
|
2570
|
+
item.diagnostics.push({
|
|
2571
|
+
code: "release_consumer_repo_missing",
|
|
2572
|
+
severity: "error",
|
|
2573
|
+
message: `First-party consumer repo ${consumer.name} was not found.`,
|
|
2574
|
+
path: consumer.path,
|
|
2575
|
+
suggestedFix: `Clone ${consumer.name} beside the topogram repo, then rerun roll-consumers.`
|
|
2576
|
+
});
|
|
2577
|
+
diagnostics.push(...item.diagnostics);
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
const packagePath = path.join(consumer.root, "package.json");
|
|
2581
|
+
if (!fs.existsSync(packagePath)) {
|
|
2582
|
+
item.diagnostics.push({
|
|
2583
|
+
code: "release_consumer_package_missing",
|
|
2584
|
+
severity: "error",
|
|
2585
|
+
message: `First-party consumer repo ${consumer.name} does not contain package.json.`,
|
|
2586
|
+
path: packagePath,
|
|
2587
|
+
suggestedFix: "Only package-backed first-party consumers can be rolled by this command."
|
|
2588
|
+
});
|
|
2589
|
+
diagnostics.push(...item.diagnostics);
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
const clean = inspectGitWorktreeClean(consumer.root);
|
|
2593
|
+
if (clean.ok !== true) {
|
|
2594
|
+
item.diagnostics.push({
|
|
2595
|
+
code: "release_consumer_worktree_dirty",
|
|
2596
|
+
severity: "error",
|
|
2597
|
+
message: clean.error || `First-party consumer repo ${consumer.name} has uncommitted changes.`,
|
|
2598
|
+
path: consumer.root,
|
|
2599
|
+
suggestedFix: "Commit, stash, or discard unrelated consumer changes before rolling the CLI version."
|
|
2600
|
+
});
|
|
2601
|
+
diagnostics.push(...item.diagnostics);
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
try {
|
|
2605
|
+
item.update = buildPackageUpdateCliPayload(version, { cwd: consumer.root });
|
|
2606
|
+
item.updated = true;
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
item.diagnostics.push({
|
|
2609
|
+
code: "release_consumer_update_failed",
|
|
2610
|
+
severity: "error",
|
|
2611
|
+
message: `Failed to update ${consumer.name}: ${messageFromError(error)}`,
|
|
2612
|
+
path: consumer.root,
|
|
2613
|
+
suggestedFix: "Fix the consumer update/check failure, then rerun roll-consumers."
|
|
2614
|
+
});
|
|
2615
|
+
diagnostics.push(...item.diagnostics);
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
const filesToStage = ["package.json", "package-lock.json", "topogram-cli.version"]
|
|
2619
|
+
.filter((file) => fs.existsSync(path.join(consumer.root, file)));
|
|
2620
|
+
const addResult = runGit(["add", ...filesToStage], consumer.root);
|
|
2621
|
+
if (addResult.status !== 0) {
|
|
2622
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2623
|
+
code: "release_consumer_git_add_failed",
|
|
2624
|
+
severity: "error",
|
|
2625
|
+
message: `Failed to stage ${consumer.name} CLI update.`,
|
|
2626
|
+
path: consumer.root,
|
|
2627
|
+
suggestedFix: "Inspect git output, stage the changed files manually, then commit and push.",
|
|
2628
|
+
result: addResult
|
|
2629
|
+
}));
|
|
2630
|
+
diagnostics.push(...item.diagnostics);
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
const staged = hasStagedGitChanges(consumer.root);
|
|
2634
|
+
if (!staged.ok) {
|
|
2635
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2636
|
+
code: "release_consumer_git_diff_failed",
|
|
2637
|
+
severity: "error",
|
|
2638
|
+
message: `Could not inspect staged changes for ${consumer.name}.`,
|
|
2639
|
+
path: consumer.root,
|
|
2640
|
+
suggestedFix: "Inspect git status manually before committing.",
|
|
2641
|
+
result: staged.result
|
|
2642
|
+
}));
|
|
2643
|
+
diagnostics.push(...item.diagnostics);
|
|
2644
|
+
continue;
|
|
2645
|
+
}
|
|
2646
|
+
if (!staged.changed) {
|
|
2647
|
+
item.ci = watch
|
|
2648
|
+
? waitForConsumerCi(consumer)
|
|
2649
|
+
: inspectConsumerCi(consumer, { strict: false });
|
|
2650
|
+
item.diagnostics.push(...item.ci.diagnostics);
|
|
2651
|
+
diagnostics.push(...item.ci.diagnostics);
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
const commitResult = runGit(["commit", "-m", `Update Topogram CLI to ${version}`], consumer.root);
|
|
2655
|
+
if (commitResult.status !== 0) {
|
|
2656
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2657
|
+
code: "release_consumer_git_commit_failed",
|
|
2658
|
+
severity: "error",
|
|
2659
|
+
message: `Failed to commit ${consumer.name} CLI update.`,
|
|
2660
|
+
path: consumer.root,
|
|
2661
|
+
suggestedFix: "Inspect git output, commit the consumer update manually, then push.",
|
|
2662
|
+
result: commitResult
|
|
2663
|
+
}));
|
|
2664
|
+
diagnostics.push(...item.diagnostics);
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2667
|
+
item.committed = true;
|
|
2668
|
+
item.commit = currentGitHead(consumer.root);
|
|
2669
|
+
if (push) {
|
|
2670
|
+
const pushResult = runGit(["push", "origin", "main"], consumer.root);
|
|
2671
|
+
if (pushResult.status !== 0) {
|
|
2672
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2673
|
+
code: "release_consumer_git_push_failed",
|
|
2674
|
+
severity: "error",
|
|
2675
|
+
message: `Failed to push ${consumer.name} CLI update.`,
|
|
2676
|
+
path: consumer.root,
|
|
2677
|
+
suggestedFix: "Push the consumer update manually, then confirm its verification workflow passes.",
|
|
2678
|
+
result: pushResult
|
|
2679
|
+
}));
|
|
2680
|
+
diagnostics.push(...item.diagnostics);
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
item.pushed = true;
|
|
2684
|
+
}
|
|
2685
|
+
item.ci = watch
|
|
2686
|
+
? waitForConsumerCi(consumer)
|
|
2687
|
+
: inspectConsumerCi(consumer, { strict: false });
|
|
2688
|
+
item.diagnostics.push(...item.ci.diagnostics);
|
|
2689
|
+
diagnostics.push(...item.ci.diagnostics);
|
|
2690
|
+
}
|
|
2691
|
+
const errors = diagnostics
|
|
2692
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
2693
|
+
.map((diagnostic) => diagnostic.message);
|
|
2694
|
+
return {
|
|
2695
|
+
ok: errors.length === 0,
|
|
2696
|
+
packageName: CLI_PACKAGE_NAME,
|
|
2697
|
+
requestedVersion: version,
|
|
2698
|
+
requestedLatest,
|
|
2699
|
+
pushed: push,
|
|
2700
|
+
watched: watch,
|
|
2701
|
+
consumers,
|
|
2702
|
+
diagnostics,
|
|
2703
|
+
errors
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
/**
|
|
2708
|
+
* @param {ReturnType<typeof buildReleaseRollConsumersPayload>} payload
|
|
2709
|
+
* @returns {void}
|
|
2710
|
+
*/
|
|
2711
|
+
function printReleaseRollConsumers(payload) {
|
|
2712
|
+
console.log(payload.ok ? "Topogram consumer rollout completed." : "Topogram consumer rollout found issues.");
|
|
2713
|
+
if (payload.requestedLatest) {
|
|
2714
|
+
console.log(`Resolved latest version: ${payload.requestedVersion}`);
|
|
2715
|
+
}
|
|
2716
|
+
console.log(`Package: ${payload.packageName}@${payload.requestedVersion}`);
|
|
2717
|
+
console.log(`Push: ${payload.pushed ? "enabled" : "disabled"}`);
|
|
2718
|
+
console.log(`Watch: ${payload.watched ? "enabled" : "disabled"}`);
|
|
2719
|
+
for (const consumer of payload.consumers) {
|
|
2720
|
+
const state = consumer.committed
|
|
2721
|
+
? consumer.pushed ? "pushed" : "committed"
|
|
2722
|
+
: consumer.updated ? "updated" : "skipped";
|
|
2723
|
+
console.log(`- ${consumer.name}: ${state}`);
|
|
2724
|
+
if (consumer.update) {
|
|
2725
|
+
console.log(` Checks run: ${consumer.update.scriptsRun.join(", ") || "none"}`);
|
|
2726
|
+
}
|
|
2727
|
+
if (consumer.commit) {
|
|
2728
|
+
console.log(` Commit: ${consumer.commit}`);
|
|
2729
|
+
}
|
|
2730
|
+
if (consumer.ci?.run?.url) {
|
|
2731
|
+
const run = consumer.ci.run;
|
|
2732
|
+
console.log(` CI: ${run.workflowName || consumer.workflow} ${run.status || "unknown"}/${run.conclusion || "unknown"} ${run.url}`);
|
|
2733
|
+
} else if (consumer.workflow) {
|
|
2734
|
+
console.log(` CI: ${consumer.workflow} not found`);
|
|
2735
|
+
}
|
|
2736
|
+
for (const diagnostic of consumer.diagnostics || []) {
|
|
2737
|
+
const label = diagnostic.severity === "error" ? "Error" : diagnostic.severity === "warning" ? "Warning" : "Note";
|
|
2738
|
+
console.log(` ${label}: ${diagnostic.message}`);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
/**
|
|
2744
|
+
* @param {{ cwd?: string, strict?: boolean }} [options]
|
|
2745
|
+
* @returns {{ ok: boolean, packageName: string, localVersion: string, latestVersion: string|null, currentPublished: boolean|null, git: { tag: string, local: boolean|null, remote: boolean|null, diagnostics: Array<Record<string, any>> }, consumerPins: ReturnType<typeof summarizeConsumerPins>, consumerCi: ReturnType<typeof summarizeConsumerCi>, consumers: Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean, matchesLocal: boolean|null, workflow: string|null, ci: ReturnType<typeof inspectConsumerCi>|null }>, diagnostics: Array<Record<string, any>>, errors: string[] }}
|
|
2498
2746
|
*/
|
|
2499
2747
|
function buildReleaseStatusPayload(options = {}) {
|
|
2500
2748
|
const cwd = options.cwd || process.cwd();
|
|
@@ -2517,9 +2765,20 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2517
2765
|
diagnostics.push(...git.diagnostics);
|
|
2518
2766
|
const consumers = discoverTopogramCliVersionConsumers(cwd).map((consumer) => ({
|
|
2519
2767
|
...consumer,
|
|
2520
|
-
matchesLocal: consumer.version ? consumer.version === localVersion : null
|
|
2768
|
+
matchesLocal: consumer.version ? consumer.version === localVersion : null,
|
|
2769
|
+
workflow: expectedConsumerWorkflowName(consumer.name),
|
|
2770
|
+
ci: null
|
|
2521
2771
|
}));
|
|
2772
|
+
if (strict) {
|
|
2773
|
+
for (const consumer of consumers) {
|
|
2774
|
+
if (consumer.matchesLocal === true) {
|
|
2775
|
+
consumer.ci = inspectConsumerCi(consumer, { strict: true });
|
|
2776
|
+
diagnostics.push(...consumer.ci.diagnostics);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2522
2780
|
const consumerPins = summarizeConsumerPins(consumers);
|
|
2781
|
+
const consumerCi = summarizeConsumerCi(consumers);
|
|
2523
2782
|
const currentPublished = latestVersion ? latestVersion === localVersion : null;
|
|
2524
2783
|
if (strict) {
|
|
2525
2784
|
diagnostics.push(...releaseStatusStrictDiagnostics({
|
|
@@ -2527,7 +2786,8 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2527
2786
|
latestVersion,
|
|
2528
2787
|
currentPublished,
|
|
2529
2788
|
git,
|
|
2530
|
-
consumerPins
|
|
2789
|
+
consumerPins,
|
|
2790
|
+
consumerCi
|
|
2531
2791
|
}));
|
|
2532
2792
|
}
|
|
2533
2793
|
const errors = diagnostics
|
|
@@ -2542,6 +2802,7 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2542
2802
|
currentPublished,
|
|
2543
2803
|
git,
|
|
2544
2804
|
consumerPins,
|
|
2805
|
+
consumerCi,
|
|
2545
2806
|
consumers,
|
|
2546
2807
|
diagnostics,
|
|
2547
2808
|
errors
|
|
@@ -2554,7 +2815,8 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2554
2815
|
* latestVersion: string|null,
|
|
2555
2816
|
* currentPublished: boolean|null,
|
|
2556
2817
|
* git: ReturnType<typeof inspectReleaseGitTag>,
|
|
2557
|
-
* consumerPins: ReturnType<typeof summarizeConsumerPins
|
|
2818
|
+
* consumerPins: ReturnType<typeof summarizeConsumerPins>,
|
|
2819
|
+
* consumerCi: ReturnType<typeof summarizeConsumerCi>
|
|
2558
2820
|
* }} release
|
|
2559
2821
|
* @returns {Array<{ code: string, severity: "error", message: string, path: string, suggestedFix: string }>}
|
|
2560
2822
|
*/
|
|
@@ -2571,13 +2833,13 @@ function releaseStatusStrictDiagnostics(release) {
|
|
|
2571
2833
|
suggestedFix: "Publish the current CLI package version or fix npm package registry auth, then rerun `topogram release status --strict`."
|
|
2572
2834
|
});
|
|
2573
2835
|
}
|
|
2574
|
-
if (release.git.local !== true) {
|
|
2836
|
+
if (release.git.local !== true && release.git.remote !== true) {
|
|
2575
2837
|
diagnostics.push({
|
|
2576
2838
|
code: "release_local_tag_missing",
|
|
2577
2839
|
severity: "error",
|
|
2578
2840
|
message: `Release tag ${release.git.tag} is missing locally.`,
|
|
2579
2841
|
path: release.git.tag,
|
|
2580
|
-
suggestedFix: `Fetch
|
|
2842
|
+
suggestedFix: `Fetch, create, or push ${release.git.tag} before treating this release as complete.`
|
|
2581
2843
|
});
|
|
2582
2844
|
}
|
|
2583
2845
|
if (release.git.remote !== true) {
|
|
@@ -2598,6 +2860,15 @@ function releaseStatusStrictDiagnostics(release) {
|
|
|
2598
2860
|
suggestedFix: "Roll first-party consumer repositories to the current CLI version before treating this release as complete."
|
|
2599
2861
|
});
|
|
2600
2862
|
}
|
|
2863
|
+
if (release.consumerCi.allCheckedAndPassing !== true) {
|
|
2864
|
+
diagnostics.push({
|
|
2865
|
+
code: "release_consumer_ci_not_current",
|
|
2866
|
+
severity: "error",
|
|
2867
|
+
message: "First-party consumer verification workflows are not all passing on the checked-out consumer commits.",
|
|
2868
|
+
path: "GitHub Actions",
|
|
2869
|
+
suggestedFix: "Wait for or fix the consumer verification workflows, then rerun `topogram release status --strict`."
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2601
2872
|
return diagnostics;
|
|
2602
2873
|
}
|
|
2603
2874
|
|
|
@@ -2618,13 +2889,27 @@ function printReleaseStatus(payload) {
|
|
|
2618
2889
|
`Consumer pins: ${payload.consumerPins.pinned}/${payload.consumerPins.known} pinned, ` +
|
|
2619
2890
|
`${payload.consumerPins.matching} matching, ${payload.consumerPins.differing} differing, ${payload.consumerPins.missing} missing`
|
|
2620
2891
|
);
|
|
2892
|
+
if (payload.strict) {
|
|
2893
|
+
console.log(
|
|
2894
|
+
`Consumer CI: ${payload.consumerCi.passing}/${payload.consumerCi.checked} passing, ` +
|
|
2895
|
+
`${payload.consumerCi.failing} failing, ${payload.consumerCi.unavailable} unavailable, ${payload.consumerCi.skipped} skipped`
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2621
2898
|
for (const consumer of payload.consumers) {
|
|
2622
2899
|
const status = consumer.matchesLocal === true
|
|
2623
2900
|
? "matches"
|
|
2624
2901
|
: consumer.matchesLocal === false
|
|
2625
2902
|
? "differs"
|
|
2626
2903
|
: "missing";
|
|
2627
|
-
|
|
2904
|
+
const ciStatus = consumer.ci?.run
|
|
2905
|
+
? `; ${consumer.ci.run.workflowName || consumer.workflow}: ${consumer.ci.run.status || "unknown"}/${consumer.ci.run.conclusion || "unknown"}`
|
|
2906
|
+
: consumer.ci?.checked
|
|
2907
|
+
? `; ${consumer.workflow || "workflow"} unavailable`
|
|
2908
|
+
: "";
|
|
2909
|
+
console.log(`- ${consumer.name}: ${consumer.version || "missing"} (${status})${ciStatus}`);
|
|
2910
|
+
if (consumer.ci?.run?.url) {
|
|
2911
|
+
console.log(` CI: ${consumer.ci.run.url}`);
|
|
2912
|
+
}
|
|
2628
2913
|
}
|
|
2629
2914
|
if (payload.diagnostics.length > 0) {
|
|
2630
2915
|
console.log("Diagnostics:");
|
|
@@ -2642,6 +2927,62 @@ function printReleaseStatus(payload) {
|
|
|
2642
2927
|
}
|
|
2643
2928
|
}
|
|
2644
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
|
+
|
|
2645
2986
|
/**
|
|
2646
2987
|
* @param {boolean|null} value
|
|
2647
2988
|
* @returns {string}
|
|
@@ -2697,9 +3038,294 @@ function inspectReleaseGitTag(version, cwd) {
|
|
|
2697
3038
|
return { tag, local, remote, diagnostics };
|
|
2698
3039
|
}
|
|
2699
3040
|
|
|
3041
|
+
/**
|
|
3042
|
+
* @param {string} name
|
|
3043
|
+
* @returns {string|null}
|
|
3044
|
+
*/
|
|
3045
|
+
function expectedConsumerWorkflowName(name) {
|
|
3046
|
+
return KNOWN_CLI_CONSUMER_WORKFLOWS[name] || null;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* @param {string[]} args
|
|
3051
|
+
* @param {string} cwd
|
|
3052
|
+
* @returns {ReturnType<typeof childProcess.spawnSync>}
|
|
3053
|
+
*/
|
|
3054
|
+
function runGit(args, cwd) {
|
|
3055
|
+
return childProcess.spawnSync("git", args, {
|
|
3056
|
+
cwd,
|
|
3057
|
+
encoding: "utf8",
|
|
3058
|
+
env: { ...process.env, PATH: process.env.PATH || "" }
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
|
|
2700
3062
|
/**
|
|
2701
3063
|
* @param {string} cwd
|
|
2702
|
-
* @returns {
|
|
3064
|
+
* @returns {{ ok: boolean, dirty: boolean|null, error: string|null }}
|
|
3065
|
+
*/
|
|
3066
|
+
function inspectGitWorktreeClean(cwd) {
|
|
3067
|
+
const result = runGit(["status", "--porcelain"], cwd);
|
|
3068
|
+
if (result.status !== 0) {
|
|
3069
|
+
return {
|
|
3070
|
+
ok: false,
|
|
3071
|
+
dirty: null,
|
|
3072
|
+
error: `Could not inspect git status: ${commandOutput(result) || "unknown error"}`
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
const dirty = String(result.stdout || "").trim().length > 0;
|
|
3076
|
+
return {
|
|
3077
|
+
ok: !dirty,
|
|
3078
|
+
dirty,
|
|
3079
|
+
error: dirty ? "Consumer repo has uncommitted changes." : null
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
/**
|
|
3084
|
+
* @param {string} cwd
|
|
3085
|
+
* @returns {{ ok: boolean, changed: boolean, result: ReturnType<typeof childProcess.spawnSync> }}
|
|
3086
|
+
*/
|
|
3087
|
+
function hasStagedGitChanges(cwd) {
|
|
3088
|
+
const result = runGit(["diff", "--cached", "--quiet"], cwd);
|
|
3089
|
+
return {
|
|
3090
|
+
ok: result.status === 0 || result.status === 1,
|
|
3091
|
+
changed: result.status === 1,
|
|
3092
|
+
result
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
/**
|
|
3097
|
+
* @param {string} cwd
|
|
3098
|
+
* @returns {string|null}
|
|
3099
|
+
*/
|
|
3100
|
+
function currentGitHead(cwd) {
|
|
3101
|
+
const result = runGit(["rev-parse", "HEAD"], cwd);
|
|
3102
|
+
return result.status === 0 ? String(result.stdout || "").trim() || null : null;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
/**
|
|
3106
|
+
* @param {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string, result: ReturnType<typeof childProcess.spawnSync> }} input
|
|
3107
|
+
* @returns {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string }}
|
|
3108
|
+
*/
|
|
3109
|
+
function commandDiagnostic(input) {
|
|
3110
|
+
const output = commandOutput(input.result);
|
|
3111
|
+
return {
|
|
3112
|
+
code: input.code,
|
|
3113
|
+
severity: input.severity,
|
|
3114
|
+
message: output ? `${input.message}\n${output}` : input.message,
|
|
3115
|
+
path: input.path,
|
|
3116
|
+
suggestedFix: input.suggestedFix
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
/**
|
|
3121
|
+
* @param {ReturnType<typeof childProcess.spawnSync>} result
|
|
3122
|
+
* @returns {string}
|
|
3123
|
+
*/
|
|
3124
|
+
function commandOutput(result) {
|
|
3125
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
3126
|
+
}
|
|
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
|
+
|
|
3186
|
+
/**
|
|
3187
|
+
* @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
|
|
3188
|
+
* @param {{ strict?: boolean }} [options]
|
|
3189
|
+
* @returns {{ checked: boolean, ok: boolean|null, expectedWorkflow: string|null, headSha: string|null, run: { databaseId?: number, workflowName?: string, status?: string, conclusion?: string, headSha?: string, url?: string }|null, diagnostics: Array<Record<string, any>> }}
|
|
3190
|
+
*/
|
|
3191
|
+
function inspectConsumerCi(consumer, options = {}) {
|
|
3192
|
+
const diagnostics = [];
|
|
3193
|
+
const expectedWorkflow = consumer.workflow || expectedConsumerWorkflowName(consumer.name);
|
|
3194
|
+
if (!consumer.root || !fs.existsSync(consumer.root)) {
|
|
3195
|
+
return {
|
|
3196
|
+
checked: false,
|
|
3197
|
+
ok: null,
|
|
3198
|
+
expectedWorkflow,
|
|
3199
|
+
headSha: null,
|
|
3200
|
+
run: null,
|
|
3201
|
+
diagnostics: []
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
const headSha = currentGitHead(consumer.root);
|
|
3205
|
+
if (!headSha) {
|
|
3206
|
+
diagnostics.push({
|
|
3207
|
+
code: "release_consumer_head_unavailable",
|
|
3208
|
+
severity: options.strict ? "error" : "warning",
|
|
3209
|
+
message: `Could not inspect local HEAD for ${consumer.name}.`,
|
|
3210
|
+
path: consumer.root,
|
|
3211
|
+
suggestedFix: "Run from a checked-out consumer git repository."
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
if (!expectedWorkflow) {
|
|
3215
|
+
diagnostics.push({
|
|
3216
|
+
code: "release_consumer_workflow_unknown",
|
|
3217
|
+
severity: options.strict ? "error" : "warning",
|
|
3218
|
+
message: `No expected verification workflow is configured for ${consumer.name}.`,
|
|
3219
|
+
path: consumer.name,
|
|
3220
|
+
suggestedFix: "Add the consumer repo to KNOWN_CLI_CONSUMER_WORKFLOWS."
|
|
3221
|
+
});
|
|
3222
|
+
return {
|
|
3223
|
+
checked: true,
|
|
3224
|
+
ok: false,
|
|
3225
|
+
expectedWorkflow,
|
|
3226
|
+
headSha,
|
|
3227
|
+
run: null,
|
|
3228
|
+
diagnostics
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
const result = childProcess.spawnSync("gh", [
|
|
3232
|
+
"run",
|
|
3233
|
+
"list",
|
|
3234
|
+
"--repo",
|
|
3235
|
+
`attebury/${consumer.name}`,
|
|
3236
|
+
"--branch",
|
|
3237
|
+
"main",
|
|
3238
|
+
"--workflow",
|
|
3239
|
+
expectedWorkflow,
|
|
3240
|
+
"--limit",
|
|
3241
|
+
"1",
|
|
3242
|
+
"--json",
|
|
3243
|
+
"databaseId,workflowName,status,conclusion,headSha,url"
|
|
3244
|
+
], {
|
|
3245
|
+
cwd: consumer.root,
|
|
3246
|
+
encoding: "utf8",
|
|
3247
|
+
env: { ...process.env, PATH: process.env.PATH || "" }
|
|
3248
|
+
});
|
|
3249
|
+
if (result.status !== 0) {
|
|
3250
|
+
diagnostics.push(commandDiagnostic({
|
|
3251
|
+
code: "release_consumer_ci_unavailable",
|
|
3252
|
+
severity: options.strict ? "error" : "warning",
|
|
3253
|
+
message: `Could not inspect ${expectedWorkflow} for ${consumer.name}.`,
|
|
3254
|
+
path: `attebury/${consumer.name}`,
|
|
3255
|
+
suggestedFix: "Check GitHub CLI auth/network access, then rerun release status.",
|
|
3256
|
+
result
|
|
3257
|
+
}));
|
|
3258
|
+
return {
|
|
3259
|
+
checked: true,
|
|
3260
|
+
ok: false,
|
|
3261
|
+
expectedWorkflow,
|
|
3262
|
+
headSha,
|
|
3263
|
+
run: null,
|
|
3264
|
+
diagnostics
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
let runs = [];
|
|
3268
|
+
try {
|
|
3269
|
+
runs = JSON.parse(String(result.stdout || "[]"));
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
diagnostics.push({
|
|
3272
|
+
code: "release_consumer_ci_unreadable",
|
|
3273
|
+
severity: options.strict ? "error" : "warning",
|
|
3274
|
+
message: `Could not parse ${consumer.name} workflow status: ${messageFromError(error)}`,
|
|
3275
|
+
path: `attebury/${consumer.name}`,
|
|
3276
|
+
suggestedFix: "Rerun release status after GitHub CLI output is valid JSON."
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
const run = Array.isArray(runs) && runs.length > 0 ? runs[0] : null;
|
|
3280
|
+
if (!run) {
|
|
3281
|
+
diagnostics.push({
|
|
3282
|
+
code: "release_consumer_ci_missing",
|
|
3283
|
+
severity: options.strict ? "error" : "warning",
|
|
3284
|
+
message: `${consumer.name} has no ${expectedWorkflow} run on main.`,
|
|
3285
|
+
path: `attebury/${consumer.name}`,
|
|
3286
|
+
suggestedFix: "Push the consumer repo and wait for its verification workflow."
|
|
3287
|
+
});
|
|
3288
|
+
return {
|
|
3289
|
+
checked: true,
|
|
3290
|
+
ok: false,
|
|
3291
|
+
expectedWorkflow,
|
|
3292
|
+
headSha,
|
|
3293
|
+
run: null,
|
|
3294
|
+
diagnostics
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
if (headSha && run.headSha && run.headSha !== headSha) {
|
|
3298
|
+
diagnostics.push({
|
|
3299
|
+
code: "release_consumer_ci_head_mismatch",
|
|
3300
|
+
severity: options.strict ? "error" : "warning",
|
|
3301
|
+
message: `${consumer.name} latest ${expectedWorkflow} run is for ${run.headSha}, not checked-out HEAD ${headSha}.`,
|
|
3302
|
+
path: run.url || `attebury/${consumer.name}`,
|
|
3303
|
+
suggestedFix: "Wait for the verification workflow on the current consumer commit, then rerun release status."
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
if (run.status !== "completed" || run.conclusion !== "success") {
|
|
3307
|
+
diagnostics.push({
|
|
3308
|
+
code: "release_consumer_ci_not_successful",
|
|
3309
|
+
severity: options.strict ? "error" : "warning",
|
|
3310
|
+
message: `${consumer.name} ${expectedWorkflow} is ${run.status || "unknown"}/${run.conclusion || "unknown"}.`,
|
|
3311
|
+
path: run.url || `attebury/${consumer.name}`,
|
|
3312
|
+
suggestedFix: "Wait for or fix the consumer verification workflow, then rerun release status."
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
return {
|
|
3316
|
+
checked: true,
|
|
3317
|
+
ok: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length === 0 &&
|
|
3318
|
+
(!options.strict || (run.status === "completed" && run.conclusion === "success" && (!headSha || !run.headSha || run.headSha === headSha))),
|
|
3319
|
+
expectedWorkflow,
|
|
3320
|
+
headSha,
|
|
3321
|
+
run,
|
|
3322
|
+
diagnostics
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
/**
|
|
3327
|
+
* @param {string} cwd
|
|
3328
|
+
* @returns {Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean }>}
|
|
2703
3329
|
*/
|
|
2704
3330
|
function discoverTopogramCliVersionConsumers(cwd) {
|
|
2705
3331
|
const roots = [];
|
|
@@ -2718,6 +3344,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
|
|
|
2718
3344
|
if (fs.existsSync(consumerRoot) && !fs.existsSync(versionPath)) {
|
|
2719
3345
|
found = {
|
|
2720
3346
|
name,
|
|
3347
|
+
root: consumerRoot,
|
|
2721
3348
|
path: versionPath,
|
|
2722
3349
|
version: null,
|
|
2723
3350
|
found: false
|
|
@@ -2729,6 +3356,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
|
|
|
2729
3356
|
}
|
|
2730
3357
|
found = {
|
|
2731
3358
|
name,
|
|
3359
|
+
root: consumerRoot,
|
|
2732
3360
|
path: versionPath,
|
|
2733
3361
|
version: fs.readFileSync(versionPath, "utf8").trim() || null,
|
|
2734
3362
|
found: true
|
|
@@ -2737,6 +3365,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
|
|
|
2737
3365
|
}
|
|
2738
3366
|
consumers.push(found || {
|
|
2739
3367
|
name,
|
|
3368
|
+
root: null,
|
|
2740
3369
|
path: path.join(roots[0], name, "topogram-cli.version"),
|
|
2741
3370
|
version: null,
|
|
2742
3371
|
found: false
|
|
@@ -2766,6 +3395,30 @@ function summarizeConsumerPins(consumers) {
|
|
|
2766
3395
|
};
|
|
2767
3396
|
}
|
|
2768
3397
|
|
|
3398
|
+
/**
|
|
3399
|
+
* @param {Array<{ name: string, matchesLocal?: boolean|null, ci?: ReturnType<typeof inspectConsumerCi>|null }>} consumers
|
|
3400
|
+
* @returns {{ checked: number, passing: number, failing: number, unavailable: number, skipped: number, allCheckedAndPassing: boolean, passingNames: string[], failingNames: string[], unavailableNames: string[], skippedNames: string[] }}
|
|
3401
|
+
*/
|
|
3402
|
+
function summarizeConsumerCi(consumers) {
|
|
3403
|
+
const checked = consumers.filter((consumer) => consumer.ci?.checked);
|
|
3404
|
+
const passingNames = checked.filter((consumer) => consumer.ci?.ok === true).map((consumer) => consumer.name);
|
|
3405
|
+
const failingNames = checked.filter((consumer) => consumer.ci?.ok === false && consumer.ci?.run).map((consumer) => consumer.name);
|
|
3406
|
+
const unavailableNames = checked.filter((consumer) => consumer.ci?.ok === false && !consumer.ci?.run).map((consumer) => consumer.name);
|
|
3407
|
+
const skippedNames = consumers.filter((consumer) => !consumer.ci?.checked).map((consumer) => consumer.name);
|
|
3408
|
+
return {
|
|
3409
|
+
checked: checked.length,
|
|
3410
|
+
passing: passingNames.length,
|
|
3411
|
+
failing: failingNames.length,
|
|
3412
|
+
unavailable: unavailableNames.length,
|
|
3413
|
+
skipped: skippedNames.length,
|
|
3414
|
+
allCheckedAndPassing: consumers.length > 0 && checked.length === consumers.length && failingNames.length === 0 && unavailableNames.length === 0,
|
|
3415
|
+
passingNames,
|
|
3416
|
+
failingNames,
|
|
3417
|
+
unavailableNames,
|
|
3418
|
+
skippedNames
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
|
|
2769
3422
|
/**
|
|
2770
3423
|
* @param {string|null} source
|
|
2771
3424
|
* @returns {{ ok: boolean, node: { version: string, minimum: string, ok: boolean, diagnostics: any[] }, npm: { available: boolean, version: string|null, diagnostics: any[] }, packageRegistry: { required: boolean, reason: string|null, registry: string, configuredRegistry: string|null, registryConfigured: boolean, nodeAuthTokenEnv: boolean, packageName: string, packageSpec: string|null, packageAccess: { ok: boolean, checkedVersion: string|null, diagnostics: any[] } }, lockfile: ReturnType<typeof inspectTopogramCliLockfile>, catalog: ReturnType<typeof buildCatalogDoctorPayload>, diagnostics: any[], errors: string[] }}
|
|
@@ -7607,6 +8260,8 @@ if (args[0] === "version" || args[0] === "--version") {
|
|
|
7607
8260
|
commandArgs = { doctor: true, inputPath: args[1] && !args[1].startsWith("-") ? args[1] : null };
|
|
7608
8261
|
} else if (args[0] === "release" && args[1] === "status") {
|
|
7609
8262
|
commandArgs = { releaseStatus: true, inputPath: null };
|
|
8263
|
+
} else if (args[0] === "release" && args[1] === "roll-consumers") {
|
|
8264
|
+
commandArgs = { releaseRollConsumers: true, releaseRollVersion: args[2], inputPath: null };
|
|
7610
8265
|
} else if (args[0] === "new" || args[0] === "create") {
|
|
7611
8266
|
commandArgs = args.includes("--list-templates")
|
|
7612
8267
|
? { templateList: true, inputPath: null }
|
|
@@ -7823,9 +8478,19 @@ if (commandArgs && Object.prototype.hasOwnProperty.call(commandArgs, "inputPath"
|
|
|
7823
8478
|
}
|
|
7824
8479
|
const emitJson = args.includes("--json");
|
|
7825
8480
|
const strictReleaseStatus = args.includes("--strict");
|
|
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;
|
|
7826
8490
|
const shouldVersion = Boolean(commandArgs?.version);
|
|
7827
8491
|
const shouldDoctor = Boolean(commandArgs?.doctor);
|
|
7828
8492
|
const shouldReleaseStatus = Boolean(commandArgs?.releaseStatus);
|
|
8493
|
+
const shouldReleaseRollConsumers = Boolean(commandArgs?.releaseRollConsumers);
|
|
7829
8494
|
const shouldCheck = Boolean(commandArgs?.check);
|
|
7830
8495
|
const shouldComponentCheck = Boolean(commandArgs?.componentCheck);
|
|
7831
8496
|
const shouldComponentBehavior = Boolean(commandArgs?.componentBehavior);
|
|
@@ -8000,6 +8665,18 @@ if (shouldPackageUpdateCli && !inputPath) {
|
|
|
8000
8665
|
process.exit(1);
|
|
8001
8666
|
}
|
|
8002
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
|
+
|
|
8003
8680
|
if (shouldImportWorkspace && !outPath) {
|
|
8004
8681
|
console.error("Missing required --out <target>.");
|
|
8005
8682
|
printImportHelp();
|
|
@@ -8051,10 +8728,33 @@ try {
|
|
|
8051
8728
|
|
|
8052
8729
|
if (shouldReleaseStatus) {
|
|
8053
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
|
+
}
|
|
8054
8736
|
if (emitJson) {
|
|
8055
8737
|
console.log(stableStringify(payload));
|
|
8738
|
+
} else if (shouldPrintReleaseMarkdown) {
|
|
8739
|
+
console.log(renderReleaseStatusMarkdown(payload).trimEnd());
|
|
8056
8740
|
} else {
|
|
8057
8741
|
printReleaseStatus(payload);
|
|
8742
|
+
if (releaseReportPath) {
|
|
8743
|
+
console.log(`Report: ${path.resolve(releaseReportPath)}`);
|
|
8744
|
+
}
|
|
8745
|
+
}
|
|
8746
|
+
process.exit(payload.ok ? 0 : 1);
|
|
8747
|
+
}
|
|
8748
|
+
|
|
8749
|
+
if (shouldReleaseRollConsumers) {
|
|
8750
|
+
const payload = buildReleaseRollConsumersPayload(commandArgs.releaseRollVersion, {
|
|
8751
|
+
push: shouldPushReleaseConsumers,
|
|
8752
|
+
watch: shouldWatchReleaseConsumers
|
|
8753
|
+
});
|
|
8754
|
+
if (emitJson) {
|
|
8755
|
+
console.log(stableStringify(payload));
|
|
8756
|
+
} else {
|
|
8757
|
+
printReleaseRollConsumers(payload);
|
|
8058
8758
|
}
|
|
8059
8759
|
process.exit(payload.ok ? 0 : 1);
|
|
8060
8760
|
}
|