@treeseed/cli 0.10.22 → 0.11.0
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/README.md +89 -116
- package/dist/cli/handlers/audit.js +28 -3
- package/dist/cli/handlers/capacity.js +2 -2
- package/dist/cli/handlers/destroy.js +6 -1
- package/dist/cli/handlers/dev.js +16 -2
- package/dist/cli/handlers/doctor.js +13 -3
- package/dist/cli/handlers/hosting.d.ts +2 -0
- package/dist/cli/handlers/hosting.js +257 -0
- package/dist/cli/handlers/operations.d.ts +2 -0
- package/dist/cli/handlers/operations.js +46 -0
- package/dist/cli/handlers/package-image.d.ts +10 -0
- package/dist/cli/handlers/package-image.js +132 -0
- package/dist/cli/handlers/package.d.ts +2 -0
- package/dist/cli/handlers/package.js +14 -0
- package/dist/cli/handlers/projects.js +3 -3
- package/dist/cli/handlers/ready.d.ts +2 -0
- package/dist/cli/handlers/ready.js +84 -0
- package/dist/cli/handlers/reconcile.d.ts +2 -0
- package/dist/cli/handlers/reconcile.js +157 -0
- package/dist/cli/handlers/release.js +12 -3
- package/dist/cli/handlers/save.js +12 -2
- package/dist/cli/handlers/seed.js +2 -2
- package/dist/cli/handlers/stage.js +8 -2
- package/dist/cli/handlers/status.js +30 -1
- package/dist/cli/handlers/tool-wrapper.js +71 -3
- package/dist/cli/handlers/treedx.d.ts +2 -0
- package/dist/cli/handlers/treedx.js +310 -0
- package/dist/cli/handlers/workflow.d.ts +40 -0
- package/dist/cli/handlers/workflow.js +41 -0
- package/dist/cli/operations-registry.js +300 -34
- package/dist/cli/registry.d.ts +6 -0
- package/dist/cli/registry.js +12 -0
- package/dist/cli/runtime.js +17 -2
- package/package.json +3 -3
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyTreeseedHostingGraph,
|
|
3
|
+
compileTreeseedHostingGraph,
|
|
4
|
+
planTreeseedHostingGraph,
|
|
5
|
+
serializeHostingApplyResult,
|
|
6
|
+
serializeHostingPlan,
|
|
7
|
+
serializeHostingUnit
|
|
8
|
+
} from "@treeseed/sdk/hosting";
|
|
9
|
+
import {
|
|
10
|
+
collectTreeseedConfigSeedValues,
|
|
11
|
+
collectTreeseedLiveHostedServiceChecks as collectLiveChecks,
|
|
12
|
+
deleteRailwayProject,
|
|
13
|
+
listRailwayProjects,
|
|
14
|
+
resolveRailwayWorkspaceContext
|
|
15
|
+
} from "@treeseed/sdk/workflow-support";
|
|
16
|
+
import { guidedResult } from "./utils.js";
|
|
17
|
+
import { workflowErrorResult } from "./workflow.js";
|
|
18
|
+
function environmentFor(value) {
|
|
19
|
+
return value === "prod" || value === "production" ? "prod" : value === "staging" ? "staging" : "local";
|
|
20
|
+
}
|
|
21
|
+
function subcommandFor(value) {
|
|
22
|
+
const subcommand = typeof value === "string" && value.trim() ? value.trim() : "status";
|
|
23
|
+
if (!["plan", "apply", "verify", "status", "destroy"].includes(subcommand)) {
|
|
24
|
+
throw new Error(`Unknown hosting subcommand "${subcommand}". Use plan, apply, verify, status, or destroy.`);
|
|
25
|
+
}
|
|
26
|
+
return subcommand;
|
|
27
|
+
}
|
|
28
|
+
function listArg(value) {
|
|
29
|
+
const raw = Array.isArray(value) ? value : value === void 0 || value === null || value === "" ? [] : [value];
|
|
30
|
+
return [...new Set(raw.flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean))];
|
|
31
|
+
}
|
|
32
|
+
function renderUnitLine(entry) {
|
|
33
|
+
return `${entry.unit.id}: ${entry.unit.serviceType} -> ${entry.unit.hostId} (${entry.unit.placement})${entry.plan?.action ? ` ${entry.plan.action}` : ""}${entry.verification ? ` verified=${entry.verification.verified ? "yes" : "no"}` : ""}`;
|
|
34
|
+
}
|
|
35
|
+
const handleHosting = async (invocation, context) => {
|
|
36
|
+
try {
|
|
37
|
+
const subcommand = subcommandFor(invocation.positionals[0]);
|
|
38
|
+
const environment = environmentFor(invocation.args.environment);
|
|
39
|
+
const dryRun = invocation.args.dryRun !== false && invocation.args.execute !== true;
|
|
40
|
+
const appId = typeof invocation.args.app === "string" && invocation.args.app.trim() ? invocation.args.app.trim() : void 0;
|
|
41
|
+
const filter = {
|
|
42
|
+
serviceIds: listArg(invocation.args.service),
|
|
43
|
+
placements: listArg(invocation.args.placement),
|
|
44
|
+
hosts: listArg(invocation.args.host)
|
|
45
|
+
};
|
|
46
|
+
const filterInput = filter.serviceIds.length || filter.placements.length || filter.hosts.length ? { filter } : {};
|
|
47
|
+
if (subcommand === "status") {
|
|
48
|
+
const graph = compileTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, ...filterInput });
|
|
49
|
+
const units = graph.units.map((unit) => serializeHostingUnit(unit));
|
|
50
|
+
return guidedResult({
|
|
51
|
+
command: "hosting status",
|
|
52
|
+
summary: `Hosting graph status for ${environment}`,
|
|
53
|
+
facts: [
|
|
54
|
+
{ label: "Environment", value: environment },
|
|
55
|
+
{ label: "Application", value: appId ?? "(workspace)" },
|
|
56
|
+
{ label: "Units", value: String(units.length) },
|
|
57
|
+
{ label: "Hosts", value: Object.keys(graph.hosts).join(", ") },
|
|
58
|
+
{ label: "Service types", value: Object.keys(graph.serviceTypes).join(", ") }
|
|
59
|
+
],
|
|
60
|
+
sections: [
|
|
61
|
+
{
|
|
62
|
+
title: "Placements",
|
|
63
|
+
lines: graph.placements.map((placement) => `${placement.label}: ${placement.serviceIds.join(", ")} on ${placement.hostIds.join(", ")}`)
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: "Units",
|
|
67
|
+
lines: units.map((unit) => renderUnitLine({ unit }))
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
report: {
|
|
71
|
+
command: "hosting status",
|
|
72
|
+
environment,
|
|
73
|
+
graph: {
|
|
74
|
+
environment,
|
|
75
|
+
appId: appId ?? null,
|
|
76
|
+
applications: graph.applications?.map((app) => ({
|
|
77
|
+
id: app.id,
|
|
78
|
+
relativeRoot: app.relativeRoot,
|
|
79
|
+
roles: app.roles
|
|
80
|
+
})) ?? [],
|
|
81
|
+
placements: graph.placements,
|
|
82
|
+
units,
|
|
83
|
+
warnings: graph.warnings
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (subcommand === "destroy") {
|
|
89
|
+
if (!appId) {
|
|
90
|
+
throw new Error("hosting destroy requires --app so the teardown boundary is explicit.");
|
|
91
|
+
}
|
|
92
|
+
const graph = compileTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, ...filterInput });
|
|
93
|
+
const selectedProjectNames = graph.units.filter((unit) => unit.host.id === "railway").map((unit) => unit.projectGroup?.environments?.[environment]?.projectName).filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
94
|
+
const railwayProjectNames = [...new Set(selectedProjectNames)];
|
|
95
|
+
const seedEnv = {
|
|
96
|
+
...context.env,
|
|
97
|
+
...collectTreeseedConfigSeedValues(context.cwd, environment, context.env)
|
|
98
|
+
};
|
|
99
|
+
const projects = railwayProjectNames.length === 0 ? [] : await listRailwayProjects({
|
|
100
|
+
workspaceId: (await resolveRailwayWorkspaceContext({ env: seedEnv })).id,
|
|
101
|
+
env: seedEnv
|
|
102
|
+
});
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const projectName of railwayProjectNames) {
|
|
105
|
+
const matchingProjects = projects.filter((entry) => entry.name === projectName || entry.id === projectName);
|
|
106
|
+
if (matchingProjects.length === 0) {
|
|
107
|
+
results.push({
|
|
108
|
+
projectName,
|
|
109
|
+
projectId: null,
|
|
110
|
+
action: dryRun ? "destroy" : "noop",
|
|
111
|
+
result: { status: dryRun ? "planned" : "missing", projectName }
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const project of matchingProjects) {
|
|
116
|
+
const result2 = dryRun ? { status: "planned", projectName, projectId: project.id } : await deleteRailwayProject({ projectId: project.id, env: seedEnv });
|
|
117
|
+
results.push({
|
|
118
|
+
projectName,
|
|
119
|
+
projectId: project.id,
|
|
120
|
+
action: dryRun ? "destroy" : result2.status === "missing" ? "noop" : "destroy",
|
|
121
|
+
result: result2
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return guidedResult({
|
|
126
|
+
command: "hosting destroy",
|
|
127
|
+
summary: `${dryRun ? "Planned" : "Destroyed"} hosting resources for ${appId} in ${environment}`,
|
|
128
|
+
facts: [
|
|
129
|
+
{ label: "Environment", value: environment },
|
|
130
|
+
{ label: "Application", value: appId },
|
|
131
|
+
{ label: "Dry run", value: dryRun ? "yes" : "no" },
|
|
132
|
+
{ label: "Railway projects", value: railwayProjectNames.length ? railwayProjectNames.join(", ") : "(none)" }
|
|
133
|
+
],
|
|
134
|
+
sections: [
|
|
135
|
+
{
|
|
136
|
+
title: "Railway Projects",
|
|
137
|
+
lines: results.length ? results.map((entry) => `${entry.projectName}: ${entry.action}`) : ["No Railway project groups selected."]
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
report: {
|
|
141
|
+
command: "hosting destroy",
|
|
142
|
+
environment,
|
|
143
|
+
appId,
|
|
144
|
+
dryRun,
|
|
145
|
+
projects: results
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (subcommand === "plan" || subcommand === "verify") {
|
|
150
|
+
const plan = await planTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, dryRun: true, ...filterInput });
|
|
151
|
+
const report2 = serializeHostingPlan(plan);
|
|
152
|
+
const liveHostedServices2 = subcommand === "verify" && invocation.args.live === true && environment !== "local" ? await collectLiveChecks({
|
|
153
|
+
tenantRoot: context.cwd,
|
|
154
|
+
target: environment,
|
|
155
|
+
appId,
|
|
156
|
+
strict: true,
|
|
157
|
+
requireLiveRailway: !appId || appId === "api",
|
|
158
|
+
requireLiveHttp: true,
|
|
159
|
+
env: {
|
|
160
|
+
...context.env,
|
|
161
|
+
...collectTreeseedConfigSeedValues(context.cwd, environment, context.env)
|
|
162
|
+
}
|
|
163
|
+
}) : null;
|
|
164
|
+
const liveFailures2 = liveHostedServices2 ? [
|
|
165
|
+
...liveHostedServices2.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
|
|
166
|
+
...liveHostedServices2.liveObservation.issues
|
|
167
|
+
] : [];
|
|
168
|
+
return guidedResult({
|
|
169
|
+
command: `hosting ${subcommand}`,
|
|
170
|
+
summary: `${subcommand === "verify" ? "Verified" : "Planned"} hosting graph for ${environment}`,
|
|
171
|
+
facts: [
|
|
172
|
+
{ label: "Environment", value: environment },
|
|
173
|
+
{ label: "Application", value: appId ?? "(workspace)" },
|
|
174
|
+
{ label: "Dry run", value: "yes" },
|
|
175
|
+
{ label: "Units", value: String(report2.units.length) },
|
|
176
|
+
{ label: "Verified units", value: String(report2.units.filter((entry) => entry.verification.verified).length) }
|
|
177
|
+
],
|
|
178
|
+
sections: [
|
|
179
|
+
{
|
|
180
|
+
title: "Placements",
|
|
181
|
+
lines: report2.placements.map((placement) => `${placement.label}: ${placement.serviceIds.join(", ")} on ${placement.hostIds.join(", ")}`)
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
title: "Plan",
|
|
185
|
+
lines: report2.units.map((entry) => renderUnitLine(entry))
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
report: liveHostedServices2 ? { ...report2, hostedServices: liveHostedServices2 } : report2,
|
|
189
|
+
exitCode: liveFailures2.length > 0 ? 1 : 0,
|
|
190
|
+
stderr: liveFailures2
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const result = await applyTreeseedHostingGraph({ tenantRoot: context.cwd, environment, appId, dryRun, ...filterInput });
|
|
194
|
+
const report = serializeHostingApplyResult(result);
|
|
195
|
+
const liveHostedServices = !dryRun && environment !== "local" ? await collectLiveChecks({
|
|
196
|
+
tenantRoot: context.cwd,
|
|
197
|
+
target: environment,
|
|
198
|
+
appId,
|
|
199
|
+
strict: true,
|
|
200
|
+
requireLiveRailway: !appId || appId === "api",
|
|
201
|
+
requireLiveHttp: true,
|
|
202
|
+
env: {
|
|
203
|
+
...context.env,
|
|
204
|
+
...collectTreeseedConfigSeedValues(context.cwd, environment, context.env)
|
|
205
|
+
}
|
|
206
|
+
}) : null;
|
|
207
|
+
const liveFailures = liveHostedServices ? [
|
|
208
|
+
...liveHostedServices.checks.filter((check) => check.status === "failed").map((check) => `${check.id}: ${check.issues.join("; ") || "failed"}`),
|
|
209
|
+
...liveHostedServices.liveObservation.issues
|
|
210
|
+
] : [];
|
|
211
|
+
const finalReport = liveHostedServices ? {
|
|
212
|
+
...report,
|
|
213
|
+
liveVerification: {
|
|
214
|
+
ok: liveFailures.length === 0,
|
|
215
|
+
source: "live-hosted-service-checks",
|
|
216
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
217
|
+
issues: liveFailures,
|
|
218
|
+
checks: liveHostedServices.checks
|
|
219
|
+
},
|
|
220
|
+
hostedServices: liveHostedServices,
|
|
221
|
+
ok: report.ok === true && liveFailures.length === 0
|
|
222
|
+
} : report;
|
|
223
|
+
return guidedResult({
|
|
224
|
+
command: "hosting apply",
|
|
225
|
+
summary: `${dryRun ? "Dry-run applied" : "Applied"} hosting graph for ${environment}`,
|
|
226
|
+
facts: [
|
|
227
|
+
{ label: "Environment", value: environment },
|
|
228
|
+
{ label: "Application", value: appId ?? "(workspace)" },
|
|
229
|
+
{ label: "Dry run", value: dryRun ? "yes" : "no" },
|
|
230
|
+
{ label: "Units", value: String(finalReport.results.length) },
|
|
231
|
+
{ label: "Verified units", value: String(finalReport.results.filter((entry) => entry.verification.verified).length) },
|
|
232
|
+
{ label: "Selected systems", value: finalReport.selectedSystems?.join(", ") || "(none)" },
|
|
233
|
+
{ label: "Railway reconcile", value: finalReport.transport?.railway?.reconcile ?? "(not selected)" },
|
|
234
|
+
{ label: "Railway deploy", value: finalReport.transport?.railway?.deploy ?? "(not selected)" },
|
|
235
|
+
{ label: "Live verification", value: finalReport.liveVerification?.ok === false ? "failed" : "passed" }
|
|
236
|
+
],
|
|
237
|
+
sections: [
|
|
238
|
+
{
|
|
239
|
+
title: "Results",
|
|
240
|
+
lines: finalReport.results.map((entry) => renderUnitLine({
|
|
241
|
+
unit: entry.unit,
|
|
242
|
+
plan: entry.plan,
|
|
243
|
+
verification: entry.verification
|
|
244
|
+
}))
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
report: finalReport,
|
|
248
|
+
exitCode: !dryRun && (liveFailures.length > 0 || finalReport.ok === false) ? 1 : 0,
|
|
249
|
+
stderr: liveFailures
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return workflowErrorResult(error);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
export {
|
|
256
|
+
handleHosting
|
|
257
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { runTreeseedOperationsRunnerSmoke } from "@treeseed/sdk/workflow-support";
|
|
2
|
+
import { guidedResult } from "./utils.js";
|
|
3
|
+
import { workflowErrorResult } from "./workflow.js";
|
|
4
|
+
function environmentFor(value) {
|
|
5
|
+
const raw = typeof value === "string" && value.trim() ? value.trim() : "staging";
|
|
6
|
+
return raw === "prod" || raw === "production" ? "prod" : "staging";
|
|
7
|
+
}
|
|
8
|
+
const handleOperations = async (invocation, context) => {
|
|
9
|
+
try {
|
|
10
|
+
const action = typeof invocation.positionals[0] === "string" ? invocation.positionals[0] : "smoke";
|
|
11
|
+
if (action !== "smoke") {
|
|
12
|
+
throw new Error(`Unsupported operations action "${action}". Use smoke.`);
|
|
13
|
+
}
|
|
14
|
+
const service = typeof invocation.args.service === "string" ? invocation.args.service : "operationsRunner";
|
|
15
|
+
if (service !== "operationsRunner") {
|
|
16
|
+
throw new Error(`Unsupported operations smoke service "${service}". Use operationsRunner.`);
|
|
17
|
+
}
|
|
18
|
+
const environment = environmentFor(invocation.args.environment);
|
|
19
|
+
const report = await runTreeseedOperationsRunnerSmoke({
|
|
20
|
+
tenantRoot: context.cwd,
|
|
21
|
+
environment,
|
|
22
|
+
env: context.env
|
|
23
|
+
});
|
|
24
|
+
return guidedResult({
|
|
25
|
+
command: "operations smoke",
|
|
26
|
+
summary: report.ok ? "Treeseed operations runner smoke passed." : "Treeseed operations runner smoke failed.",
|
|
27
|
+
facts: [
|
|
28
|
+
{ label: "Environment", value: environment },
|
|
29
|
+
{ label: "Base URL", value: report.baseUrl },
|
|
30
|
+
{ label: "Operation", value: report.operationId ?? "(none)" },
|
|
31
|
+
{ label: "Final status", value: report.finalStatus ?? "(none)" },
|
|
32
|
+
{ label: "Runner", value: report.runnerId ?? "(none)" }
|
|
33
|
+
],
|
|
34
|
+
nextSteps: report.ok ? [] : [
|
|
35
|
+
`npx trsd hosting verify --environment ${environment} --service operationsRunner --live --json`
|
|
36
|
+
],
|
|
37
|
+
report,
|
|
38
|
+
exitCode: report.ok ? 0 : 1
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return workflowErrorResult(error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export {
|
|
45
|
+
handleOperations
|
|
46
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TreeseedCommandHandler, TreeseedParsedInvocation } from '../types.js';
|
|
2
|
+
export declare function runPackageImageCommand(invocation: TreeseedParsedInvocation, context: Parameters<TreeseedCommandHandler>[1], options?: {
|
|
3
|
+
packageId?: string | null;
|
|
4
|
+
commandName?: string;
|
|
5
|
+
}): Promise<import("../operations-types.js").TreeseedCommandResult | {
|
|
6
|
+
exitCode: number;
|
|
7
|
+
stdout: string[];
|
|
8
|
+
stderr: never[];
|
|
9
|
+
report: Record<string, unknown>;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGitHubApiClient,
|
|
3
|
+
dispatchGitHubWorkflowRun,
|
|
4
|
+
ensureGitHubActionsEnvironment,
|
|
5
|
+
getLatestGitHubWorkflowRun,
|
|
6
|
+
listGitHubEnvironmentSecretNames,
|
|
7
|
+
listGitHubEnvironmentVariableNames,
|
|
8
|
+
planTreeseedPackageDevelopmentImage,
|
|
9
|
+
resolveGitHubCredentialForRepository,
|
|
10
|
+
resolveTreeseedLaunchEnvironment,
|
|
11
|
+
upsertGitHubEnvironmentSecret,
|
|
12
|
+
upsertGitHubEnvironmentVariable
|
|
13
|
+
} from "@treeseed/sdk/workflow-support";
|
|
14
|
+
import { fail, guidedResult } from "./utils.js";
|
|
15
|
+
function stringArg(invocation, key) {
|
|
16
|
+
const value = invocation.args[key];
|
|
17
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
18
|
+
}
|
|
19
|
+
function boolArg(invocation, key) {
|
|
20
|
+
return invocation.args[key] === true;
|
|
21
|
+
}
|
|
22
|
+
function jsonResult(invocation, context, report) {
|
|
23
|
+
if (context.outputFormat === "json" || boolArg(invocation, "json")) {
|
|
24
|
+
return { exitCode: 0, stdout: [JSON.stringify(report, null, 2)], stderr: [], report };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
async function runPackageImageCommand(invocation, context, options = {}) {
|
|
29
|
+
const packageId = options.packageId ?? stringArg(invocation, "package") ?? stringArg(invocation, "packageId");
|
|
30
|
+
if (!packageId) {
|
|
31
|
+
return fail("Missing --package. Use `trsd package image --package <package-id>`.");
|
|
32
|
+
}
|
|
33
|
+
const branch = stringArg(invocation, "branch") ?? "staging";
|
|
34
|
+
const workflow = stringArg(invocation, "workflow") ?? null;
|
|
35
|
+
const execute = boolArg(invocation, "execute");
|
|
36
|
+
const syncConfig = boolArg(invocation, "syncConfig");
|
|
37
|
+
const planOnly = boolArg(invocation, "plan") || !execute;
|
|
38
|
+
const imagePlan = planTreeseedPackageDevelopmentImage(context.cwd, packageId, { branch });
|
|
39
|
+
const selectedWorkflow = workflow ?? imagePlan.workflow;
|
|
40
|
+
const configEnv = resolveTreeseedLaunchEnvironment({
|
|
41
|
+
tenantRoot: context.cwd,
|
|
42
|
+
scope: "staging",
|
|
43
|
+
baseEnv: context.env
|
|
44
|
+
});
|
|
45
|
+
const dockerHub = {
|
|
46
|
+
usernameConfigured: Boolean(String(configEnv.DOCKERHUB_USERNAME ?? "").trim()),
|
|
47
|
+
tokenConfigured: Boolean(String(configEnv.DOCKERHUB_TOKEN ?? "").trim()),
|
|
48
|
+
requiredSecrets: ["DOCKERHUB_TOKEN"],
|
|
49
|
+
requiredVariables: ["DOCKERHUB_USERNAME"]
|
|
50
|
+
};
|
|
51
|
+
const credential = resolveGitHubCredentialForRepository(imagePlan.repository, { values: configEnv, env: context.env });
|
|
52
|
+
const githubClientEnv = credential.token ? { ...configEnv, GH_TOKEN: credential.token, GITHUB_TOKEN: credential.token } : configEnv;
|
|
53
|
+
const report = {
|
|
54
|
+
ok: true,
|
|
55
|
+
action: planOnly ? "plan" : "dispatch",
|
|
56
|
+
package: imagePlan.package,
|
|
57
|
+
repository: imagePlan.repository,
|
|
58
|
+
workflow: selectedWorkflow,
|
|
59
|
+
branch: imagePlan.branch,
|
|
60
|
+
refs: imagePlan.refs,
|
|
61
|
+
credential: {
|
|
62
|
+
repository: credential.repository,
|
|
63
|
+
envName: credential.envName,
|
|
64
|
+
configured: credential.configured,
|
|
65
|
+
source: credential.source,
|
|
66
|
+
fallbackUsed: credential.fallbackUsed
|
|
67
|
+
},
|
|
68
|
+
dockerHub,
|
|
69
|
+
hosting: imagePlan.hosting
|
|
70
|
+
};
|
|
71
|
+
if (syncConfig) {
|
|
72
|
+
const client = createGitHubApiClient({ env: githubClientEnv });
|
|
73
|
+
const environment = imagePlan.hosting?.environment ?? "staging";
|
|
74
|
+
await ensureGitHubActionsEnvironment(imagePlan.repository, environment, {
|
|
75
|
+
client,
|
|
76
|
+
branchName: imagePlan.branch
|
|
77
|
+
});
|
|
78
|
+
const [secretNames, variableNames] = await Promise.all([
|
|
79
|
+
listGitHubEnvironmentSecretNames(imagePlan.repository, environment, { client }),
|
|
80
|
+
listGitHubEnvironmentVariableNames(imagePlan.repository, environment, { client })
|
|
81
|
+
]);
|
|
82
|
+
const synced = {
|
|
83
|
+
environment,
|
|
84
|
+
secrets: [],
|
|
85
|
+
variables: []
|
|
86
|
+
};
|
|
87
|
+
if (configEnv.DOCKERHUB_TOKEN) {
|
|
88
|
+
await upsertGitHubEnvironmentSecret(imagePlan.repository, environment, "DOCKERHUB_TOKEN", configEnv.DOCKERHUB_TOKEN, { client });
|
|
89
|
+
synced.secrets.push({ name: "DOCKERHUB_TOKEN", existed: secretNames.has("DOCKERHUB_TOKEN") });
|
|
90
|
+
}
|
|
91
|
+
if (configEnv.DOCKERHUB_USERNAME) {
|
|
92
|
+
await upsertGitHubEnvironmentVariable(imagePlan.repository, environment, "DOCKERHUB_USERNAME", configEnv.DOCKERHUB_USERNAME, { client });
|
|
93
|
+
synced.variables.push({ name: "DOCKERHUB_USERNAME", existed: variableNames.has("DOCKERHUB_USERNAME") });
|
|
94
|
+
}
|
|
95
|
+
report.syncedConfig = synced;
|
|
96
|
+
}
|
|
97
|
+
if (execute) {
|
|
98
|
+
const client = createGitHubApiClient({ env: githubClientEnv });
|
|
99
|
+
report.dispatch = await dispatchGitHubWorkflowRun(imagePlan.repository, {
|
|
100
|
+
client,
|
|
101
|
+
workflow: selectedWorkflow,
|
|
102
|
+
branch: imagePlan.branch
|
|
103
|
+
});
|
|
104
|
+
report.latestWorkflowRun = await getLatestGitHubWorkflowRun(imagePlan.repository, {
|
|
105
|
+
client,
|
|
106
|
+
workflow: selectedWorkflow,
|
|
107
|
+
branch: imagePlan.branch
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const json = jsonResult(invocation, context, report);
|
|
111
|
+
if (json) return json;
|
|
112
|
+
return guidedResult({
|
|
113
|
+
command: options.commandName ?? "package image",
|
|
114
|
+
summary: execute ? "Package development image workflow dispatched." : "Package development image plan ready.",
|
|
115
|
+
facts: [
|
|
116
|
+
{ label: "Package", value: `${imagePlan.package.id} (${imagePlan.package.path})` },
|
|
117
|
+
{ label: "Workflow", value: `${selectedWorkflow} @ ${imagePlan.branch}` },
|
|
118
|
+
{ label: "Image", value: imagePlan.refs.imageRef },
|
|
119
|
+
{ label: "Moving tag", value: imagePlan.refs.movingImageRef ?? "disabled" },
|
|
120
|
+
{ label: "GitHub credential", value: credential.configured ? `${credential.envName}${credential.fallbackUsed ? " (fallback)" : ""}` : `${credential.envName} missing` },
|
|
121
|
+
{ label: "Docker Hub config", value: dockerHub.usernameConfigured && dockerHub.tokenConfigured ? "configured" : "missing" },
|
|
122
|
+
{ label: "Hosting override", value: imagePlan.hosting ? `${imagePlan.hosting.overrideEnvVar}=${imagePlan.refs.imageRef}` : "not declared" }
|
|
123
|
+
],
|
|
124
|
+
sections: [
|
|
125
|
+
{ title: "Next", lines: imagePlan.hosting ? [imagePlan.hosting.command] : [] }
|
|
126
|
+
],
|
|
127
|
+
report
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
runPackageImageCommand
|
|
132
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runPackageImageCommand } from "./package-image.js";
|
|
2
|
+
import { fail } from "./utils.js";
|
|
3
|
+
const handlePackage = async (invocation, context) => {
|
|
4
|
+
const action = invocation.positionals[0] ?? "status";
|
|
5
|
+
try {
|
|
6
|
+
if (action === "image") return runPackageImageCommand(invocation, context, { commandName: "package image" });
|
|
7
|
+
return fail("Unknown package action. Use image.");
|
|
8
|
+
} catch (error) {
|
|
9
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export {
|
|
13
|
+
handlePackage
|
|
14
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MarketClientError } from "@treeseed/sdk/market-client";
|
|
2
2
|
import { parseProjectLaunchHostBindingSpecs } from "@treeseed/sdk";
|
|
3
3
|
import { fail, guidedResult } from "./utils.js";
|
|
4
4
|
import { createMarketClientForInvocation } from "./market-utils.js";
|
|
@@ -47,7 +47,7 @@ function projectUsage(action) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
function authFailure(error) {
|
|
50
|
-
if (error instanceof
|
|
50
|
+
if (error instanceof MarketClientError && [401, 403].includes(error.status)) {
|
|
51
51
|
return fail(error.message, 2);
|
|
52
52
|
}
|
|
53
53
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -57,7 +57,7 @@ function authFailure(error) {
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
function deploymentApiExitCode(error) {
|
|
60
|
-
if (error instanceof
|
|
60
|
+
if (error instanceof MarketClientError) {
|
|
61
61
|
if ([401, 403].includes(error.status)) return 2;
|
|
62
62
|
const payload = error.payload;
|
|
63
63
|
const code = payload?.error?.code ?? payload?.code;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectTreeseedDeploymentReadiness,
|
|
3
|
+
collectTreeseedLiveHostedServiceChecks,
|
|
4
|
+
collectTreeseedHostedServiceChecks,
|
|
5
|
+
formatTreeseedReadinessReport,
|
|
6
|
+
runTreeseedOperationsRunnerSmoke
|
|
7
|
+
} from "@treeseed/sdk/workflow-support";
|
|
8
|
+
import { guidedResult } from "./utils.js";
|
|
9
|
+
import { createWorkflowSdk, workflowErrorResult } from "./workflow.js";
|
|
10
|
+
function environmentFor(value) {
|
|
11
|
+
const raw = typeof value === "string" && value.trim() ? value.trim() : "staging";
|
|
12
|
+
if (raw === "prod" || raw === "production") return "prod";
|
|
13
|
+
if (raw === "local") return "local";
|
|
14
|
+
return "staging";
|
|
15
|
+
}
|
|
16
|
+
function failedCount(section) {
|
|
17
|
+
return Number(section?.summary?.failed ?? 0);
|
|
18
|
+
}
|
|
19
|
+
const handleReady = async (invocation, context) => {
|
|
20
|
+
try {
|
|
21
|
+
const environment = environmentFor(invocation.positionals[0] ?? invocation.args.environment);
|
|
22
|
+
const live = invocation.args.live === true || environment !== "local";
|
|
23
|
+
const strict = invocation.args.strict === true || environment !== "local";
|
|
24
|
+
const status = await createWorkflowSdk(context).status();
|
|
25
|
+
const readiness = collectTreeseedDeploymentReadiness({ tenantRoot: context.cwd, environment });
|
|
26
|
+
const hostedServices = live ? await collectTreeseedLiveHostedServiceChecks({
|
|
27
|
+
tenantRoot: context.cwd,
|
|
28
|
+
target: environment,
|
|
29
|
+
strict,
|
|
30
|
+
requireLiveRailway: strict,
|
|
31
|
+
requireLiveHttp: strict,
|
|
32
|
+
env: context.env
|
|
33
|
+
}) : collectTreeseedHostedServiceChecks({ tenantRoot: context.cwd, target: environment });
|
|
34
|
+
const runnerSmoke = live && environment !== "local" && readiness.ok ? await runTreeseedOperationsRunnerSmoke({
|
|
35
|
+
tenantRoot: context.cwd,
|
|
36
|
+
environment,
|
|
37
|
+
env: context.env
|
|
38
|
+
}) : null;
|
|
39
|
+
const ok = readiness.ok && failedCount(hostedServices) === 0 && (runnerSmoke ? runnerSmoke.ok : true);
|
|
40
|
+
const nextActions = [
|
|
41
|
+
...readiness.checks.filter((check) => check.status === "failed" && check.remediation).map((check) => check.remediation),
|
|
42
|
+
...runnerSmoke && !runnerSmoke.ok ? [
|
|
43
|
+
`npx trsd operations smoke --environment ${environment} --service operationsRunner --json`,
|
|
44
|
+
`npx trsd hosting verify --environment ${environment} --service operationsRunner --live --json`
|
|
45
|
+
] : []
|
|
46
|
+
];
|
|
47
|
+
const report = {
|
|
48
|
+
ok,
|
|
49
|
+
environment,
|
|
50
|
+
live,
|
|
51
|
+
strict,
|
|
52
|
+
workflow: status.payload,
|
|
53
|
+
deploymentReadiness: readiness,
|
|
54
|
+
hostedServices,
|
|
55
|
+
runnerSmoke,
|
|
56
|
+
nextActions
|
|
57
|
+
};
|
|
58
|
+
return guidedResult({
|
|
59
|
+
command: "ready",
|
|
60
|
+
summary: ok ? `Treeseed ${environment} readiness passed.` : `Treeseed ${environment} readiness failed.`,
|
|
61
|
+
facts: [
|
|
62
|
+
{ label: "Environment", value: environment },
|
|
63
|
+
{ label: "Live", value: live ? "yes" : "no" },
|
|
64
|
+
{ label: "Readiness failed", value: readiness.summary.failed },
|
|
65
|
+
{ label: "Hosted checks failed", value: hostedServices.summary.failed },
|
|
66
|
+
{ label: "Runner smoke", value: runnerSmoke ? runnerSmoke.ok ? "passed" : "failed" : "skipped" }
|
|
67
|
+
],
|
|
68
|
+
sections: [
|
|
69
|
+
{
|
|
70
|
+
title: "Deployment readiness",
|
|
71
|
+
lines: formatTreeseedReadinessReport(readiness).split("\n")
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
nextSteps: nextActions,
|
|
75
|
+
report,
|
|
76
|
+
exitCode: ok ? 0 : 1
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return workflowErrorResult(error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export {
|
|
83
|
+
handleReady
|
|
84
|
+
};
|