@lchase/kiln 0.13.0 → 0.15.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 +14 -3
- package/assets/serve.css +136 -11
- package/assets/serve.js +44 -5
- package/dist/attention.js +3 -1
- package/dist/board-view.js +4 -169
- package/dist/cli.js +48 -2
- package/dist/config.js +1 -0
- package/dist/driver.js +44 -9
- package/dist/events.js +10 -1
- package/dist/host.js +45 -2
- package/dist/job-view.js +755 -0
- package/dist/jobs.js +9 -0
- package/dist/markdown.js +91 -0
- package/dist/project.js +26 -1
- package/dist/reconcile.js +5 -2
- package/dist/schemas.js +21 -1
- package/dist/serve-page.js +275 -66
- package/dist/serve.js +46 -2
- package/dist/settings.js +4 -2
- package/dist/triggers.js +220 -21
- package/package.json +1 -1
package/dist/driver.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ensureFixTrailers } from "./fix-commit.js";
|
|
|
13
13
|
import { holdDetail, pausedFrom, recordDailyCap, reviseNotesSinceRun, spendSnapshot, stopReasonFromEvents, withReviseNotes, } from "./rails.js";
|
|
14
14
|
import { appendInvestigationFindings, existingFindingsPath, findingsFilePath, INVESTIGATION_WORKFLOW, readFindingsMarkdown, writeFindings, } from "./findings.js";
|
|
15
15
|
import { defaultHost } from "./host.js";
|
|
16
|
-
import { readJobSpec, resolveJobSpec } from "./jobs.js";
|
|
16
|
+
import { markJobCancelled, readJobSpec, resolveJobSpec } from "./jobs.js";
|
|
17
17
|
import { jobBasename, packageRoot } from "./paths.js";
|
|
18
18
|
import { decide, isLanded, jobState, } from "./reconcile.js";
|
|
19
19
|
import { fileMtimeMs, isPidAlive, readAttemptLedger, recoverRuns, resetAttemptLedger, } from "./reliability.js";
|
|
@@ -23,7 +23,7 @@ import { gateAnswerCommand, renderStatus, renderTick, statusView, yakCliPath, sa
|
|
|
23
23
|
import { AGENT_SANDBOX_IMAGE, injectAgentSandbox, jobIsSandboxed, probeSandboxEnvironment, PROXY_SANDBOX_IMAGE, requiredSandboxImages, resolveSandbox, sandboxIsEnabled, sandboxPreflight, sandboxPreflightFor, } from "./sandbox.js";
|
|
24
24
|
import { JobSchema } from "./schemas.js";
|
|
25
25
|
import { ensureReviewHead, evaluateTriggers, recordOwnedPullRequest, reviewHeadOf, } from "./triggers.js";
|
|
26
|
-
import { leafStepIds, materializeWorkflow, REVIEW_WORKFLOW, workflowInputs, workflowNeedsTestCommand, workflowStepPlan, } from "./workflow.js";
|
|
26
|
+
import { assertWorkflowName, leafStepIds, materializeWorkflow, REVIEW_WORKFLOW, workflowInputs, workflowNeedsTestCommand, workflowStepPlan, } from "./workflow.js";
|
|
27
27
|
const TAG_PREFIX = "kiln:";
|
|
28
28
|
export function jobTag(jobId) {
|
|
29
29
|
return `${TAG_PREFIX}${jobId}`;
|
|
@@ -137,14 +137,48 @@ export function answerJob(config, jobId, choice, host = defaultHost()) {
|
|
|
137
137
|
return output.endsWith("\n") || output.length === 0 ? output : `${output}\n`;
|
|
138
138
|
}
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* the same deliver path tick uses. Everything else is a new `yak run`, the
|
|
144
|
-
* same launch tick uses. The attempt ledger is cleared either way, and the
|
|
145
|
-
* current run id is ignored so a dead job is not stuck on the old journal.
|
|
146
|
-
* A dead journal is cancelled first.
|
|
140
|
+
* Cancel queued jobs that have not started.
|
|
141
|
+
* A running, blocked, or finished job is left alone. Calling again is a no-op.
|
|
142
|
+
* The event log records `job.cancelled`. Branches are not deleted.
|
|
147
143
|
*/
|
|
144
|
+
export function cancelQueuedJobs(config, options = {}, host = defaultHost()) {
|
|
145
|
+
return withTickLock(config.kilnDir, () => {
|
|
146
|
+
if (options.repo && !config.repos[options.repo]) {
|
|
147
|
+
throw new Error(`Unknown repo ${options.repo}.`);
|
|
148
|
+
}
|
|
149
|
+
if (options.workflow)
|
|
150
|
+
assertWorkflowName(options.workflow);
|
|
151
|
+
const now = options.now ?? new Date();
|
|
152
|
+
const world = observe(config, host, now);
|
|
153
|
+
const cancelled = [];
|
|
154
|
+
for (const observed of world.observation.jobs) {
|
|
155
|
+
if (observed.error)
|
|
156
|
+
continue;
|
|
157
|
+
if (options.repo && observed.repo !== options.repo)
|
|
158
|
+
continue;
|
|
159
|
+
if (options.workflow && observed.workflow !== options.workflow)
|
|
160
|
+
continue;
|
|
161
|
+
if (jobState(observed, world.observation) !== "queued")
|
|
162
|
+
continue;
|
|
163
|
+
const job = world.jobs.get(observed.jobId);
|
|
164
|
+
if (!job || job.cancelledAt)
|
|
165
|
+
continue;
|
|
166
|
+
markJobCancelled(config, job, now);
|
|
167
|
+
appendEvent(config.kilnDir, {
|
|
168
|
+
job_id: job.id,
|
|
169
|
+
event: "job.cancelled",
|
|
170
|
+
ts: now.toISOString(),
|
|
171
|
+
properties: {
|
|
172
|
+
repo: job.repo,
|
|
173
|
+
workflow: job.workflow,
|
|
174
|
+
once: "job.cancelled",
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
cancelled.push(job.id);
|
|
178
|
+
}
|
|
179
|
+
return { cancelled };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
148
182
|
export function retryJob(config, jobId, host = defaultHost()) {
|
|
149
183
|
return withTickLock(config.kilnDir, () => {
|
|
150
184
|
const world = observe(config, host);
|
|
@@ -366,6 +400,7 @@ function observe(config, host, now = new Date()) {
|
|
|
366
400
|
...(launchActivityMs !== undefined ? { launchActivityMs } : {}),
|
|
367
401
|
...(logPath ? { logPath } : {}),
|
|
368
402
|
...(entry.job?.createdAt ? { createdAt: entry.job.createdAt } : {}),
|
|
403
|
+
...(entry.job?.cancelledAt ? { cancelled: true } : {}),
|
|
369
404
|
...(logTail ? { logTail } : {}),
|
|
370
405
|
...(diffSummary ? { diffSummary } : {}),
|
|
371
406
|
...(stepPlan && stepPlan.length > 0 ? { stepPlan } : {}),
|
package/dist/events.js
CHANGED
|
@@ -14,6 +14,7 @@ import { redactDeep } from "./redact.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export const EVENT_NAMES = [
|
|
16
16
|
"job.added",
|
|
17
|
+
"job.cancelled",
|
|
17
18
|
"job.retried",
|
|
18
19
|
"run.started",
|
|
19
20
|
"phase.started",
|
|
@@ -52,6 +53,11 @@ const JobRetried = z.object({
|
|
|
52
53
|
repo: z.string().min(1),
|
|
53
54
|
once,
|
|
54
55
|
});
|
|
56
|
+
const JobCancelled = z.object({
|
|
57
|
+
repo: z.string().min(1),
|
|
58
|
+
workflow: z.string().min(1),
|
|
59
|
+
once,
|
|
60
|
+
});
|
|
55
61
|
const RunStarted = z.object({
|
|
56
62
|
kiln_version: z.string().min(1),
|
|
57
63
|
yak_version: z.string().min(1),
|
|
@@ -163,7 +169,9 @@ const TriggerFailed = z.object({
|
|
|
163
169
|
});
|
|
164
170
|
const TriggerSkipped = z.object({
|
|
165
171
|
trigger_id: z.string().min(1),
|
|
166
|
-
reason: z.enum(["paused", "capped"]),
|
|
172
|
+
reason: z.enum(["paused", "capped", "burst-cap", "queued-cap"]),
|
|
173
|
+
/** Pull request or issue the skip applies to, when the fire skipped one item. */
|
|
174
|
+
external_id: z.string().min(1).optional(),
|
|
167
175
|
once,
|
|
168
176
|
});
|
|
169
177
|
const ReviewPosted = z.object({
|
|
@@ -196,6 +204,7 @@ const envelope = {
|
|
|
196
204
|
};
|
|
197
205
|
export const EventSchema = z.discriminatedUnion("event", [
|
|
198
206
|
z.object({ ...envelope, event: z.literal("job.added"), job_id: z.string().min(1), properties: JobAdded }),
|
|
207
|
+
z.object({ ...envelope, event: z.literal("job.cancelled"), job_id: z.string().min(1), properties: JobCancelled }),
|
|
199
208
|
z.object({ ...envelope, event: z.literal("job.retried"), job_id: z.string().min(1), properties: JobRetried }),
|
|
200
209
|
z.object({ ...envelope, event: z.literal("run.started"), job_id: z.string().min(1), properties: RunStarted }),
|
|
201
210
|
z.object({ ...envelope, event: z.literal("phase.started"), job_id: z.string().min(1), properties: PhaseStarted }),
|
package/dist/host.js
CHANGED
|
@@ -15,6 +15,15 @@ export class HostError extends Error {
|
|
|
15
15
|
this.name = "HostError";
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
/** GitHub label `review-requests` requires when the trigger sets none. */
|
|
19
|
+
export const DEFAULT_REVIEW_LABEL = "kiln-review";
|
|
20
|
+
/** `gh pr list --search` query. Opt-in: the pull request must carry `label`. */
|
|
21
|
+
export function reviewSearchQuery(label) {
|
|
22
|
+
const trimmed = label.trim();
|
|
23
|
+
if (!trimmed || /[\r\n]/.test(trimmed))
|
|
24
|
+
throw new Error("Review label must be a single line.");
|
|
25
|
+
return `review-requested:@me label:${quoteSearch(trimmed)}`;
|
|
26
|
+
}
|
|
18
27
|
export function defaultHost() {
|
|
19
28
|
return { github: realGitHub, git: realGitRemote, feedback: realPullRequestFeedback };
|
|
20
29
|
}
|
|
@@ -24,14 +33,27 @@ export function productionHost() {
|
|
|
24
33
|
}
|
|
25
34
|
/**
|
|
26
35
|
* Open pull requests awaiting the authenticated user's review, per registered repo.
|
|
27
|
-
*
|
|
36
|
+
* The search is opt-in: a pull request must carry `label`, or `kiln-review` when
|
|
37
|
+
* that is omitted. A remote that is not GitHub is skipped, so a local test origin does not call gh.
|
|
28
38
|
*/
|
|
29
39
|
export function listReviewRequests(repos) {
|
|
30
40
|
const found = [];
|
|
31
41
|
for (const repo of repos) {
|
|
32
42
|
if (!githubRemote(repo.path))
|
|
33
43
|
continue;
|
|
34
|
-
const
|
|
44
|
+
const label = repo.label?.trim() || DEFAULT_REVIEW_LABEL;
|
|
45
|
+
const result = spawnSync(ghBin(), [
|
|
46
|
+
"pr",
|
|
47
|
+
"list",
|
|
48
|
+
"--search",
|
|
49
|
+
reviewSearchQuery(label),
|
|
50
|
+
"--state",
|
|
51
|
+
"open",
|
|
52
|
+
"--limit",
|
|
53
|
+
"50",
|
|
54
|
+
"--json",
|
|
55
|
+
"number,title,url,headRefOid,labels",
|
|
56
|
+
], {
|
|
35
57
|
cwd: repo.path,
|
|
36
58
|
encoding: "utf8",
|
|
37
59
|
timeout: 20_000,
|
|
@@ -57,6 +79,8 @@ export function listReviewRequests(repos) {
|
|
|
57
79
|
const sha = typeof record.headRefOid === "string" ? record.headRefOid.trim().toLowerCase() : "";
|
|
58
80
|
if (!Number.isInteger(number) || number <= 0 || !/^[0-9a-f]{7,64}$/.test(sha))
|
|
59
81
|
continue;
|
|
82
|
+
if (!carriesLabel(record.labels, label))
|
|
83
|
+
continue;
|
|
60
84
|
const title = typeof record.title === "string" && record.title.trim() ? record.title.trim() : `PR ${number}`;
|
|
61
85
|
const url = typeof record.url === "string" ? record.url : "";
|
|
62
86
|
found.push({ repo: repo.name, number, title, url, headSha: sha });
|
|
@@ -64,6 +88,25 @@ export function listReviewRequests(repos) {
|
|
|
64
88
|
}
|
|
65
89
|
return found;
|
|
66
90
|
}
|
|
91
|
+
/** Missing `labels` trusts the search. A present list must include `label`. */
|
|
92
|
+
function carriesLabel(labels, label) {
|
|
93
|
+
if (labels === undefined)
|
|
94
|
+
return true;
|
|
95
|
+
if (!Array.isArray(labels))
|
|
96
|
+
return false;
|
|
97
|
+
const want = label.trim().toLowerCase();
|
|
98
|
+
return labels.some((item) => {
|
|
99
|
+
if (typeof item === "string")
|
|
100
|
+
return item.trim().toLowerCase() === want;
|
|
101
|
+
if (typeof item !== "object" || item === null || !("name" in item))
|
|
102
|
+
return false;
|
|
103
|
+
const name = item.name;
|
|
104
|
+
return typeof name === "string" && name.trim().toLowerCase() === want;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function quoteSearch(value) {
|
|
108
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
109
|
+
}
|
|
67
110
|
function githubRemote(checkout) {
|
|
68
111
|
const result = gitResult(checkout, ["remote", "get-url", "origin"]);
|
|
69
112
|
if (result.status !== 0)
|