@topogram/cli 0.3.62 → 0.3.64
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/adoption/plan.d.ts +6 -0
- package/src/adoption/reporting.d.ts +10 -0
- package/src/adoption/review-groups.d.ts +6 -0
- package/src/agent-brief.d.ts +3 -0
- package/src/agent-brief.js +495 -0
- package/src/agent-ops/query-builders.d.ts +26 -0
- package/src/archive/archive.d.ts +2 -0
- package/src/archive/compact.d.ts +1 -0
- package/src/archive/unarchive.d.ts +1 -0
- package/src/catalog.d.ts +10 -0
- package/src/catalog.js +62 -66
- package/src/cli/catalog-alias.d.ts +1 -0
- package/src/cli/command-parser.js +38 -0
- package/src/cli/command-parsers/core.js +102 -0
- package/src/cli/command-parsers/generator.js +39 -0
- package/src/cli/command-parsers/import.js +44 -0
- package/src/cli/command-parsers/legacy-workflow.js +21 -0
- package/src/cli/command-parsers/project.js +47 -0
- package/src/cli/command-parsers/sdlc.js +47 -0
- package/src/cli/command-parsers/shared.js +51 -0
- package/src/cli/command-parsers/template.js +48 -0
- package/src/cli/commands/agent.js +47 -0
- package/src/cli/commands/catalog.js +617 -0
- package/src/cli/commands/check.js +268 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/emit.js +149 -0
- package/src/cli/commands/generate.js +96 -0
- package/src/cli/commands/generator-policy.js +785 -0
- package/src/cli/commands/generator.js +443 -0
- package/src/cli/commands/import-runner.js +157 -0
- package/src/cli/commands/import.js +1734 -0
- package/src/cli/commands/inspect.js +55 -0
- package/src/cli/commands/new.js +94 -0
- package/src/cli/commands/package.js +815 -0
- package/src/cli/commands/query.js +1302 -0
- package/src/cli/commands/release-rollout.js +257 -0
- package/src/cli/commands/release-shared.js +528 -0
- package/src/cli/commands/release-status.js +429 -0
- package/src/cli/commands/release.js +107 -0
- package/src/cli/commands/sdlc.js +168 -0
- package/src/cli/commands/setup.js +76 -0
- package/src/cli/commands/source.js +291 -0
- package/src/cli/commands/template-runner.js +198 -0
- package/src/cli/commands/template.js +2145 -0
- package/src/cli/commands/trust.js +219 -0
- package/src/cli/commands/version.js +40 -0
- package/src/cli/commands/widget.js +168 -0
- package/src/cli/commands/workflow.js +63 -0
- package/src/cli/dispatcher.js +392 -0
- package/src/cli/help-dispatch.js +188 -0
- package/src/cli/help.js +296 -0
- package/src/cli/migration-guidance.js +59 -0
- package/src/cli/options.js +96 -0
- package/src/cli/output-safety.js +107 -0
- package/src/cli/path-normalization.js +29 -0
- package/src/cli.js +47 -11711
- package/src/example-implementation.d.ts +2 -0
- package/src/format.d.ts +1 -0
- package/src/generator/check.d.ts +1 -0
- package/src/generator/context/bundle.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/native/parity-bundle.js +2 -1
- package/src/generator/surfaces/web/html-escape.js +22 -0
- package/src/generator/surfaces/web/react.js +10 -8
- package/src/generator/surfaces/web/sveltekit.js +7 -5
- package/src/generator/surfaces/web/vanilla.js +8 -4
- package/src/generator.d.ts +2 -0
- package/src/github-client.js +520 -0
- package/src/import/core/shared.js +20 -62
- package/src/import/extractors/api/flutter-dio.js +4 -8
- package/src/import/extractors/api/react-native-repository.js +4 -8
- package/src/import/index.d.ts +4 -0
- package/src/import/provenance.d.ts +4 -0
- package/src/new-project.js +100 -11
- package/src/npm-safety.js +79 -0
- package/src/parser.d.ts +1 -0
- package/src/path-helpers.d.ts +1 -0
- package/src/path-helpers.js +20 -0
- package/src/project-config.js +1 -0
- package/src/reconcile/docs.d.ts +8 -0
- package/src/reconcile/journeys.d.ts +1 -0
- package/src/resolver.d.ts +1 -0
- package/src/runtime-support.js +29 -0
- package/src/sdlc/adopt.d.ts +1 -0
- package/src/sdlc/check.d.ts +1 -0
- package/src/sdlc/explain.d.ts +1 -0
- package/src/sdlc/release.d.ts +1 -0
- package/src/sdlc/scaffold.d.ts +1 -0
- package/src/sdlc/transition.d.ts +1 -0
- package/src/text-helpers.d.ts +6 -0
- package/src/text-helpers.js +245 -0
- package/src/topogram-config.js +306 -0
- package/src/validator.d.ts +2 -0
- package/src/workflows/adoption/index.js +26 -0
- package/src/workflows/docs-generate.js +262 -0
- package/src/workflows/docs-scan.js +703 -0
- package/src/workflows/docs.js +15 -0
- package/src/workflows/import-app/api.js +799 -0
- package/src/workflows/import-app/db.js +538 -0
- package/src/workflows/import-app/index.js +30 -0
- package/src/workflows/import-app/shared.js +218 -0
- package/src/workflows/import-app/ui.js +443 -0
- package/src/workflows/import-app/workflow.js +159 -0
- package/src/workflows/reconcile/adoption-plan.js +742 -0
- package/src/workflows/reconcile/auth.js +692 -0
- package/src/workflows/reconcile/bundle-core.js +600 -0
- package/src/workflows/reconcile/bundle-shared.js +75 -0
- package/src/workflows/reconcile/candidate-model.js +477 -0
- package/src/workflows/reconcile/canonical-surface.js +264 -0
- package/src/workflows/reconcile/gap-report.js +333 -0
- package/src/workflows/reconcile/ids.js +6 -0
- package/src/workflows/reconcile/impacts.js +625 -0
- package/src/workflows/reconcile/index.js +7 -0
- package/src/workflows/reconcile/renderers.js +461 -0
- package/src/workflows/reconcile/summary.js +90 -0
- package/src/workflows/reconcile/workflow.js +309 -0
- package/src/workflows/shared.js +189 -0
- package/src/workflows/types.d.ts +93 -0
- package/src/workflows.d.ts +1 -0
- package/src/workflows.js +10 -7652
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import childProcess from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
latestWorkflowRun,
|
|
9
|
+
workflowRunJobs
|
|
10
|
+
} from "../../github-client.js";
|
|
11
|
+
import {
|
|
12
|
+
githubRepoSlug,
|
|
13
|
+
releaseConsumerRepos,
|
|
14
|
+
releaseConsumerWorkflowJobs,
|
|
15
|
+
releaseConsumerWorkflowName
|
|
16
|
+
} from "../../topogram-config.js";
|
|
17
|
+
|
|
18
|
+
const REPO_ROOT = decodeURIComponent(new URL("../../../../", import.meta.url).pathname);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Record<string, any>} AnyRecord
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {unknown} error
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function messageFromError(error) {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} version
|
|
34
|
+
* @param {string} cwd
|
|
35
|
+
* @returns {{ tag: string, local: boolean|null, remote: boolean|null, diagnostics: Array<AnyRecord> }}
|
|
36
|
+
*/
|
|
37
|
+
export function inspectReleaseGitTag(version, cwd) {
|
|
38
|
+
const tag = `topogram-v${version}`;
|
|
39
|
+
const diagnostics = [];
|
|
40
|
+
let local = null;
|
|
41
|
+
let remote = null;
|
|
42
|
+
const localResult = childProcess.spawnSync("git", ["tag", "--list", tag], {
|
|
43
|
+
cwd,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
env: { ...process.env, PATH: process.env.PATH || "" }
|
|
46
|
+
});
|
|
47
|
+
if (localResult.status === 0) {
|
|
48
|
+
local = String(localResult.stdout || "").trim() === tag;
|
|
49
|
+
} else {
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
code: "release_local_tag_unavailable",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `Could not inspect local git tag ${tag}.`,
|
|
54
|
+
path: cwd,
|
|
55
|
+
suggestedFix: "Run from a git checkout with git available."
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const remoteResult = childProcess.spawnSync("git", ["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${tag}`], {
|
|
59
|
+
cwd,
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
env: { ...process.env, PATH: process.env.PATH || "" }
|
|
62
|
+
});
|
|
63
|
+
if (remoteResult.status === 0) {
|
|
64
|
+
remote = true;
|
|
65
|
+
} else if (remoteResult.status === 2) {
|
|
66
|
+
remote = false;
|
|
67
|
+
} else {
|
|
68
|
+
diagnostics.push({
|
|
69
|
+
code: "release_remote_tag_unavailable",
|
|
70
|
+
severity: "warning",
|
|
71
|
+
message: `Could not inspect remote git tag ${tag}.`,
|
|
72
|
+
path: cwd,
|
|
73
|
+
suggestedFix: "Check git remote access, then rerun `topogram release status`."
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { tag, local, remote, diagnostics };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} name
|
|
81
|
+
* @returns {string|null}
|
|
82
|
+
*/
|
|
83
|
+
export function expectedConsumerWorkflowName(name) {
|
|
84
|
+
return releaseConsumerWorkflowName(name);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} name
|
|
89
|
+
* @returns {string[]}
|
|
90
|
+
*/
|
|
91
|
+
function expectedConsumerWorkflowJobs(name) {
|
|
92
|
+
return releaseConsumerWorkflowJobs(name);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {{ name: string }|string} consumer
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function consumerGithubRepoSlug(consumer) {
|
|
100
|
+
const name = typeof consumer === "string" ? consumer : consumer.name;
|
|
101
|
+
return githubRepoSlug(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string[]} args
|
|
106
|
+
* @param {string} cwd
|
|
107
|
+
* @returns {ReturnType<typeof childProcess.spawnSync>}
|
|
108
|
+
*/
|
|
109
|
+
export function runGit(args, cwd) {
|
|
110
|
+
return childProcess.spawnSync("git", args, {
|
|
111
|
+
cwd,
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
env: { ...process.env, PATH: process.env.PATH || "" }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} cwd
|
|
119
|
+
* @returns {{ ok: boolean, dirty: boolean|null, error: string|null }}
|
|
120
|
+
*/
|
|
121
|
+
export function inspectGitWorktreeClean(cwd) {
|
|
122
|
+
const result = runGit(["status", "--porcelain"], cwd);
|
|
123
|
+
if (result.status !== 0) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
dirty: null,
|
|
127
|
+
error: `Could not inspect git status: ${commandOutput(result) || "unknown error"}`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const dirty = String(result.stdout || "").trim().length > 0;
|
|
131
|
+
return {
|
|
132
|
+
ok: !dirty,
|
|
133
|
+
dirty,
|
|
134
|
+
error: dirty ? "Consumer repo has uncommitted changes." : null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} cwd
|
|
140
|
+
* @returns {{ ok: boolean, changed: boolean, result: ReturnType<typeof childProcess.spawnSync> }}
|
|
141
|
+
*/
|
|
142
|
+
export function hasStagedGitChanges(cwd) {
|
|
143
|
+
const result = runGit(["diff", "--cached", "--quiet"], cwd);
|
|
144
|
+
return {
|
|
145
|
+
ok: result.status === 0 || result.status === 1,
|
|
146
|
+
changed: result.status === 1,
|
|
147
|
+
result
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string} cwd
|
|
153
|
+
* @returns {string|null}
|
|
154
|
+
*/
|
|
155
|
+
export function currentGitHead(cwd) {
|
|
156
|
+
const result = runGit(["rev-parse", "HEAD"], cwd);
|
|
157
|
+
return result.status === 0 ? String(result.stdout || "").trim() || null : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string, result: ReturnType<typeof childProcess.spawnSync> }} input
|
|
162
|
+
* @returns {{ code: string, severity: "error"|"warning", message: string, path: string|null, suggestedFix: string }}
|
|
163
|
+
*/
|
|
164
|
+
export function commandDiagnostic(input) {
|
|
165
|
+
const output = commandOutput(input.result);
|
|
166
|
+
return {
|
|
167
|
+
code: input.code,
|
|
168
|
+
severity: input.severity,
|
|
169
|
+
message: output ? `${input.message}\n${output}` : input.message,
|
|
170
|
+
path: input.path,
|
|
171
|
+
suggestedFix: input.suggestedFix
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {ReturnType<typeof childProcess.spawnSync>} result
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function commandOutput(result) {
|
|
180
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {number} ms
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
function sleepSync(ms) {
|
|
188
|
+
if (ms <= 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const buffer = new SharedArrayBuffer(4);
|
|
192
|
+
const view = new Int32Array(buffer);
|
|
193
|
+
Atomics.wait(view, 0, 0, ms);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {string} name
|
|
198
|
+
* @param {number} fallback
|
|
199
|
+
* @returns {number}
|
|
200
|
+
*/
|
|
201
|
+
function positiveIntegerEnv(name, fallback) {
|
|
202
|
+
const value = Number.parseInt(process.env[name] || "", 10);
|
|
203
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
|
|
208
|
+
* @param {{ timeoutMs?: number, intervalMs?: number }} [options]
|
|
209
|
+
* @returns {ReturnType<typeof inspectConsumerCi>}
|
|
210
|
+
*/
|
|
211
|
+
export function waitForConsumerCi(consumer, options = {}) {
|
|
212
|
+
const timeoutMs = options.timeoutMs || positiveIntegerEnv("TOPOGRAM_RELEASE_WATCH_TIMEOUT_MS", 20 * 60 * 1000);
|
|
213
|
+
const intervalMs = options.intervalMs || positiveIntegerEnv("TOPOGRAM_RELEASE_WATCH_INTERVAL_MS", 5000);
|
|
214
|
+
const startedAt = Date.now();
|
|
215
|
+
let latest = inspectConsumerCi(consumer, { strict: false });
|
|
216
|
+
while (true) {
|
|
217
|
+
const currentRun = latest.run &&
|
|
218
|
+
latest.headSha &&
|
|
219
|
+
latest.run?.headSha &&
|
|
220
|
+
latest.run.headSha === latest.headSha;
|
|
221
|
+
if (currentRun && latest.run?.status === "completed") {
|
|
222
|
+
return inspectConsumerCi(consumer, { strict: true });
|
|
223
|
+
}
|
|
224
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
225
|
+
const strictLatest = inspectConsumerCi(consumer, { strict: true });
|
|
226
|
+
strictLatest.diagnostics.push({
|
|
227
|
+
code: "release_consumer_ci_watch_timeout",
|
|
228
|
+
severity: "error",
|
|
229
|
+
message: `${consumer.name} verification workflow did not complete on the current commit before the watch timeout.`,
|
|
230
|
+
path: strictLatest.run?.url || consumerGithubRepoSlug(consumer),
|
|
231
|
+
suggestedFix: "Open the consumer workflow, fix failures if needed, then rerun release status."
|
|
232
|
+
});
|
|
233
|
+
strictLatest.ok = false;
|
|
234
|
+
return strictLatest;
|
|
235
|
+
}
|
|
236
|
+
sleepSync(intervalMs);
|
|
237
|
+
latest = inspectConsumerCi(consumer, { strict: false });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
|
|
243
|
+
* @param {{ strict?: boolean }} [options]
|
|
244
|
+
* @returns {{ checked: boolean, ok: boolean|null, expectedWorkflow: string|null, expectedJobs: string[], headSha: string|null, run: AnyRecord|null, diagnostics: Array<AnyRecord> }}
|
|
245
|
+
*/
|
|
246
|
+
export function inspectConsumerCi(consumer, options = {}) {
|
|
247
|
+
const diagnostics = [];
|
|
248
|
+
const expectedWorkflow = consumer.workflow || expectedConsumerWorkflowName(consumer.name);
|
|
249
|
+
const expectedJobs = expectedConsumerWorkflowJobs(consumer.name);
|
|
250
|
+
const repoSlug = consumerGithubRepoSlug(consumer);
|
|
251
|
+
if (!consumer.root || !fs.existsSync(consumer.root)) {
|
|
252
|
+
return {
|
|
253
|
+
checked: false,
|
|
254
|
+
ok: null,
|
|
255
|
+
expectedWorkflow,
|
|
256
|
+
expectedJobs,
|
|
257
|
+
headSha: null,
|
|
258
|
+
run: null,
|
|
259
|
+
diagnostics: []
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const headSha = currentGitHead(consumer.root);
|
|
263
|
+
if (!headSha) {
|
|
264
|
+
diagnostics.push({
|
|
265
|
+
code: "release_consumer_head_unavailable",
|
|
266
|
+
severity: options.strict ? "error" : "warning",
|
|
267
|
+
message: `Could not inspect local HEAD for ${consumer.name}.`,
|
|
268
|
+
path: consumer.root,
|
|
269
|
+
suggestedFix: "Run from a checked-out consumer git repository."
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (!expectedWorkflow) {
|
|
273
|
+
diagnostics.push({
|
|
274
|
+
code: "release_consumer_workflow_unknown",
|
|
275
|
+
severity: options.strict ? "error" : "warning",
|
|
276
|
+
message: `No expected verification workflow is configured for ${consumer.name}.`,
|
|
277
|
+
path: consumer.name,
|
|
278
|
+
suggestedFix: "Add the consumer repo to topogram.config.json release.workflows or the built-in release workflow defaults."
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
checked: true,
|
|
282
|
+
ok: false,
|
|
283
|
+
expectedWorkflow,
|
|
284
|
+
expectedJobs,
|
|
285
|
+
headSha,
|
|
286
|
+
run: null,
|
|
287
|
+
diagnostics
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/** @type {AnyRecord|null} */
|
|
291
|
+
let run = null;
|
|
292
|
+
try {
|
|
293
|
+
run = latestWorkflowRun({
|
|
294
|
+
repoSlug,
|
|
295
|
+
branch: "main",
|
|
296
|
+
workflowName: expectedWorkflow,
|
|
297
|
+
cwd: consumer.root
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
diagnostics.push({
|
|
301
|
+
code: "release_consumer_ci_unavailable",
|
|
302
|
+
severity: options.strict ? "error" : "warning",
|
|
303
|
+
message: [`Could not inspect ${expectedWorkflow} for ${consumer.name}.`, messageFromError(error)].filter(Boolean).join("\n"),
|
|
304
|
+
path: repoSlug,
|
|
305
|
+
suggestedFix: "Set GITHUB_TOKEN or GH_TOKEN with Actions read access, or run `gh auth login` for local fallback; then rerun release status."
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
checked: true,
|
|
309
|
+
ok: false,
|
|
310
|
+
expectedWorkflow,
|
|
311
|
+
expectedJobs,
|
|
312
|
+
headSha,
|
|
313
|
+
run: null,
|
|
314
|
+
diagnostics
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (!run) {
|
|
318
|
+
diagnostics.push({
|
|
319
|
+
code: "release_consumer_ci_missing",
|
|
320
|
+
severity: options.strict ? "error" : "warning",
|
|
321
|
+
message: `${consumer.name} has no ${expectedWorkflow} run on main.`,
|
|
322
|
+
path: repoSlug,
|
|
323
|
+
suggestedFix: "Push the consumer repo and wait for its verification workflow."
|
|
324
|
+
});
|
|
325
|
+
return {
|
|
326
|
+
checked: true,
|
|
327
|
+
ok: false,
|
|
328
|
+
expectedWorkflow,
|
|
329
|
+
expectedJobs,
|
|
330
|
+
headSha,
|
|
331
|
+
run: null,
|
|
332
|
+
diagnostics
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (headSha && run.headSha && run.headSha !== headSha) {
|
|
336
|
+
diagnostics.push({
|
|
337
|
+
code: "release_consumer_ci_head_mismatch",
|
|
338
|
+
severity: options.strict ? "error" : "warning",
|
|
339
|
+
message: `${consumer.name} latest ${expectedWorkflow} run is for ${run.headSha}, not checked-out HEAD ${headSha}.`,
|
|
340
|
+
path: run.url || repoSlug,
|
|
341
|
+
suggestedFix: "Wait for the verification workflow on the current consumer commit, then rerun release status."
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (run.status !== "completed" || run.conclusion !== "success") {
|
|
345
|
+
diagnostics.push({
|
|
346
|
+
code: "release_consumer_ci_not_successful",
|
|
347
|
+
severity: options.strict ? "error" : "warning",
|
|
348
|
+
message: `${consumer.name} ${expectedWorkflow} is ${run.status || "unknown"}/${run.conclusion || "unknown"}.`,
|
|
349
|
+
path: run.url || repoSlug,
|
|
350
|
+
suggestedFix: "Wait for or fix the consumer verification workflow, then rerun release status."
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (expectedJobs.length > 0 && run.databaseId) {
|
|
354
|
+
const jobResult = inspectConsumerWorkflowJobs(consumer, run.databaseId, expectedJobs, options);
|
|
355
|
+
if (jobResult.jobs) {
|
|
356
|
+
run.jobs = jobResult.jobs;
|
|
357
|
+
}
|
|
358
|
+
diagnostics.push(...jobResult.diagnostics);
|
|
359
|
+
} else if (expectedJobs.length > 0) {
|
|
360
|
+
diagnostics.push({
|
|
361
|
+
code: "release_consumer_ci_jobs_unavailable",
|
|
362
|
+
severity: options.strict ? "error" : "warning",
|
|
363
|
+
message: `${consumer.name} ${expectedWorkflow} run did not include a database id, so expected jobs could not be inspected.`,
|
|
364
|
+
path: run.url || repoSlug,
|
|
365
|
+
suggestedFix: "Rerun release status after GitHub exposes the workflow run id."
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
369
|
+
return {
|
|
370
|
+
checked: true,
|
|
371
|
+
ok: errorCount === 0 &&
|
|
372
|
+
(!options.strict || (run.status === "completed" && run.conclusion === "success" && (!headSha || !run.headSha || run.headSha === headSha))),
|
|
373
|
+
expectedWorkflow,
|
|
374
|
+
expectedJobs,
|
|
375
|
+
headSha,
|
|
376
|
+
run,
|
|
377
|
+
diagnostics
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {{ name: string, root?: string|null }} consumer
|
|
383
|
+
* @param {number|string} runId
|
|
384
|
+
* @param {string[]} expectedJobs
|
|
385
|
+
* @param {{ strict?: boolean }} [options]
|
|
386
|
+
* @returns {{ jobs: Array<AnyRecord>|null, diagnostics: Array<AnyRecord> }}
|
|
387
|
+
*/
|
|
388
|
+
function inspectConsumerWorkflowJobs(consumer, runId, expectedJobs, options = {}) {
|
|
389
|
+
const diagnostics = [];
|
|
390
|
+
const repoSlug = consumerGithubRepoSlug(consumer);
|
|
391
|
+
let jobs = [];
|
|
392
|
+
try {
|
|
393
|
+
jobs = workflowRunJobs({
|
|
394
|
+
repoSlug,
|
|
395
|
+
runId,
|
|
396
|
+
cwd: consumer.root || process.cwd()
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
diagnostics.push({
|
|
400
|
+
code: "release_consumer_ci_jobs_unavailable",
|
|
401
|
+
severity: options.strict ? "error" : "warning",
|
|
402
|
+
message: [`Could not inspect expected jobs for ${consumer.name}.`, messageFromError(error)].filter(Boolean).join("\n"),
|
|
403
|
+
path: repoSlug,
|
|
404
|
+
suggestedFix: "Set GITHUB_TOKEN or GH_TOKEN with Actions read access, or run `gh auth login` for local fallback; then rerun release status."
|
|
405
|
+
});
|
|
406
|
+
return { jobs: null, diagnostics };
|
|
407
|
+
}
|
|
408
|
+
for (const expectedJob of expectedJobs) {
|
|
409
|
+
const job = jobs.find((candidate) => candidate?.name === expectedJob);
|
|
410
|
+
if (!job) {
|
|
411
|
+
diagnostics.push({
|
|
412
|
+
code: "release_consumer_ci_job_missing",
|
|
413
|
+
severity: options.strict ? "error" : "warning",
|
|
414
|
+
message: `${consumer.name} workflow is missing expected job '${expectedJob}'.`,
|
|
415
|
+
path: repoSlug,
|
|
416
|
+
suggestedFix: "Update the consumer workflow or the release-status expected job list, then rerun release status."
|
|
417
|
+
});
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (job.status !== "completed" || job.conclusion !== "success") {
|
|
421
|
+
diagnostics.push({
|
|
422
|
+
code: "release_consumer_ci_job_not_successful",
|
|
423
|
+
severity: options.strict ? "error" : "warning",
|
|
424
|
+
message: `${consumer.name} job '${expectedJob}' is ${job.status || "unknown"}/${job.conclusion || "unknown"}.`,
|
|
425
|
+
path: job.url || repoSlug,
|
|
426
|
+
suggestedFix: "Wait for or fix the expected workflow job, then rerun release status."
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return { jobs, diagnostics };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* @param {string} cwd
|
|
435
|
+
* @returns {Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean }>}
|
|
436
|
+
*/
|
|
437
|
+
export function discoverTopogramCliVersionConsumers(cwd) {
|
|
438
|
+
/** @type {string[]} */
|
|
439
|
+
const roots = [];
|
|
440
|
+
for (const root of [cwd, REPO_ROOT, path.dirname(REPO_ROOT)]) {
|
|
441
|
+
const resolved = path.resolve(root);
|
|
442
|
+
if (!roots.includes(resolved)) {
|
|
443
|
+
roots.push(resolved);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const consumers = [];
|
|
447
|
+
for (const name of releaseConsumerRepos(cwd)) {
|
|
448
|
+
let found = null;
|
|
449
|
+
for (const root of roots) {
|
|
450
|
+
const consumerRoot = path.join(root, name);
|
|
451
|
+
const versionPath = path.join(consumerRoot, "topogram-cli.version");
|
|
452
|
+
if (fs.existsSync(consumerRoot) && !fs.existsSync(versionPath)) {
|
|
453
|
+
found = {
|
|
454
|
+
name,
|
|
455
|
+
root: consumerRoot,
|
|
456
|
+
path: versionPath,
|
|
457
|
+
version: null,
|
|
458
|
+
found: false
|
|
459
|
+
};
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
if (!fs.existsSync(versionPath)) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
found = {
|
|
466
|
+
name,
|
|
467
|
+
root: consumerRoot,
|
|
468
|
+
path: versionPath,
|
|
469
|
+
version: fs.readFileSync(versionPath, "utf8").trim() || null,
|
|
470
|
+
found: true
|
|
471
|
+
};
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
consumers.push(found || {
|
|
475
|
+
name,
|
|
476
|
+
root: null,
|
|
477
|
+
path: path.join(roots[0], name, "topogram-cli.version"),
|
|
478
|
+
version: null,
|
|
479
|
+
found: false
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return consumers;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @param {Array<any>} consumers
|
|
487
|
+
* @returns {{ known: number, pinned: number, matching: number, differing: number, missing: number, allKnownPinned: boolean, matchingNames: string[], differingNames: string[], missingNames: string[] }}
|
|
488
|
+
*/
|
|
489
|
+
export function summarizeConsumerPins(consumers) {
|
|
490
|
+
const matchingNames = consumers.filter((consumer) => consumer.matchesLocal === true).map((consumer) => consumer.name);
|
|
491
|
+
const differingNames = consumers.filter((consumer) => consumer.matchesLocal === false).map((consumer) => consumer.name);
|
|
492
|
+
const missingNames = consumers.filter((consumer) => !consumer.found || !consumer.version).map((consumer) => consumer.name);
|
|
493
|
+
return {
|
|
494
|
+
known: consumers.length,
|
|
495
|
+
pinned: consumers.filter((consumer) => consumer.found && consumer.version).length,
|
|
496
|
+
matching: matchingNames.length,
|
|
497
|
+
differing: differingNames.length,
|
|
498
|
+
missing: missingNames.length,
|
|
499
|
+
allKnownPinned: consumers.length > 0 && differingNames.length === 0 && missingNames.length === 0,
|
|
500
|
+
matchingNames,
|
|
501
|
+
differingNames,
|
|
502
|
+
missingNames
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* @param {Array<any>} consumers
|
|
508
|
+
* @returns {{ checked: number, passing: number, failing: number, unavailable: number, skipped: number, allCheckedAndPassing: boolean, passingNames: string[], failingNames: string[], unavailableNames: string[], skippedNames: string[] }}
|
|
509
|
+
*/
|
|
510
|
+
export function summarizeConsumerCi(consumers) {
|
|
511
|
+
const checked = consumers.filter((consumer) => consumer.ci?.checked);
|
|
512
|
+
const passingNames = checked.filter((consumer) => consumer.ci?.ok === true).map((consumer) => consumer.name);
|
|
513
|
+
const failingNames = checked.filter((consumer) => consumer.ci?.ok === false && consumer.ci?.run).map((consumer) => consumer.name);
|
|
514
|
+
const unavailableNames = checked.filter((consumer) => consumer.ci?.ok === false && !consumer.ci?.run).map((consumer) => consumer.name);
|
|
515
|
+
const skippedNames = consumers.filter((consumer) => !consumer.ci?.checked).map((consumer) => consumer.name);
|
|
516
|
+
return {
|
|
517
|
+
checked: checked.length,
|
|
518
|
+
passing: passingNames.length,
|
|
519
|
+
failing: failingNames.length,
|
|
520
|
+
unavailable: unavailableNames.length,
|
|
521
|
+
skipped: skippedNames.length,
|
|
522
|
+
allCheckedAndPassing: consumers.length > 0 && checked.length === consumers.length && failingNames.length === 0 && unavailableNames.length === 0,
|
|
523
|
+
passingNames,
|
|
524
|
+
failingNames,
|
|
525
|
+
unavailableNames,
|
|
526
|
+
skippedNames
|
|
527
|
+
};
|
|
528
|
+
}
|