@treeseed/sdk 0.6.16 → 0.6.18
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/dist/db/d1.d.ts +3493 -0
- package/dist/db/d1.js +8 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +2 -0
- package/dist/db/node-sqlite.d.ts +3544 -0
- package/dist/db/node-sqlite.js +119 -0
- package/dist/db/schema.d.ts +6272 -0
- package/dist/db/schema.js +231 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/operations/providers/default.js +1 -0
- package/dist/operations/services/commit-message-provider.d.ts +33 -1
- package/dist/operations/services/commit-message-provider.js +228 -51
- package/dist/operations/services/config-runtime.js +0 -1
- package/dist/operations/services/deploy.d.ts +19 -5
- package/dist/operations/services/deploy.js +75 -36
- package/dist/operations/services/github-actions-verification.d.ts +123 -0
- package/dist/operations/services/github-actions-verification.js +440 -0
- package/dist/operations/services/mailpit-runtime.d.ts +5 -0
- package/dist/operations/services/mailpit-runtime.js +2 -2
- package/dist/operations/services/repository-save-orchestrator.js +64 -8
- package/dist/operations/services/runtime-tools.d.ts +6 -0
- package/dist/operations/services/runtime-tools.js +11 -0
- package/dist/operations-registry.js +1 -0
- package/dist/platform/contracts.d.ts +6 -0
- package/dist/platform/deploy-config.js +17 -0
- package/dist/reconcile/builtin-adapters.js +2 -16
- package/dist/reconcile/contracts.d.ts +1 -1
- package/dist/reconcile/desired-state.d.ts +6 -0
- package/dist/reconcile/desired-state.js +1 -13
- package/dist/reconcile/engine.d.ts +12 -0
- package/dist/reconcile/state.js +2 -1
- package/dist/reconcile/units.js +0 -1
- package/dist/scripts/tenant-d1-migrate-local.js +5 -2
- package/dist/scripts/tenant-destroy.js +3 -1
- package/dist/sdk.js +2 -6
- package/dist/types/cloudflare.d.ts +0 -1
- package/dist/workflow/operations.d.ts +2 -1
- package/dist/workflow/operations.js +115 -35
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +6 -0
- package/dist/workflow.d.ts +24 -2
- package/dist/workflow.js +6 -0
- package/package.json +19 -5
- package/templates/github/deploy.workflow.yml +4 -0
- package/dist/wrangler-d1.d.ts +0 -25
- package/dist/wrangler-d1.js +0 -89
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import {
|
|
3
|
+
createGitHubApiClient,
|
|
4
|
+
parseGitHubRepositorySlug
|
|
5
|
+
} from "./github-api.js";
|
|
6
|
+
function normalizeWorkflowRun(run) {
|
|
7
|
+
return {
|
|
8
|
+
id: Number(run.id ?? 0),
|
|
9
|
+
status: typeof run.status === "string" ? run.status : null,
|
|
10
|
+
conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
|
|
11
|
+
url: typeof run.html_url === "string" ? run.html_url : null,
|
|
12
|
+
headSha: typeof run.head_sha === "string" ? run.head_sha : null,
|
|
13
|
+
headBranch: typeof run.head_branch === "string" ? run.head_branch : null,
|
|
14
|
+
createdAt: typeof run.created_at === "string" ? run.created_at : null,
|
|
15
|
+
updatedAt: typeof run.updated_at === "string" ? run.updated_at : null
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function normalizeWorkflowJobStep(step) {
|
|
19
|
+
return {
|
|
20
|
+
name: String(step.name ?? ""),
|
|
21
|
+
number: typeof step.number === "number" ? step.number : null,
|
|
22
|
+
status: typeof step.status === "string" ? step.status : null,
|
|
23
|
+
conclusion: typeof step.conclusion === "string" ? step.conclusion : null,
|
|
24
|
+
startedAt: typeof step.started_at === "string" ? step.started_at : null,
|
|
25
|
+
completedAt: typeof step.completed_at === "string" ? step.completed_at : null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function isSuccessfulConclusion(conclusion) {
|
|
29
|
+
return conclusion === "success" || conclusion === "skipped" || conclusion === "neutral";
|
|
30
|
+
}
|
|
31
|
+
function isFailedConclusion(conclusion) {
|
|
32
|
+
return Boolean(conclusion && !isSuccessfulConclusion(conclusion));
|
|
33
|
+
}
|
|
34
|
+
function normalizeWorkflowJob(job) {
|
|
35
|
+
const steps = Array.isArray(job.steps) ? job.steps.map((step) => normalizeWorkflowJobStep(step)) : [];
|
|
36
|
+
return {
|
|
37
|
+
id: Number(job.id ?? 0),
|
|
38
|
+
name: String(job.name ?? ""),
|
|
39
|
+
status: typeof job.status === "string" ? job.status : null,
|
|
40
|
+
conclusion: typeof job.conclusion === "string" ? job.conclusion : null,
|
|
41
|
+
url: typeof job.html_url === "string" ? job.html_url : null,
|
|
42
|
+
steps,
|
|
43
|
+
failedSteps: steps.filter((step) => isFailedConclusion(step.conclusion))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function workflowStateForRun(run) {
|
|
47
|
+
if (!run) return "missing";
|
|
48
|
+
if (run.status !== "completed") return "pending";
|
|
49
|
+
return isSuccessfulConclusion(run.conclusion) ? "success" : "failure";
|
|
50
|
+
}
|
|
51
|
+
function aggregateWorkflowState(states) {
|
|
52
|
+
if (states.includes("error")) return "error";
|
|
53
|
+
if (states.includes("not_pushed")) return "not_pushed";
|
|
54
|
+
if (states.includes("failure")) return "failure";
|
|
55
|
+
if (states.includes("missing")) return "missing";
|
|
56
|
+
if (states.includes("pending")) return "pending";
|
|
57
|
+
return "success";
|
|
58
|
+
}
|
|
59
|
+
function cappedLogExcerpt(value, maxLines) {
|
|
60
|
+
const text = typeof value === "string" ? value : value instanceof Uint8Array ? Buffer.from(value).toString("utf8") : Buffer.isBuffer(value) ? value.toString("utf8") : String(value ?? "");
|
|
61
|
+
const lines = text.split(/\r?\n/u);
|
|
62
|
+
return lines.slice(Math.max(0, lines.length - maxLines)).join("\n").trim();
|
|
63
|
+
}
|
|
64
|
+
async function downloadJobLogExcerpt(client, repository, jobId, maxLines) {
|
|
65
|
+
const { owner, name } = parseGitHubRepositorySlug(repository);
|
|
66
|
+
const response = await client.request("GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs", {
|
|
67
|
+
owner,
|
|
68
|
+
repo: name,
|
|
69
|
+
job_id: jobId
|
|
70
|
+
});
|
|
71
|
+
return cappedLogExcerpt(response.data, maxLines);
|
|
72
|
+
}
|
|
73
|
+
async function loadWorkflowJobs(client, repository, runId, options) {
|
|
74
|
+
const { owner, name } = parseGitHubRepositorySlug(repository);
|
|
75
|
+
const jobs = await client.rest.actions.listJobsForWorkflowRun({
|
|
76
|
+
owner,
|
|
77
|
+
repo: name,
|
|
78
|
+
run_id: runId,
|
|
79
|
+
per_page: 100
|
|
80
|
+
});
|
|
81
|
+
const normalized = jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
|
|
82
|
+
if (!options.includeLogs) {
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
85
|
+
await Promise.all(normalized.filter((job) => isFailedConclusion(job.conclusion) && job.id > 0).map(async (job) => {
|
|
86
|
+
try {
|
|
87
|
+
job.logExcerpt = await downloadJobLogExcerpt(client, repository, job.id, options.logLines);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
job.logExcerpt = `Unable to fetch job log: ${error instanceof Error ? error.message : String(error)}`;
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
async function resolveRemoteBranchHead(client, repository, branch) {
|
|
95
|
+
const { owner, name } = parseGitHubRepositorySlug(repository);
|
|
96
|
+
const remote = await client.rest.repos.getBranch({
|
|
97
|
+
owner,
|
|
98
|
+
repo: name,
|
|
99
|
+
branch
|
|
100
|
+
});
|
|
101
|
+
return remote.data.commit.sha;
|
|
102
|
+
}
|
|
103
|
+
async function findWorkflowRun(client, repository, workflow, branch, headSha) {
|
|
104
|
+
const { owner, name } = parseGitHubRepositorySlug(repository);
|
|
105
|
+
const listed = await client.rest.actions.listWorkflowRuns({
|
|
106
|
+
owner,
|
|
107
|
+
repo: name,
|
|
108
|
+
workflow_id: workflow,
|
|
109
|
+
branch: branch ?? void 0,
|
|
110
|
+
head_sha: headSha ?? void 0,
|
|
111
|
+
per_page: 20
|
|
112
|
+
});
|
|
113
|
+
const runs = listed.data.workflow_runs.map((run) => normalizeWorkflowRun(run)).filter((run) => (!headSha || run.headSha === headSha) && (!branch || run.headBranch === branch));
|
|
114
|
+
return runs[0] ?? null;
|
|
115
|
+
}
|
|
116
|
+
function inspectCommand(repository, runId) {
|
|
117
|
+
return repository && runId ? `gh run view ${runId} --repo ${repository} --log-failed` : null;
|
|
118
|
+
}
|
|
119
|
+
function workflowMessage(workflow, state, conclusion) {
|
|
120
|
+
switch (state) {
|
|
121
|
+
case "success":
|
|
122
|
+
return `${workflow} completed successfully.`;
|
|
123
|
+
case "failure":
|
|
124
|
+
return `${workflow} completed with conclusion ${conclusion ?? "unknown"}.`;
|
|
125
|
+
case "pending":
|
|
126
|
+
return `${workflow} is still running or queued.`;
|
|
127
|
+
case "missing":
|
|
128
|
+
return `${workflow} has no run for this branch HEAD.`;
|
|
129
|
+
case "not_pushed":
|
|
130
|
+
return `${workflow} cannot be checked because the local HEAD is not the remote branch HEAD.`;
|
|
131
|
+
case "error":
|
|
132
|
+
return `${workflow} could not be inspected.`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function isGitHubNotFoundError(error) {
|
|
136
|
+
const status = typeof error?.status === "number" ? Number(error.status) : null;
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
138
|
+
return status === 404 || /not found/iu.test(message);
|
|
139
|
+
}
|
|
140
|
+
async function inspectWorkflow(client, target, workflow, options) {
|
|
141
|
+
if (!target.repository || !target.branch || !target.headSha) {
|
|
142
|
+
return {
|
|
143
|
+
workflow,
|
|
144
|
+
state: "error",
|
|
145
|
+
status: null,
|
|
146
|
+
conclusion: null,
|
|
147
|
+
runId: null,
|
|
148
|
+
url: null,
|
|
149
|
+
headSha: target.headSha,
|
|
150
|
+
branch: target.branch,
|
|
151
|
+
createdAt: null,
|
|
152
|
+
updatedAt: null,
|
|
153
|
+
jobs: [],
|
|
154
|
+
failedJobs: [],
|
|
155
|
+
inspectCommand: null,
|
|
156
|
+
message: "Repository, branch, or head SHA is unavailable."
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const run = await findWorkflowRun(client, target.repository, workflow, target.branch, target.headSha);
|
|
161
|
+
const state = workflowStateForRun(run);
|
|
162
|
+
const jobs = run?.id && run.status === "completed" ? await loadWorkflowJobs(client, target.repository, run.id, options) : [];
|
|
163
|
+
const failedJobs = jobs.filter((job) => isFailedConclusion(job.conclusion));
|
|
164
|
+
return {
|
|
165
|
+
workflow,
|
|
166
|
+
state,
|
|
167
|
+
status: run?.status ?? null,
|
|
168
|
+
conclusion: run?.conclusion ?? null,
|
|
169
|
+
runId: run?.id ?? null,
|
|
170
|
+
url: run?.url ?? null,
|
|
171
|
+
headSha: run?.headSha ?? target.headSha,
|
|
172
|
+
branch: run?.headBranch ?? target.branch,
|
|
173
|
+
createdAt: run?.createdAt ?? null,
|
|
174
|
+
updatedAt: run?.updatedAt ?? null,
|
|
175
|
+
jobs,
|
|
176
|
+
failedJobs,
|
|
177
|
+
inspectCommand: inspectCommand(target.repository, run?.id ?? null),
|
|
178
|
+
message: workflowMessage(workflow, state, run?.conclusion ?? null)
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (isGitHubNotFoundError(error)) {
|
|
182
|
+
return {
|
|
183
|
+
workflow,
|
|
184
|
+
state: "missing",
|
|
185
|
+
status: null,
|
|
186
|
+
conclusion: null,
|
|
187
|
+
runId: null,
|
|
188
|
+
url: null,
|
|
189
|
+
headSha: target.headSha,
|
|
190
|
+
branch: target.branch,
|
|
191
|
+
createdAt: null,
|
|
192
|
+
updatedAt: null,
|
|
193
|
+
jobs: [],
|
|
194
|
+
failedJobs: [],
|
|
195
|
+
inspectCommand: null,
|
|
196
|
+
message: `${workflow} is missing or has no run for this branch HEAD.`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
workflow,
|
|
201
|
+
state: "error",
|
|
202
|
+
status: null,
|
|
203
|
+
conclusion: null,
|
|
204
|
+
runId: null,
|
|
205
|
+
url: null,
|
|
206
|
+
headSha: target.headSha,
|
|
207
|
+
branch: target.branch,
|
|
208
|
+
createdAt: null,
|
|
209
|
+
updatedAt: null,
|
|
210
|
+
jobs: [],
|
|
211
|
+
failedJobs: [],
|
|
212
|
+
inspectCommand: null,
|
|
213
|
+
message: error instanceof Error ? error.message : String(error)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function repositoryFailure(target) {
|
|
218
|
+
return {
|
|
219
|
+
type: "repository",
|
|
220
|
+
repository: target.repository,
|
|
221
|
+
repoName: target.name,
|
|
222
|
+
workflow: null,
|
|
223
|
+
runId: null,
|
|
224
|
+
jobId: null,
|
|
225
|
+
jobName: null,
|
|
226
|
+
state: target.state,
|
|
227
|
+
conclusion: null,
|
|
228
|
+
url: null,
|
|
229
|
+
inspectCommand: null,
|
|
230
|
+
message: target.message ?? `${target.name} requires attention.`,
|
|
231
|
+
failedSteps: []
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function workflowFailure(repo, workflow) {
|
|
235
|
+
return {
|
|
236
|
+
type: "workflow",
|
|
237
|
+
repository: repo.repository,
|
|
238
|
+
repoName: repo.name,
|
|
239
|
+
workflow: workflow.workflow,
|
|
240
|
+
runId: workflow.runId,
|
|
241
|
+
jobId: null,
|
|
242
|
+
jobName: null,
|
|
243
|
+
state: workflow.state,
|
|
244
|
+
conclusion: workflow.conclusion,
|
|
245
|
+
url: workflow.url,
|
|
246
|
+
inspectCommand: workflow.inspectCommand,
|
|
247
|
+
message: workflow.message ?? `${repo.name} ${workflow.workflow} requires attention.`,
|
|
248
|
+
failedSteps: []
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function jobFailure(repo, workflow, job) {
|
|
252
|
+
return {
|
|
253
|
+
type: "job",
|
|
254
|
+
repository: repo.repository,
|
|
255
|
+
repoName: repo.name,
|
|
256
|
+
workflow: workflow.workflow,
|
|
257
|
+
runId: workflow.runId,
|
|
258
|
+
jobId: job.id,
|
|
259
|
+
jobName: job.name,
|
|
260
|
+
state: workflow.state,
|
|
261
|
+
conclusion: job.conclusion,
|
|
262
|
+
url: job.url ?? workflow.url,
|
|
263
|
+
inspectCommand: workflow.inspectCommand,
|
|
264
|
+
message: `${repo.name} ${workflow.workflow} job ${job.name || job.id} completed with conclusion ${job.conclusion ?? "unknown"}.`,
|
|
265
|
+
failedSteps: job.failedSteps,
|
|
266
|
+
logExcerpt: job.logExcerpt ?? null
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function collectFailures(repositories) {
|
|
270
|
+
const failures = [];
|
|
271
|
+
for (const repo of repositories) {
|
|
272
|
+
if (repo.state === "not_pushed" || repo.state === "error") {
|
|
273
|
+
failures.push(repositoryFailure(repo));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
for (const workflow of repo.workflows) {
|
|
277
|
+
if (workflow.state === "failure" && workflow.failedJobs.length > 0) {
|
|
278
|
+
failures.push(...workflow.failedJobs.map((job) => jobFailure(repo, workflow, job)));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (workflow.state === "failure" || workflow.state === "missing" || workflow.state === "error") {
|
|
282
|
+
failures.push(workflowFailure(repo, workflow));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return failures;
|
|
287
|
+
}
|
|
288
|
+
function summarize(repositories, failures) {
|
|
289
|
+
const workflows = repositories.flatMap((repo) => repo.workflows);
|
|
290
|
+
return {
|
|
291
|
+
repositories: repositories.length,
|
|
292
|
+
workflows: workflows.length,
|
|
293
|
+
success: workflows.filter((workflow) => workflow.state === "success").length,
|
|
294
|
+
failure: workflows.filter((workflow) => workflow.state === "failure").length,
|
|
295
|
+
pending: workflows.filter((workflow) => workflow.state === "pending").length,
|
|
296
|
+
missing: workflows.filter((workflow) => workflow.state === "missing").length,
|
|
297
|
+
notPushed: repositories.filter((repo) => repo.state === "not_pushed").length,
|
|
298
|
+
error: workflows.filter((workflow) => workflow.state === "error").length + repositories.filter((repo) => repo.state === "error").length,
|
|
299
|
+
failures: failures.length
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function inspectTarget(client, target, options) {
|
|
303
|
+
if (!target.repository || !target.branch || !target.headSha) {
|
|
304
|
+
return {
|
|
305
|
+
name: target.name,
|
|
306
|
+
repoPath: target.repoPath,
|
|
307
|
+
repository: target.repository,
|
|
308
|
+
kind: target.kind ?? "package",
|
|
309
|
+
branch: target.branch,
|
|
310
|
+
headSha: target.headSha,
|
|
311
|
+
remoteHeadSha: null,
|
|
312
|
+
remoteSynced: false,
|
|
313
|
+
state: "error",
|
|
314
|
+
message: "Repository, branch, or head SHA is unavailable.",
|
|
315
|
+
workflows: []
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const remoteHeadSha = await resolveRemoteBranchHead(client, target.repository, target.branch);
|
|
320
|
+
if (remoteHeadSha !== target.headSha) {
|
|
321
|
+
return {
|
|
322
|
+
name: target.name,
|
|
323
|
+
repoPath: target.repoPath,
|
|
324
|
+
repository: target.repository,
|
|
325
|
+
kind: target.kind ?? "package",
|
|
326
|
+
branch: target.branch,
|
|
327
|
+
headSha: target.headSha,
|
|
328
|
+
remoteHeadSha,
|
|
329
|
+
remoteSynced: false,
|
|
330
|
+
state: "not_pushed",
|
|
331
|
+
message: `Local HEAD ${target.headSha.slice(0, 12)} does not match origin/${target.branch} ${remoteHeadSha.slice(0, 12)}.`,
|
|
332
|
+
workflows: target.workflows.map((workflow) => ({
|
|
333
|
+
workflow,
|
|
334
|
+
state: "not_pushed",
|
|
335
|
+
status: null,
|
|
336
|
+
conclusion: null,
|
|
337
|
+
runId: null,
|
|
338
|
+
url: null,
|
|
339
|
+
headSha: target.headSha,
|
|
340
|
+
branch: target.branch,
|
|
341
|
+
createdAt: null,
|
|
342
|
+
updatedAt: null,
|
|
343
|
+
jobs: [],
|
|
344
|
+
failedJobs: [],
|
|
345
|
+
inspectCommand: null,
|
|
346
|
+
message: workflowMessage(workflow, "not_pushed", null)
|
|
347
|
+
}))
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const workflows = await Promise.all(target.workflows.map((workflow) => inspectWorkflow(client, target, workflow, options)));
|
|
351
|
+
return {
|
|
352
|
+
name: target.name,
|
|
353
|
+
repoPath: target.repoPath,
|
|
354
|
+
repository: target.repository,
|
|
355
|
+
kind: target.kind ?? "package",
|
|
356
|
+
branch: target.branch,
|
|
357
|
+
headSha: target.headSha,
|
|
358
|
+
remoteHeadSha,
|
|
359
|
+
remoteSynced: true,
|
|
360
|
+
state: aggregateWorkflowState(workflows.map((workflow) => workflow.state)),
|
|
361
|
+
message: null,
|
|
362
|
+
workflows
|
|
363
|
+
};
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return {
|
|
366
|
+
name: target.name,
|
|
367
|
+
repoPath: target.repoPath,
|
|
368
|
+
repository: target.repository,
|
|
369
|
+
kind: target.kind ?? "package",
|
|
370
|
+
branch: target.branch,
|
|
371
|
+
headSha: target.headSha,
|
|
372
|
+
remoteHeadSha: null,
|
|
373
|
+
remoteSynced: false,
|
|
374
|
+
state: "error",
|
|
375
|
+
message: error instanceof Error ? error.message : String(error),
|
|
376
|
+
workflows: []
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function inspectGitHubActionsVerification(targets, {
|
|
381
|
+
client = createGitHubApiClient(),
|
|
382
|
+
includeLogs = false,
|
|
383
|
+
logLines = 120
|
|
384
|
+
} = {}) {
|
|
385
|
+
const resolvedLogLines = Math.max(20, Math.min(1e3, Math.floor(logLines)));
|
|
386
|
+
const repositories = await Promise.all(targets.map((target) => inspectTarget(client, target, {
|
|
387
|
+
includeLogs,
|
|
388
|
+
logLines: resolvedLogLines
|
|
389
|
+
})));
|
|
390
|
+
const failures = collectFailures(repositories);
|
|
391
|
+
return {
|
|
392
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
393
|
+
repositories,
|
|
394
|
+
failures,
|
|
395
|
+
summary: summarize(repositories, failures)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function skippedGitHubActionsGate(gate, reason) {
|
|
399
|
+
return {
|
|
400
|
+
name: gate.name,
|
|
401
|
+
repository: gate.repository ?? null,
|
|
402
|
+
workflow: gate.workflow,
|
|
403
|
+
branch: gate.branch,
|
|
404
|
+
headSha: gate.headSha,
|
|
405
|
+
status: "skipped",
|
|
406
|
+
reason,
|
|
407
|
+
conclusion: null,
|
|
408
|
+
runId: null,
|
|
409
|
+
url: null
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function formatGitHubActionsGateFailure(gate, result) {
|
|
413
|
+
const repository = String(result.repository ?? gate.repository ?? gate.name);
|
|
414
|
+
const runId = typeof result.runId === "number" || typeof result.runId === "string" ? String(result.runId) : "";
|
|
415
|
+
const url = typeof result.url === "string" && result.url ? `
|
|
416
|
+
${result.url}` : "";
|
|
417
|
+
const failedJobs = Array.isArray(result.failedJobs) ? result.failedJobs.map((job) => typeof job?.name === "string" ? String(job.name) : "").filter(Boolean) : [];
|
|
418
|
+
const jobLine = failedJobs.length > 0 ? `
|
|
419
|
+
Failed jobs: ${failedJobs.join(", ")}` : "";
|
|
420
|
+
const command = runId ? `
|
|
421
|
+
Inspect with: gh run view ${runId} --repo ${repository} --log-failed` : "";
|
|
422
|
+
return `${gate.name} ${gate.workflow} completed with conclusion ${String(result.conclusion ?? "unknown")} in ${repository}.${url}${jobLine}${command}`;
|
|
423
|
+
}
|
|
424
|
+
async function waitForGitHubActionsGate(gate, options = {}) {
|
|
425
|
+
const { waitForGitHubWorkflowCompletion } = await import("./github-automation.js");
|
|
426
|
+
return await waitForGitHubWorkflowCompletion(gate.repoPath, {
|
|
427
|
+
repository: gate.repository,
|
|
428
|
+
workflow: gate.workflow,
|
|
429
|
+
headSha: gate.headSha,
|
|
430
|
+
branch: gate.branch,
|
|
431
|
+
timeoutSeconds: options.timeoutSeconds,
|
|
432
|
+
pollSeconds: options.pollSeconds
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
export {
|
|
436
|
+
formatGitHubActionsGateFailure,
|
|
437
|
+
inspectGitHubActionsVerification,
|
|
438
|
+
skippedGitHubActionsGate,
|
|
439
|
+
waitForGitHubActionsGate
|
|
440
|
+
};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export type TreeseedMailpitContainer = {
|
|
2
|
+
name: string;
|
|
3
|
+
image: string;
|
|
4
|
+
ports: string;
|
|
5
|
+
};
|
|
1
6
|
export declare function dockerIsAvailable(): boolean;
|
|
2
7
|
export declare function findRunningMailpitContainer(): any;
|
|
3
8
|
export declare function stopKnownMailpitContainers(): boolean;
|
|
@@ -35,11 +35,11 @@ function stopKnownMailpitContainers() {
|
|
|
35
35
|
if (!container) {
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
-
const stopResult = runDocker(["stop", container.name]
|
|
38
|
+
const stopResult = runDocker(["stop", container.name]);
|
|
39
39
|
if (stopResult.status !== 0) {
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
|
-
const removeResult = runDocker(["rm", "-f", container.name]
|
|
42
|
+
const removeResult = runDocker(["rm", "-f", container.name]);
|
|
43
43
|
return removeResult.status === 0;
|
|
44
44
|
}
|
|
45
45
|
function streamKnownMailpitLogs() {
|
|
@@ -505,6 +505,9 @@ async function commitMessageFor(node, options, context) {
|
|
|
505
505
|
provider: options.commitMessageProvider
|
|
506
506
|
});
|
|
507
507
|
}
|
|
508
|
+
function commitSubject(message) {
|
|
509
|
+
return String(message ?? "").split(/\r?\n/u)[0]?.trim() || null;
|
|
510
|
+
}
|
|
508
511
|
function gitDiffSummary(repoDir) {
|
|
509
512
|
const changedFiles = run("git", ["status", "--porcelain"], { cwd: repoDir, capture: true });
|
|
510
513
|
const diff = run("git", ["diff", "--cached"], { cwd: repoDir, capture: true });
|
|
@@ -884,19 +887,68 @@ function ensurePackageTagReady(node, options, tagName, branch, workflowRunId) {
|
|
|
884
887
|
}
|
|
885
888
|
return message;
|
|
886
889
|
}
|
|
887
|
-
function
|
|
888
|
-
|
|
889
|
-
|
|
890
|
+
function treeCommitForPath(repoDir, ref, path) {
|
|
891
|
+
try {
|
|
892
|
+
const output = run("git", ["ls-tree", ref, "--", path], { cwd: repoDir, capture: true }).trim();
|
|
893
|
+
const match = output.match(/^\d+\s+commit\s+([0-9a-f]{40})\t/u);
|
|
894
|
+
return match?.[1] ?? null;
|
|
895
|
+
} catch {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function collectSubmodulePointerChanges(node, finalizedCommits) {
|
|
900
|
+
const changes = [];
|
|
901
|
+
for (const [repoName, finalizedCommit] of finalizedCommits.entries()) {
|
|
890
902
|
const childRelativePath = node.id === "." ? repoName : repoName.startsWith(`${node.id}/`) ? repoName.slice(node.id.length + 1) : null;
|
|
891
903
|
if (!childRelativePath) continue;
|
|
892
904
|
const childPath = resolve(node.path, childRelativePath);
|
|
893
905
|
if (!existsSync(childPath) || !isGitRepo(childPath)) continue;
|
|
894
906
|
const status = run("git", ["status", "--porcelain", "--", childRelativePath], { cwd: node.path, capture: true });
|
|
895
|
-
|
|
896
|
-
|
|
907
|
+
const oldSha = treeCommitForPath(node.path, "HEAD", childRelativePath);
|
|
908
|
+
const newSha = finalizedCommit || headCommit(childPath);
|
|
909
|
+
if (status.trim().length > 0 || oldSha && newSha && oldSha !== newSha) {
|
|
910
|
+
changes.push({
|
|
911
|
+
path: childRelativePath,
|
|
912
|
+
oldSha,
|
|
913
|
+
newSha
|
|
914
|
+
});
|
|
897
915
|
}
|
|
898
916
|
}
|
|
899
|
-
return
|
|
917
|
+
return changes;
|
|
918
|
+
}
|
|
919
|
+
function commitContextDependencyUpdates(updates) {
|
|
920
|
+
return updates.map((update) => ({
|
|
921
|
+
packageName: update.packageName,
|
|
922
|
+
field: update.field,
|
|
923
|
+
from: update.from,
|
|
924
|
+
to: update.to,
|
|
925
|
+
tagName: update.tagName
|
|
926
|
+
}));
|
|
927
|
+
}
|
|
928
|
+
function commitContextPackageChanges(node, state, submodulePointers) {
|
|
929
|
+
const pointersByPath = new Map(submodulePointers.map((pointer) => [pointer.path, pointer]));
|
|
930
|
+
const changes = [];
|
|
931
|
+
for (const [relativePath, report] of state.reports.entries()) {
|
|
932
|
+
if (relativePath === node.id || relativePath === ".") continue;
|
|
933
|
+
const childRelativePath = node.id === "." ? relativePath : relativePath.startsWith(`${node.id}/`) ? relativePath.slice(node.id.length + 1) : null;
|
|
934
|
+
if (!childRelativePath) continue;
|
|
935
|
+
const pointer = pointersByPath.get(childRelativePath);
|
|
936
|
+
if (!pointer && !report.committed && !report.tagName && !report.dependencySpec) continue;
|
|
937
|
+
changes.push({
|
|
938
|
+
name: report.name,
|
|
939
|
+
path: childRelativePath,
|
|
940
|
+
oldSha: pointer?.oldSha ?? null,
|
|
941
|
+
newSha: pointer?.newSha ?? report.commitSha,
|
|
942
|
+
tagName: report.tagName,
|
|
943
|
+
version: report.version,
|
|
944
|
+
dependencySpec: report.dependencySpec,
|
|
945
|
+
commitSubject: commitSubject(report.commitMessage)
|
|
946
|
+
});
|
|
947
|
+
if (pointer) {
|
|
948
|
+
pointer.packageName = report.name;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return changes;
|
|
900
952
|
}
|
|
901
953
|
function syncBranchBeforeSave(node, options, branch) {
|
|
902
954
|
checkoutOrCreateBranch(node, options, branch);
|
|
@@ -1201,7 +1253,8 @@ async function saveOneRepository(node, options, state) {
|
|
|
1201
1253
|
const dependencyUpdates = updateDependencyReferences(node, state.finalizedReferences);
|
|
1202
1254
|
const dependencyChanged = dependencyUpdates.length > 0;
|
|
1203
1255
|
const gitDependencyRefreshSpecs = dependencyUpdates.map((update) => state.finalizedReferences.get(update.packageName)).filter((reference) => Boolean(reference) && reference.mode === "dev-git-tag").map((reference) => `${reference.packageName}@${reference.installSpec ?? reference.spec}`);
|
|
1204
|
-
const
|
|
1256
|
+
const submodulePointers = collectSubmodulePointerChanges(node, state.finalizedCommits);
|
|
1257
|
+
const submodulesChanged = submodulePointers.length > 0;
|
|
1205
1258
|
const packageNeedsVersion = node.kind === "package" && (hasMeaningfulChanges(node.path) || dependencyChanged || submodulesChanged);
|
|
1206
1259
|
let plannedVersion = null;
|
|
1207
1260
|
if (packageNeedsVersion) {
|
|
@@ -1294,7 +1347,10 @@ async function saveOneRepository(node, options, state) {
|
|
|
1294
1347
|
changedFiles,
|
|
1295
1348
|
diff,
|
|
1296
1349
|
plannedVersion: plannedVersion ?? report.version,
|
|
1297
|
-
plannedTag: node.plannedTag ?? report.tagName
|
|
1350
|
+
plannedTag: node.plannedTag ?? report.tagName,
|
|
1351
|
+
dependencyUpdates: commitContextDependencyUpdates(dependencyUpdates),
|
|
1352
|
+
submodulePointers,
|
|
1353
|
+
packageChanges: commitContextPackageChanges(node, state, submodulePointers)
|
|
1298
1354
|
});
|
|
1299
1355
|
report.commitMessage = messageResult.message;
|
|
1300
1356
|
report.commitMessageProvider = messageResult.provider;
|
|
@@ -109,6 +109,9 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
|
|
|
109
109
|
rootDir: string | undefined;
|
|
110
110
|
publicBaseUrl: string | undefined;
|
|
111
111
|
localBaseUrl: string | undefined;
|
|
112
|
+
local: {
|
|
113
|
+
runtime: string | undefined;
|
|
114
|
+
} | undefined;
|
|
112
115
|
environments: {
|
|
113
116
|
local: {
|
|
114
117
|
baseUrl: string | undefined;
|
|
@@ -160,6 +163,9 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
|
|
|
160
163
|
provider: string | undefined;
|
|
161
164
|
rootDir: string | undefined;
|
|
162
165
|
publicBaseUrl: string | undefined;
|
|
166
|
+
local: {
|
|
167
|
+
runtime: string | undefined;
|
|
168
|
+
} | undefined;
|
|
163
169
|
cloudflare: {
|
|
164
170
|
workerName: string | undefined;
|
|
165
171
|
};
|
|
@@ -112,6 +112,15 @@ function parseServiceEnvironmentConfig(value) {
|
|
|
112
112
|
railwayEnvironment: optionalString(record.railwayEnvironment)
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
+
function parseLocalRuntimeConfig(value, label) {
|
|
116
|
+
const record = optionalRecord(value, label);
|
|
117
|
+
if (!record) {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
runtime: optionalEnum(record.runtime ?? record.runtime_mode ?? record.runtimeMode, `${label}.runtime`, ["auto", "provider", "local"])
|
|
122
|
+
};
|
|
123
|
+
}
|
|
115
124
|
function parseManagedServiceConfig(value, label) {
|
|
116
125
|
const record = optionalRecord(value, label);
|
|
117
126
|
if (!record) {
|
|
@@ -125,6 +134,7 @@ function parseManagedServiceConfig(value, label) {
|
|
|
125
134
|
provider: optionalString(record.provider),
|
|
126
135
|
rootDir: optionalString(record.rootDir),
|
|
127
136
|
publicBaseUrl: optionalString(record.publicBaseUrl),
|
|
137
|
+
local: parseLocalRuntimeConfig(record.local, `${label}.local`),
|
|
128
138
|
cloudflare: {
|
|
129
139
|
workerName: optionalString(cloudflare.workerName)
|
|
130
140
|
},
|
|
@@ -212,6 +222,7 @@ function parseSurfaceConfig(value, label) {
|
|
|
212
222
|
rootDir: optionalString(record.rootDir),
|
|
213
223
|
publicBaseUrl: optionalString(record.publicBaseUrl),
|
|
214
224
|
localBaseUrl: optionalString(record.localBaseUrl),
|
|
225
|
+
local: parseLocalRuntimeConfig(record.local, `${label}.local`),
|
|
215
226
|
environments: (() => {
|
|
216
227
|
const environments = optionalRecord(record.environments, `${label}.environments`);
|
|
217
228
|
if (!environments) {
|
|
@@ -4,6 +4,7 @@ function operation(spec) {
|
|
|
4
4
|
const workspaceCommand = (name) => `workspace${":"}${name}`;
|
|
5
5
|
const TRESEED_OPERATION_SPECS = [
|
|
6
6
|
operation({ id: "workspace.status", name: "status", aliases: [], group: "Workflow", summary: "Show Treeseed project health and the current task state.", description: "Report branch/task state, runtime readiness, preview/deploy state, auth readiness, and recommended next operations.", provider: "default", related: ["tasks", "switch", "config"] }),
|
|
7
|
+
operation({ id: "workspace.ci", name: "ci", aliases: [], group: "Validation", summary: "Inspect remote GitHub Actions status for the active workspace commits.", description: "Check the latest GitHub Actions workflow runs for the current branch heads across market and checked-out package repositories, including failed jobs and inspect commands.", provider: "default", related: ["status", "save", "stage"] }),
|
|
7
8
|
operation({ id: "branch.tasks", name: "tasks", aliases: [], group: "Workflow", summary: "List task branches plus package branch alignment.", description: "List task branches from market and checked-out package repos, including preview metadata and branch/pointer alignment state.", provider: "default", related: ["status", "switch", "close"] }),
|
|
8
9
|
operation({ id: "branch.switch", name: "switch", aliases: [], group: "Workflow", summary: "Create or resume a recursive task branch.", description: "Create or resume the task branch in market and any checked-out package repos, with optional preview provisioning.", provider: "default", related: ["tasks", "dev", "save"] }),
|
|
9
10
|
operation({ id: "branch.save", name: "save", aliases: [], group: "Workflow", summary: "Recursively verify, commit, and push the current task checkpoint.", description: "Save dirty package repos in dependency order, then verify, commit, push, and optionally refresh preview for market.", provider: "default", related: ["switch", "stage", "status"] }),
|
|
@@ -122,6 +122,7 @@ export interface TreeseedPlatformSurfaceConfig {
|
|
|
122
122
|
rootDir?: string;
|
|
123
123
|
publicBaseUrl?: string;
|
|
124
124
|
localBaseUrl?: string;
|
|
125
|
+
local?: TreeseedLocalRuntimeConfig;
|
|
125
126
|
environments?: Partial<Record<'local' | 'staging' | 'prod', TreeseedManagedServiceEnvironmentConfig>>;
|
|
126
127
|
cache?: TreeseedWebSurfaceCacheConfig;
|
|
127
128
|
}
|
|
@@ -182,6 +183,10 @@ export interface TreeseedManagedServiceEnvironmentConfig {
|
|
|
182
183
|
domain?: string;
|
|
183
184
|
railwayEnvironment?: string;
|
|
184
185
|
}
|
|
186
|
+
export type TreeseedLocalRuntimeMode = 'auto' | 'provider' | 'local';
|
|
187
|
+
export interface TreeseedLocalRuntimeConfig {
|
|
188
|
+
runtime?: TreeseedLocalRuntimeMode;
|
|
189
|
+
}
|
|
185
190
|
export interface TreeseedManagedServiceCloudflareConfig {
|
|
186
191
|
workerName?: string;
|
|
187
192
|
}
|
|
@@ -207,6 +212,7 @@ export interface TreeseedManagedServiceConfig {
|
|
|
207
212
|
publicBaseUrl?: string;
|
|
208
213
|
cloudflare?: TreeseedManagedServiceCloudflareConfig;
|
|
209
214
|
railway?: TreeseedManagedServiceRailwayConfig;
|
|
215
|
+
local?: TreeseedLocalRuntimeConfig;
|
|
210
216
|
environments?: Partial<Record<'local' | 'staging' | 'prod', TreeseedManagedServiceEnvironmentConfig>>;
|
|
211
217
|
}
|
|
212
218
|
export interface TreeseedManagedServicesConfig {
|