@topogram/cli 0.3.46 → 0.3.47
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 +535 -8
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",
|
|
@@ -178,6 +192,7 @@ function printUsage(options = {}) {
|
|
|
178
192
|
console.log("Usage: topogram doctor [--json] [--catalog <path-or-source>]");
|
|
179
193
|
console.log("Usage: topogram setup package-auth|catalog-auth");
|
|
180
194
|
console.log("Usage: topogram release status [--json] [--strict]");
|
|
195
|
+
console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push]");
|
|
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");
|
|
@@ -744,13 +760,17 @@ function printPackageHelp() {
|
|
|
744
760
|
|
|
745
761
|
function printReleaseHelp() {
|
|
746
762
|
console.log("Usage: topogram release status [--json] [--strict]");
|
|
763
|
+
console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push]");
|
|
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 prints latest workflow URLs.");
|
|
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 roll-consumers 0.3.46");
|
|
773
|
+
console.log(" topogram release roll-consumers --latest");
|
|
754
774
|
console.log("");
|
|
755
775
|
console.log("Release preparation and publishing are repo-level tasks in the Topogram source checkout:");
|
|
756
776
|
console.log(" npm run release:prepare -- <version>");
|
|
@@ -2493,8 +2513,208 @@ function printPackageUpdateCli(payload) {
|
|
|
2493
2513
|
}
|
|
2494
2514
|
|
|
2495
2515
|
/**
|
|
2496
|
-
* @param {
|
|
2497
|
-
* @
|
|
2516
|
+
* @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[] }}
|
|
2519
|
+
*/
|
|
2520
|
+
function buildReleaseRollConsumersPayload(requested, options = {}) {
|
|
2521
|
+
const cwd = options.cwd || process.cwd();
|
|
2522
|
+
const push = options.push !== false;
|
|
2523
|
+
const requestedLatest = requested === "latest" || requested === "--latest";
|
|
2524
|
+
const diagnostics = [];
|
|
2525
|
+
const version = requestedLatest
|
|
2526
|
+
? resolveLatestTopogramCliVersionForPackageUpdate(cwd, diagnostics)
|
|
2527
|
+
: requested;
|
|
2528
|
+
if (!isPackageVersion(version)) {
|
|
2529
|
+
throw new Error("topogram release roll-consumers requires <version> or --latest.");
|
|
2530
|
+
}
|
|
2531
|
+
const consumers = [];
|
|
2532
|
+
for (const consumer of discoverTopogramCliVersionConsumers(cwd)) {
|
|
2533
|
+
const workflow = expectedConsumerWorkflowName(consumer.name);
|
|
2534
|
+
const item = {
|
|
2535
|
+
name: consumer.name,
|
|
2536
|
+
root: consumer.root,
|
|
2537
|
+
workflow,
|
|
2538
|
+
updated: false,
|
|
2539
|
+
committed: false,
|
|
2540
|
+
pushed: false,
|
|
2541
|
+
commit: null,
|
|
2542
|
+
update: null,
|
|
2543
|
+
ci: null,
|
|
2544
|
+
diagnostics: []
|
|
2545
|
+
};
|
|
2546
|
+
consumers.push(item);
|
|
2547
|
+
if (!consumer.root || !fs.existsSync(consumer.root)) {
|
|
2548
|
+
item.diagnostics.push({
|
|
2549
|
+
code: "release_consumer_repo_missing",
|
|
2550
|
+
severity: "error",
|
|
2551
|
+
message: `First-party consumer repo ${consumer.name} was not found.`,
|
|
2552
|
+
path: consumer.path,
|
|
2553
|
+
suggestedFix: `Clone ${consumer.name} beside the topogram repo, then rerun roll-consumers.`
|
|
2554
|
+
});
|
|
2555
|
+
diagnostics.push(...item.diagnostics);
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const packagePath = path.join(consumer.root, "package.json");
|
|
2559
|
+
if (!fs.existsSync(packagePath)) {
|
|
2560
|
+
item.diagnostics.push({
|
|
2561
|
+
code: "release_consumer_package_missing",
|
|
2562
|
+
severity: "error",
|
|
2563
|
+
message: `First-party consumer repo ${consumer.name} does not contain package.json.`,
|
|
2564
|
+
path: packagePath,
|
|
2565
|
+
suggestedFix: "Only package-backed first-party consumers can be rolled by this command."
|
|
2566
|
+
});
|
|
2567
|
+
diagnostics.push(...item.diagnostics);
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
const clean = inspectGitWorktreeClean(consumer.root);
|
|
2571
|
+
if (clean.ok !== true) {
|
|
2572
|
+
item.diagnostics.push({
|
|
2573
|
+
code: "release_consumer_worktree_dirty",
|
|
2574
|
+
severity: "error",
|
|
2575
|
+
message: clean.error || `First-party consumer repo ${consumer.name} has uncommitted changes.`,
|
|
2576
|
+
path: consumer.root,
|
|
2577
|
+
suggestedFix: "Commit, stash, or discard unrelated consumer changes before rolling the CLI version."
|
|
2578
|
+
});
|
|
2579
|
+
diagnostics.push(...item.diagnostics);
|
|
2580
|
+
continue;
|
|
2581
|
+
}
|
|
2582
|
+
try {
|
|
2583
|
+
item.update = buildPackageUpdateCliPayload(version, { cwd: consumer.root });
|
|
2584
|
+
item.updated = true;
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
item.diagnostics.push({
|
|
2587
|
+
code: "release_consumer_update_failed",
|
|
2588
|
+
severity: "error",
|
|
2589
|
+
message: `Failed to update ${consumer.name}: ${messageFromError(error)}`,
|
|
2590
|
+
path: consumer.root,
|
|
2591
|
+
suggestedFix: "Fix the consumer update/check failure, then rerun roll-consumers."
|
|
2592
|
+
});
|
|
2593
|
+
diagnostics.push(...item.diagnostics);
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
const filesToStage = ["package.json", "package-lock.json", "topogram-cli.version"]
|
|
2597
|
+
.filter((file) => fs.existsSync(path.join(consumer.root, file)));
|
|
2598
|
+
const addResult = runGit(["add", ...filesToStage], consumer.root);
|
|
2599
|
+
if (addResult.status !== 0) {
|
|
2600
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2601
|
+
code: "release_consumer_git_add_failed",
|
|
2602
|
+
severity: "error",
|
|
2603
|
+
message: `Failed to stage ${consumer.name} CLI update.`,
|
|
2604
|
+
path: consumer.root,
|
|
2605
|
+
suggestedFix: "Inspect git output, stage the changed files manually, then commit and push.",
|
|
2606
|
+
result: addResult
|
|
2607
|
+
}));
|
|
2608
|
+
diagnostics.push(...item.diagnostics);
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
const staged = hasStagedGitChanges(consumer.root);
|
|
2612
|
+
if (!staged.ok) {
|
|
2613
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2614
|
+
code: "release_consumer_git_diff_failed",
|
|
2615
|
+
severity: "error",
|
|
2616
|
+
message: `Could not inspect staged changes for ${consumer.name}.`,
|
|
2617
|
+
path: consumer.root,
|
|
2618
|
+
suggestedFix: "Inspect git status manually before committing.",
|
|
2619
|
+
result: staged.result
|
|
2620
|
+
}));
|
|
2621
|
+
diagnostics.push(...item.diagnostics);
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
if (!staged.changed) {
|
|
2625
|
+
item.ci = inspectConsumerCi(consumer, { strict: false });
|
|
2626
|
+
item.diagnostics.push(...item.ci.diagnostics);
|
|
2627
|
+
diagnostics.push(...item.ci.diagnostics);
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
const commitResult = runGit(["commit", "-m", `Update Topogram CLI to ${version}`], consumer.root);
|
|
2631
|
+
if (commitResult.status !== 0) {
|
|
2632
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2633
|
+
code: "release_consumer_git_commit_failed",
|
|
2634
|
+
severity: "error",
|
|
2635
|
+
message: `Failed to commit ${consumer.name} CLI update.`,
|
|
2636
|
+
path: consumer.root,
|
|
2637
|
+
suggestedFix: "Inspect git output, commit the consumer update manually, then push.",
|
|
2638
|
+
result: commitResult
|
|
2639
|
+
}));
|
|
2640
|
+
diagnostics.push(...item.diagnostics);
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
item.committed = true;
|
|
2644
|
+
item.commit = currentGitHead(consumer.root);
|
|
2645
|
+
if (push) {
|
|
2646
|
+
const pushResult = runGit(["push", "origin", "main"], consumer.root);
|
|
2647
|
+
if (pushResult.status !== 0) {
|
|
2648
|
+
item.diagnostics.push(commandDiagnostic({
|
|
2649
|
+
code: "release_consumer_git_push_failed",
|
|
2650
|
+
severity: "error",
|
|
2651
|
+
message: `Failed to push ${consumer.name} CLI update.`,
|
|
2652
|
+
path: consumer.root,
|
|
2653
|
+
suggestedFix: "Push the consumer update manually, then confirm its verification workflow passes.",
|
|
2654
|
+
result: pushResult
|
|
2655
|
+
}));
|
|
2656
|
+
diagnostics.push(...item.diagnostics);
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
item.pushed = true;
|
|
2660
|
+
}
|
|
2661
|
+
item.ci = inspectConsumerCi(consumer, { strict: false });
|
|
2662
|
+
item.diagnostics.push(...item.ci.diagnostics);
|
|
2663
|
+
diagnostics.push(...item.ci.diagnostics);
|
|
2664
|
+
}
|
|
2665
|
+
const errors = diagnostics
|
|
2666
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
2667
|
+
.map((diagnostic) => diagnostic.message);
|
|
2668
|
+
return {
|
|
2669
|
+
ok: errors.length === 0,
|
|
2670
|
+
packageName: CLI_PACKAGE_NAME,
|
|
2671
|
+
requestedVersion: version,
|
|
2672
|
+
requestedLatest,
|
|
2673
|
+
pushed: push,
|
|
2674
|
+
consumers,
|
|
2675
|
+
diagnostics,
|
|
2676
|
+
errors
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
/**
|
|
2681
|
+
* @param {ReturnType<typeof buildReleaseRollConsumersPayload>} payload
|
|
2682
|
+
* @returns {void}
|
|
2683
|
+
*/
|
|
2684
|
+
function printReleaseRollConsumers(payload) {
|
|
2685
|
+
console.log(payload.ok ? "Topogram consumer rollout completed." : "Topogram consumer rollout found issues.");
|
|
2686
|
+
if (payload.requestedLatest) {
|
|
2687
|
+
console.log(`Resolved latest version: ${payload.requestedVersion}`);
|
|
2688
|
+
}
|
|
2689
|
+
console.log(`Package: ${payload.packageName}@${payload.requestedVersion}`);
|
|
2690
|
+
console.log(`Push: ${payload.pushed ? "enabled" : "disabled"}`);
|
|
2691
|
+
for (const consumer of payload.consumers) {
|
|
2692
|
+
const state = consumer.committed
|
|
2693
|
+
? consumer.pushed ? "pushed" : "committed"
|
|
2694
|
+
: consumer.updated ? "updated" : "skipped";
|
|
2695
|
+
console.log(`- ${consumer.name}: ${state}`);
|
|
2696
|
+
if (consumer.update) {
|
|
2697
|
+
console.log(` Checks run: ${consumer.update.scriptsRun.join(", ") || "none"}`);
|
|
2698
|
+
}
|
|
2699
|
+
if (consumer.commit) {
|
|
2700
|
+
console.log(` Commit: ${consumer.commit}`);
|
|
2701
|
+
}
|
|
2702
|
+
if (consumer.ci?.run?.url) {
|
|
2703
|
+
const run = consumer.ci.run;
|
|
2704
|
+
console.log(` CI: ${run.workflowName || consumer.workflow} ${run.status || "unknown"}/${run.conclusion || "unknown"} ${run.url}`);
|
|
2705
|
+
} else if (consumer.workflow) {
|
|
2706
|
+
console.log(` CI: ${consumer.workflow} not found`);
|
|
2707
|
+
}
|
|
2708
|
+
for (const diagnostic of consumer.diagnostics || []) {
|
|
2709
|
+
const label = diagnostic.severity === "error" ? "Error" : diagnostic.severity === "warning" ? "Warning" : "Note";
|
|
2710
|
+
console.log(` ${label}: ${diagnostic.message}`);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/**
|
|
2716
|
+
* @param {{ cwd?: string, strict?: boolean }} [options]
|
|
2717
|
+
* @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
2718
|
*/
|
|
2499
2719
|
function buildReleaseStatusPayload(options = {}) {
|
|
2500
2720
|
const cwd = options.cwd || process.cwd();
|
|
@@ -2517,9 +2737,20 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2517
2737
|
diagnostics.push(...git.diagnostics);
|
|
2518
2738
|
const consumers = discoverTopogramCliVersionConsumers(cwd).map((consumer) => ({
|
|
2519
2739
|
...consumer,
|
|
2520
|
-
matchesLocal: consumer.version ? consumer.version === localVersion : null
|
|
2740
|
+
matchesLocal: consumer.version ? consumer.version === localVersion : null,
|
|
2741
|
+
workflow: expectedConsumerWorkflowName(consumer.name),
|
|
2742
|
+
ci: null
|
|
2521
2743
|
}));
|
|
2744
|
+
if (strict) {
|
|
2745
|
+
for (const consumer of consumers) {
|
|
2746
|
+
if (consumer.matchesLocal === true) {
|
|
2747
|
+
consumer.ci = inspectConsumerCi(consumer, { strict: true });
|
|
2748
|
+
diagnostics.push(...consumer.ci.diagnostics);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2522
2752
|
const consumerPins = summarizeConsumerPins(consumers);
|
|
2753
|
+
const consumerCi = summarizeConsumerCi(consumers);
|
|
2523
2754
|
const currentPublished = latestVersion ? latestVersion === localVersion : null;
|
|
2524
2755
|
if (strict) {
|
|
2525
2756
|
diagnostics.push(...releaseStatusStrictDiagnostics({
|
|
@@ -2527,7 +2758,8 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2527
2758
|
latestVersion,
|
|
2528
2759
|
currentPublished,
|
|
2529
2760
|
git,
|
|
2530
|
-
consumerPins
|
|
2761
|
+
consumerPins,
|
|
2762
|
+
consumerCi
|
|
2531
2763
|
}));
|
|
2532
2764
|
}
|
|
2533
2765
|
const errors = diagnostics
|
|
@@ -2542,6 +2774,7 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2542
2774
|
currentPublished,
|
|
2543
2775
|
git,
|
|
2544
2776
|
consumerPins,
|
|
2777
|
+
consumerCi,
|
|
2545
2778
|
consumers,
|
|
2546
2779
|
diagnostics,
|
|
2547
2780
|
errors
|
|
@@ -2554,7 +2787,8 @@ function buildReleaseStatusPayload(options = {}) {
|
|
|
2554
2787
|
* latestVersion: string|null,
|
|
2555
2788
|
* currentPublished: boolean|null,
|
|
2556
2789
|
* git: ReturnType<typeof inspectReleaseGitTag>,
|
|
2557
|
-
* consumerPins: ReturnType<typeof summarizeConsumerPins
|
|
2790
|
+
* consumerPins: ReturnType<typeof summarizeConsumerPins>,
|
|
2791
|
+
* consumerCi: ReturnType<typeof summarizeConsumerCi>
|
|
2558
2792
|
* }} release
|
|
2559
2793
|
* @returns {Array<{ code: string, severity: "error", message: string, path: string, suggestedFix: string }>}
|
|
2560
2794
|
*/
|
|
@@ -2598,6 +2832,15 @@ function releaseStatusStrictDiagnostics(release) {
|
|
|
2598
2832
|
suggestedFix: "Roll first-party consumer repositories to the current CLI version before treating this release as complete."
|
|
2599
2833
|
});
|
|
2600
2834
|
}
|
|
2835
|
+
if (release.consumerCi.allCheckedAndPassing !== true) {
|
|
2836
|
+
diagnostics.push({
|
|
2837
|
+
code: "release_consumer_ci_not_current",
|
|
2838
|
+
severity: "error",
|
|
2839
|
+
message: "First-party consumer verification workflows are not all passing on the checked-out consumer commits.",
|
|
2840
|
+
path: "GitHub Actions",
|
|
2841
|
+
suggestedFix: "Wait for or fix the consumer verification workflows, then rerun `topogram release status --strict`."
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2601
2844
|
return diagnostics;
|
|
2602
2845
|
}
|
|
2603
2846
|
|
|
@@ -2618,13 +2861,27 @@ function printReleaseStatus(payload) {
|
|
|
2618
2861
|
`Consumer pins: ${payload.consumerPins.pinned}/${payload.consumerPins.known} pinned, ` +
|
|
2619
2862
|
`${payload.consumerPins.matching} matching, ${payload.consumerPins.differing} differing, ${payload.consumerPins.missing} missing`
|
|
2620
2863
|
);
|
|
2864
|
+
if (payload.strict) {
|
|
2865
|
+
console.log(
|
|
2866
|
+
`Consumer CI: ${payload.consumerCi.passing}/${payload.consumerCi.checked} passing, ` +
|
|
2867
|
+
`${payload.consumerCi.failing} failing, ${payload.consumerCi.unavailable} unavailable, ${payload.consumerCi.skipped} skipped`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2621
2870
|
for (const consumer of payload.consumers) {
|
|
2622
2871
|
const status = consumer.matchesLocal === true
|
|
2623
2872
|
? "matches"
|
|
2624
2873
|
: consumer.matchesLocal === false
|
|
2625
2874
|
? "differs"
|
|
2626
2875
|
: "missing";
|
|
2627
|
-
|
|
2876
|
+
const ciStatus = consumer.ci?.run
|
|
2877
|
+
? `; ${consumer.ci.run.workflowName || consumer.workflow}: ${consumer.ci.run.status || "unknown"}/${consumer.ci.run.conclusion || "unknown"}`
|
|
2878
|
+
: consumer.ci?.checked
|
|
2879
|
+
? `; ${consumer.workflow || "workflow"} unavailable`
|
|
2880
|
+
: "";
|
|
2881
|
+
console.log(`- ${consumer.name}: ${consumer.version || "missing"} (${status})${ciStatus}`);
|
|
2882
|
+
if (consumer.ci?.run?.url) {
|
|
2883
|
+
console.log(` CI: ${consumer.ci.run.url}`);
|
|
2884
|
+
}
|
|
2628
2885
|
}
|
|
2629
2886
|
if (payload.diagnostics.length > 0) {
|
|
2630
2887
|
console.log("Diagnostics:");
|
|
@@ -2698,8 +2955,235 @@ function inspectReleaseGitTag(version, cwd) {
|
|
|
2698
2955
|
}
|
|
2699
2956
|
|
|
2700
2957
|
/**
|
|
2958
|
+
* @param {string} name
|
|
2959
|
+
* @returns {string|null}
|
|
2960
|
+
*/
|
|
2961
|
+
function expectedConsumerWorkflowName(name) {
|
|
2962
|
+
return KNOWN_CLI_CONSUMER_WORKFLOWS[name] || null;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
/**
|
|
2966
|
+
* @param {string[]} args
|
|
2701
2967
|
* @param {string} cwd
|
|
2702
|
-
* @returns {
|
|
2968
|
+
* @returns {ReturnType<typeof childProcess.spawnSync>}
|
|
2969
|
+
*/
|
|
2970
|
+
function runGit(args, cwd) {
|
|
2971
|
+
return childProcess.spawnSync("git", args, {
|
|
2972
|
+
cwd,
|
|
2973
|
+
encoding: "utf8",
|
|
2974
|
+
env: { ...process.env, PATH: process.env.PATH || "" }
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
/**
|
|
2979
|
+
* @param {string} cwd
|
|
2980
|
+
* @returns {{ ok: boolean, dirty: boolean|null, error: string|null }}
|
|
2981
|
+
*/
|
|
2982
|
+
function inspectGitWorktreeClean(cwd) {
|
|
2983
|
+
const result = runGit(["status", "--porcelain"], cwd);
|
|
2984
|
+
if (result.status !== 0) {
|
|
2985
|
+
return {
|
|
2986
|
+
ok: false,
|
|
2987
|
+
dirty: null,
|
|
2988
|
+
error: `Could not inspect git status: ${commandOutput(result) || "unknown error"}`
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
const dirty = String(result.stdout || "").trim().length > 0;
|
|
2992
|
+
return {
|
|
2993
|
+
ok: !dirty,
|
|
2994
|
+
dirty,
|
|
2995
|
+
error: dirty ? "Consumer repo has uncommitted changes." : null
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
/**
|
|
3000
|
+
* @param {string} cwd
|
|
3001
|
+
* @returns {{ ok: boolean, changed: boolean, result: ReturnType<typeof childProcess.spawnSync> }}
|
|
3002
|
+
*/
|
|
3003
|
+
function hasStagedGitChanges(cwd) {
|
|
3004
|
+
const result = runGit(["diff", "--cached", "--quiet"], cwd);
|
|
3005
|
+
return {
|
|
3006
|
+
ok: result.status === 0 || result.status === 1,
|
|
3007
|
+
changed: result.status === 1,
|
|
3008
|
+
result
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
/**
|
|
3013
|
+
* @param {string} cwd
|
|
3014
|
+
* @returns {string|null}
|
|
3015
|
+
*/
|
|
3016
|
+
function currentGitHead(cwd) {
|
|
3017
|
+
const result = runGit(["rev-parse", "HEAD"], cwd);
|
|
3018
|
+
return result.status === 0 ? String(result.stdout || "").trim() || null : null;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
/**
|
|
3022
|
+
* @param {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string, result: ReturnType<typeof childProcess.spawnSync> }} input
|
|
3023
|
+
* @returns {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string }}
|
|
3024
|
+
*/
|
|
3025
|
+
function commandDiagnostic(input) {
|
|
3026
|
+
const output = commandOutput(input.result);
|
|
3027
|
+
return {
|
|
3028
|
+
code: input.code,
|
|
3029
|
+
severity: input.severity,
|
|
3030
|
+
message: output ? `${input.message}\n${output}` : input.message,
|
|
3031
|
+
path: input.path,
|
|
3032
|
+
suggestedFix: input.suggestedFix
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
/**
|
|
3037
|
+
* @param {ReturnType<typeof childProcess.spawnSync>} result
|
|
3038
|
+
* @returns {string}
|
|
3039
|
+
*/
|
|
3040
|
+
function commandOutput(result) {
|
|
3041
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
/**
|
|
3045
|
+
* @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
|
|
3046
|
+
* @param {{ strict?: boolean }} [options]
|
|
3047
|
+
* @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>> }}
|
|
3048
|
+
*/
|
|
3049
|
+
function inspectConsumerCi(consumer, options = {}) {
|
|
3050
|
+
const diagnostics = [];
|
|
3051
|
+
const expectedWorkflow = consumer.workflow || expectedConsumerWorkflowName(consumer.name);
|
|
3052
|
+
if (!consumer.root || !fs.existsSync(consumer.root)) {
|
|
3053
|
+
return {
|
|
3054
|
+
checked: false,
|
|
3055
|
+
ok: null,
|
|
3056
|
+
expectedWorkflow,
|
|
3057
|
+
headSha: null,
|
|
3058
|
+
run: null,
|
|
3059
|
+
diagnostics: []
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
const headSha = currentGitHead(consumer.root);
|
|
3063
|
+
if (!headSha) {
|
|
3064
|
+
diagnostics.push({
|
|
3065
|
+
code: "release_consumer_head_unavailable",
|
|
3066
|
+
severity: options.strict ? "error" : "warning",
|
|
3067
|
+
message: `Could not inspect local HEAD for ${consumer.name}.`,
|
|
3068
|
+
path: consumer.root,
|
|
3069
|
+
suggestedFix: "Run from a checked-out consumer git repository."
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
if (!expectedWorkflow) {
|
|
3073
|
+
diagnostics.push({
|
|
3074
|
+
code: "release_consumer_workflow_unknown",
|
|
3075
|
+
severity: options.strict ? "error" : "warning",
|
|
3076
|
+
message: `No expected verification workflow is configured for ${consumer.name}.`,
|
|
3077
|
+
path: consumer.name,
|
|
3078
|
+
suggestedFix: "Add the consumer repo to KNOWN_CLI_CONSUMER_WORKFLOWS."
|
|
3079
|
+
});
|
|
3080
|
+
return {
|
|
3081
|
+
checked: true,
|
|
3082
|
+
ok: false,
|
|
3083
|
+
expectedWorkflow,
|
|
3084
|
+
headSha,
|
|
3085
|
+
run: null,
|
|
3086
|
+
diagnostics
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
const result = childProcess.spawnSync("gh", [
|
|
3090
|
+
"run",
|
|
3091
|
+
"list",
|
|
3092
|
+
"--repo",
|
|
3093
|
+
`attebury/${consumer.name}`,
|
|
3094
|
+
"--branch",
|
|
3095
|
+
"main",
|
|
3096
|
+
"--workflow",
|
|
3097
|
+
expectedWorkflow,
|
|
3098
|
+
"--limit",
|
|
3099
|
+
"1",
|
|
3100
|
+
"--json",
|
|
3101
|
+
"databaseId,workflowName,status,conclusion,headSha,url"
|
|
3102
|
+
], {
|
|
3103
|
+
cwd: consumer.root,
|
|
3104
|
+
encoding: "utf8",
|
|
3105
|
+
env: { ...process.env, PATH: process.env.PATH || "" }
|
|
3106
|
+
});
|
|
3107
|
+
if (result.status !== 0) {
|
|
3108
|
+
diagnostics.push(commandDiagnostic({
|
|
3109
|
+
code: "release_consumer_ci_unavailable",
|
|
3110
|
+
severity: options.strict ? "error" : "warning",
|
|
3111
|
+
message: `Could not inspect ${expectedWorkflow} for ${consumer.name}.`,
|
|
3112
|
+
path: `attebury/${consumer.name}`,
|
|
3113
|
+
suggestedFix: "Check GitHub CLI auth/network access, then rerun release status.",
|
|
3114
|
+
result
|
|
3115
|
+
}));
|
|
3116
|
+
return {
|
|
3117
|
+
checked: true,
|
|
3118
|
+
ok: false,
|
|
3119
|
+
expectedWorkflow,
|
|
3120
|
+
headSha,
|
|
3121
|
+
run: null,
|
|
3122
|
+
diagnostics
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
let runs = [];
|
|
3126
|
+
try {
|
|
3127
|
+
runs = JSON.parse(String(result.stdout || "[]"));
|
|
3128
|
+
} catch (error) {
|
|
3129
|
+
diagnostics.push({
|
|
3130
|
+
code: "release_consumer_ci_unreadable",
|
|
3131
|
+
severity: options.strict ? "error" : "warning",
|
|
3132
|
+
message: `Could not parse ${consumer.name} workflow status: ${messageFromError(error)}`,
|
|
3133
|
+
path: `attebury/${consumer.name}`,
|
|
3134
|
+
suggestedFix: "Rerun release status after GitHub CLI output is valid JSON."
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
const run = Array.isArray(runs) && runs.length > 0 ? runs[0] : null;
|
|
3138
|
+
if (!run) {
|
|
3139
|
+
diagnostics.push({
|
|
3140
|
+
code: "release_consumer_ci_missing",
|
|
3141
|
+
severity: options.strict ? "error" : "warning",
|
|
3142
|
+
message: `${consumer.name} has no ${expectedWorkflow} run on main.`,
|
|
3143
|
+
path: `attebury/${consumer.name}`,
|
|
3144
|
+
suggestedFix: "Push the consumer repo and wait for its verification workflow."
|
|
3145
|
+
});
|
|
3146
|
+
return {
|
|
3147
|
+
checked: true,
|
|
3148
|
+
ok: false,
|
|
3149
|
+
expectedWorkflow,
|
|
3150
|
+
headSha,
|
|
3151
|
+
run: null,
|
|
3152
|
+
diagnostics
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
if (headSha && run.headSha && run.headSha !== headSha) {
|
|
3156
|
+
diagnostics.push({
|
|
3157
|
+
code: "release_consumer_ci_head_mismatch",
|
|
3158
|
+
severity: options.strict ? "error" : "warning",
|
|
3159
|
+
message: `${consumer.name} latest ${expectedWorkflow} run is for ${run.headSha}, not checked-out HEAD ${headSha}.`,
|
|
3160
|
+
path: run.url || `attebury/${consumer.name}`,
|
|
3161
|
+
suggestedFix: "Wait for the verification workflow on the current consumer commit, then rerun release status."
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
if (run.status !== "completed" || run.conclusion !== "success") {
|
|
3165
|
+
diagnostics.push({
|
|
3166
|
+
code: "release_consumer_ci_not_successful",
|
|
3167
|
+
severity: options.strict ? "error" : "warning",
|
|
3168
|
+
message: `${consumer.name} ${expectedWorkflow} is ${run.status || "unknown"}/${run.conclusion || "unknown"}.`,
|
|
3169
|
+
path: run.url || `attebury/${consumer.name}`,
|
|
3170
|
+
suggestedFix: "Wait for or fix the consumer verification workflow, then rerun release status."
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
return {
|
|
3174
|
+
checked: true,
|
|
3175
|
+
ok: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length === 0 &&
|
|
3176
|
+
(!options.strict || (run.status === "completed" && run.conclusion === "success" && (!headSha || !run.headSha || run.headSha === headSha))),
|
|
3177
|
+
expectedWorkflow,
|
|
3178
|
+
headSha,
|
|
3179
|
+
run,
|
|
3180
|
+
diagnostics
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/**
|
|
3185
|
+
* @param {string} cwd
|
|
3186
|
+
* @returns {Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean }>}
|
|
2703
3187
|
*/
|
|
2704
3188
|
function discoverTopogramCliVersionConsumers(cwd) {
|
|
2705
3189
|
const roots = [];
|
|
@@ -2718,6 +3202,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
|
|
|
2718
3202
|
if (fs.existsSync(consumerRoot) && !fs.existsSync(versionPath)) {
|
|
2719
3203
|
found = {
|
|
2720
3204
|
name,
|
|
3205
|
+
root: consumerRoot,
|
|
2721
3206
|
path: versionPath,
|
|
2722
3207
|
version: null,
|
|
2723
3208
|
found: false
|
|
@@ -2729,6 +3214,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
|
|
|
2729
3214
|
}
|
|
2730
3215
|
found = {
|
|
2731
3216
|
name,
|
|
3217
|
+
root: consumerRoot,
|
|
2732
3218
|
path: versionPath,
|
|
2733
3219
|
version: fs.readFileSync(versionPath, "utf8").trim() || null,
|
|
2734
3220
|
found: true
|
|
@@ -2737,6 +3223,7 @@ function discoverTopogramCliVersionConsumers(cwd) {
|
|
|
2737
3223
|
}
|
|
2738
3224
|
consumers.push(found || {
|
|
2739
3225
|
name,
|
|
3226
|
+
root: null,
|
|
2740
3227
|
path: path.join(roots[0], name, "topogram-cli.version"),
|
|
2741
3228
|
version: null,
|
|
2742
3229
|
found: false
|
|
@@ -2766,6 +3253,30 @@ function summarizeConsumerPins(consumers) {
|
|
|
2766
3253
|
};
|
|
2767
3254
|
}
|
|
2768
3255
|
|
|
3256
|
+
/**
|
|
3257
|
+
* @param {Array<{ name: string, matchesLocal?: boolean|null, ci?: ReturnType<typeof inspectConsumerCi>|null }>} consumers
|
|
3258
|
+
* @returns {{ checked: number, passing: number, failing: number, unavailable: number, skipped: number, allCheckedAndPassing: boolean, passingNames: string[], failingNames: string[], unavailableNames: string[], skippedNames: string[] }}
|
|
3259
|
+
*/
|
|
3260
|
+
function summarizeConsumerCi(consumers) {
|
|
3261
|
+
const checked = consumers.filter((consumer) => consumer.ci?.checked);
|
|
3262
|
+
const passingNames = checked.filter((consumer) => consumer.ci?.ok === true).map((consumer) => consumer.name);
|
|
3263
|
+
const failingNames = checked.filter((consumer) => consumer.ci?.ok === false && consumer.ci?.run).map((consumer) => consumer.name);
|
|
3264
|
+
const unavailableNames = checked.filter((consumer) => consumer.ci?.ok === false && !consumer.ci?.run).map((consumer) => consumer.name);
|
|
3265
|
+
const skippedNames = consumers.filter((consumer) => !consumer.ci?.checked).map((consumer) => consumer.name);
|
|
3266
|
+
return {
|
|
3267
|
+
checked: checked.length,
|
|
3268
|
+
passing: passingNames.length,
|
|
3269
|
+
failing: failingNames.length,
|
|
3270
|
+
unavailable: unavailableNames.length,
|
|
3271
|
+
skipped: skippedNames.length,
|
|
3272
|
+
allCheckedAndPassing: consumers.length > 0 && checked.length === consumers.length && failingNames.length === 0 && unavailableNames.length === 0,
|
|
3273
|
+
passingNames,
|
|
3274
|
+
failingNames,
|
|
3275
|
+
unavailableNames,
|
|
3276
|
+
skippedNames
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
|
|
2769
3280
|
/**
|
|
2770
3281
|
* @param {string|null} source
|
|
2771
3282
|
* @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 +8118,8 @@ if (args[0] === "version" || args[0] === "--version") {
|
|
|
7607
8118
|
commandArgs = { doctor: true, inputPath: args[1] && !args[1].startsWith("-") ? args[1] : null };
|
|
7608
8119
|
} else if (args[0] === "release" && args[1] === "status") {
|
|
7609
8120
|
commandArgs = { releaseStatus: true, inputPath: null };
|
|
8121
|
+
} else if (args[0] === "release" && args[1] === "roll-consumers") {
|
|
8122
|
+
commandArgs = { releaseRollConsumers: true, releaseRollVersion: args[2], inputPath: null };
|
|
7610
8123
|
} else if (args[0] === "new" || args[0] === "create") {
|
|
7611
8124
|
commandArgs = args.includes("--list-templates")
|
|
7612
8125
|
? { templateList: true, inputPath: null }
|
|
@@ -7823,9 +8336,11 @@ if (commandArgs && Object.prototype.hasOwnProperty.call(commandArgs, "inputPath"
|
|
|
7823
8336
|
}
|
|
7824
8337
|
const emitJson = args.includes("--json");
|
|
7825
8338
|
const strictReleaseStatus = args.includes("--strict");
|
|
8339
|
+
const shouldPushReleaseConsumers = !args.includes("--no-push");
|
|
7826
8340
|
const shouldVersion = Boolean(commandArgs?.version);
|
|
7827
8341
|
const shouldDoctor = Boolean(commandArgs?.doctor);
|
|
7828
8342
|
const shouldReleaseStatus = Boolean(commandArgs?.releaseStatus);
|
|
8343
|
+
const shouldReleaseRollConsumers = Boolean(commandArgs?.releaseRollConsumers);
|
|
7829
8344
|
const shouldCheck = Boolean(commandArgs?.check);
|
|
7830
8345
|
const shouldComponentCheck = Boolean(commandArgs?.componentCheck);
|
|
7831
8346
|
const shouldComponentBehavior = Boolean(commandArgs?.componentBehavior);
|
|
@@ -8059,6 +8574,18 @@ try {
|
|
|
8059
8574
|
process.exit(payload.ok ? 0 : 1);
|
|
8060
8575
|
}
|
|
8061
8576
|
|
|
8577
|
+
if (shouldReleaseRollConsumers) {
|
|
8578
|
+
const payload = buildReleaseRollConsumersPayload(commandArgs.releaseRollVersion, {
|
|
8579
|
+
push: shouldPushReleaseConsumers
|
|
8580
|
+
});
|
|
8581
|
+
if (emitJson) {
|
|
8582
|
+
console.log(stableStringify(payload));
|
|
8583
|
+
} else {
|
|
8584
|
+
printReleaseRollConsumers(payload);
|
|
8585
|
+
}
|
|
8586
|
+
process.exit(payload.ok ? 0 : 1);
|
|
8587
|
+
}
|
|
8588
|
+
|
|
8062
8589
|
if (shouldQueryList) {
|
|
8063
8590
|
const payload = buildQueryListPayload();
|
|
8064
8591
|
if (emitJson) {
|