@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/jobs.js
CHANGED
|
@@ -102,6 +102,15 @@ export function writeJob(config, input) {
|
|
|
102
102
|
});
|
|
103
103
|
return { job, jsonPath };
|
|
104
104
|
}
|
|
105
|
+
/** Record a cancel on the job file. A second call leaves the file as it is. */
|
|
106
|
+
export function markJobCancelled(config, job, now = new Date()) {
|
|
107
|
+
if (job.cancelledAt)
|
|
108
|
+
return job;
|
|
109
|
+
const next = JobSchema.parse({ ...job, cancelledAt: now.toISOString() });
|
|
110
|
+
const jsonPath = jobFilePath(config.jobsDir, job.id);
|
|
111
|
+
writeFileSync(jsonPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
105
114
|
export function renderAddedJob(job, jsonPath) {
|
|
106
115
|
const lines = [`added ${job.id}`, `repo ${job.repo}`, `workflow ${job.workflow}`, `spec ${job.spec}`];
|
|
107
116
|
if (job.issue)
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small markdown subset for the board. Text is escaped before any tag is
|
|
3
|
+
* added, so a draft cannot inject HTML or a script. Links are http(s) only.
|
|
4
|
+
*/
|
|
5
|
+
export function renderMarkdown(src) {
|
|
6
|
+
const lines = src.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
7
|
+
let index = 0;
|
|
8
|
+
let html = "";
|
|
9
|
+
while (index < lines.length) {
|
|
10
|
+
const line = lines[index] ?? "";
|
|
11
|
+
if (!line.trim()) {
|
|
12
|
+
index += 1;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (line.startsWith("```")) {
|
|
16
|
+
const fence = [];
|
|
17
|
+
index += 1;
|
|
18
|
+
while (index < lines.length && !(lines[index] ?? "").startsWith("```")) {
|
|
19
|
+
fence.push(lines[index] ?? "");
|
|
20
|
+
index += 1;
|
|
21
|
+
}
|
|
22
|
+
index += 1;
|
|
23
|
+
html += `<pre><code>${escapeHtml(fence.join("\n"))}</code></pre>`;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
27
|
+
if (heading?.[1] && heading[2] !== undefined) {
|
|
28
|
+
const level = Math.min(heading[1].length, 3);
|
|
29
|
+
html += `<h${level}>${inline(heading[2])}</h${level}>`;
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (line.startsWith(">")) {
|
|
34
|
+
const quote = [];
|
|
35
|
+
while (index < lines.length && (lines[index] ?? "").startsWith(">")) {
|
|
36
|
+
quote.push((lines[index] ?? "").replace(/^>\s?/, ""));
|
|
37
|
+
index += 1;
|
|
38
|
+
}
|
|
39
|
+
html += `<blockquote>${inline(quote.join(" "))}</blockquote>`;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (isList(line)) {
|
|
43
|
+
const ordered = /^\s*\d+\./.test(line);
|
|
44
|
+
const items = [];
|
|
45
|
+
while (index < lines.length && (isList(lines[index] ?? "") || (/^\s{2,}\S/.test(lines[index] ?? "") && items.length > 0))) {
|
|
46
|
+
const item = lines[index] ?? "";
|
|
47
|
+
index += 1;
|
|
48
|
+
if (isList(item))
|
|
49
|
+
items.push(item.replace(/^\s*(?:[-*+]|\d+\.)\s+/, ""));
|
|
50
|
+
else
|
|
51
|
+
items[items.length - 1] = `${items[items.length - 1]} ${item.trim()}`;
|
|
52
|
+
}
|
|
53
|
+
const tag = ordered ? "ol" : "ul";
|
|
54
|
+
html += `<${tag}>${items.map((item) => `<li>${inline(item)}</li>`).join("")}</${tag}>`;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const paragraph = [];
|
|
58
|
+
while (index < lines.length && (lines[index] ?? "").trim() && !isBlock(lines[index] ?? "")) {
|
|
59
|
+
paragraph.push(lines[index] ?? "");
|
|
60
|
+
index += 1;
|
|
61
|
+
}
|
|
62
|
+
html += `<p>${inline(paragraph.join(" "))}</p>`;
|
|
63
|
+
}
|
|
64
|
+
return html;
|
|
65
|
+
}
|
|
66
|
+
function isList(line) {
|
|
67
|
+
return /^\s*(?:[-*+]|\d+\.)\s+/.test(line);
|
|
68
|
+
}
|
|
69
|
+
function isBlock(line) {
|
|
70
|
+
return /^(?:#{1,6}\s|```|>)/.test(line) || isList(line);
|
|
71
|
+
}
|
|
72
|
+
function inline(raw) {
|
|
73
|
+
const codes = [];
|
|
74
|
+
const slotted = raw.replace(/`([^`]+)`/g, (_match, code) => {
|
|
75
|
+
codes.push(escapeHtml(code));
|
|
76
|
+
return `\u0000${codes.length - 1}\u0000`;
|
|
77
|
+
});
|
|
78
|
+
let html = escapeHtml(slotted);
|
|
79
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
80
|
+
html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
|
|
81
|
+
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" rel="noopener noreferrer">$1</a>');
|
|
82
|
+
return html.replace(/\u0000(\d+)\u0000/g, (_match, slot) => `<code>${codes[Number(slot)] ?? ""}</code>`);
|
|
83
|
+
}
|
|
84
|
+
export function escapeHtml(value) {
|
|
85
|
+
return value
|
|
86
|
+
.replaceAll("&", "&")
|
|
87
|
+
.replaceAll("<", "<")
|
|
88
|
+
.replaceAll(">", ">")
|
|
89
|
+
.replaceAll('"', """)
|
|
90
|
+
.replaceAll("'", "'");
|
|
91
|
+
}
|
package/dist/project.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { basename, join, relative, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { appendEvent } from "./events.js";
|
|
6
7
|
import { costFromJournal, decideFailure } from "./failure.js";
|
|
7
8
|
import { jobBasename, packageRoot } from "./paths.js";
|
|
@@ -24,7 +25,31 @@ export function projectRunJournal(input) {
|
|
|
24
25
|
console.error(`kiln: event log: ${message}`);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Version of the `@lchase/kiln` package that contains this file.
|
|
30
|
+
* Walks up from the running module so a parent package.json (the workspace
|
|
31
|
+
* root has none) cannot supply a stale or missing version.
|
|
32
|
+
*/
|
|
27
33
|
export function kilnVersion() {
|
|
34
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
for (let i = 0; i < 8; i += 1) {
|
|
36
|
+
const pkgPath = join(dir, "package.json");
|
|
37
|
+
if (existsSync(pkgPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
40
|
+
if (parsed.name === "@lchase/kiln" && typeof parsed.version === "string" && parsed.version) {
|
|
41
|
+
return parsed.version;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// An unreadable file is not the running package. Keep walking.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const parent = dirname(dir);
|
|
49
|
+
if (parent === dir)
|
|
50
|
+
break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
28
53
|
return packageVersion(join(packageRoot(), "package.json"));
|
|
29
54
|
}
|
|
30
55
|
export function yakVersion() {
|
package/dist/reconcile.js
CHANGED
|
@@ -35,6 +35,7 @@ function inFlight(job, observation) {
|
|
|
35
35
|
* | launching breadcrumb | running |
|
|
36
36
|
* | dependencies unmet | blocked |
|
|
37
37
|
* | launch error | failed |
|
|
38
|
+
* | cancelled before start | cancelled |
|
|
38
39
|
* | otherwise | queued |
|
|
39
40
|
*
|
|
40
41
|
* `deliver=local`: `done` is a finished run whose gate says approve.
|
|
@@ -51,6 +52,8 @@ export function jobState(job, observation) {
|
|
|
51
52
|
return "in-review";
|
|
52
53
|
if (job.launching && (!job.run || job.runRetired || job.fixInProgress))
|
|
53
54
|
return "running";
|
|
55
|
+
if (job.cancelled && !job.run && !job.launching)
|
|
56
|
+
return "cancelled";
|
|
54
57
|
if ((job.feedbackExhausted || job.feedbackAmbiguous) && isLanded(job) && !job.fixInProgress) {
|
|
55
58
|
if (job.delivery?.merged || job.delivery?.prState === "MERGED")
|
|
56
59
|
return "done";
|
|
@@ -165,7 +168,7 @@ function needsDelivery(job) {
|
|
|
165
168
|
return true;
|
|
166
169
|
}
|
|
167
170
|
function canLaunch(job, observation) {
|
|
168
|
-
if (job.error || job.run || job.launching || job.launchFailed || job.fixInProgress)
|
|
171
|
+
if (job.cancelled || job.error || job.run || job.launching || job.launchFailed || job.fixInProgress)
|
|
169
172
|
return false;
|
|
170
173
|
return dependenciesMet(job, observation);
|
|
171
174
|
}
|
|
@@ -177,7 +180,7 @@ function canStartFix(job) {
|
|
|
177
180
|
}
|
|
178
181
|
/** A recorded transient failure whose backoff has elapsed. One retry per tick, ahead of a new job. */
|
|
179
182
|
function canRetry(job, observation, now) {
|
|
180
|
-
if (job.error || job.launching || job.retriesExhausted || job.stopReason || job.ledgerError)
|
|
183
|
+
if (job.cancelled || job.error || job.launching || job.retriesExhausted || job.stopReason || job.ledgerError)
|
|
181
184
|
return false;
|
|
182
185
|
if (job.run && !job.runRetired)
|
|
183
186
|
return false;
|
package/dist/schemas.js
CHANGED
|
@@ -48,6 +48,11 @@ export const RepoEntrySchema = z.object({
|
|
|
48
48
|
.int("Max fix rounds must be a whole number.")
|
|
49
49
|
.min(1, "Max fix rounds must be at least 1.")
|
|
50
50
|
.optional(),
|
|
51
|
+
/**
|
|
52
|
+
* GitHub label `review-requests` searches for in this checkout.
|
|
53
|
+
* Overrides the trigger `label`. Omitted uses that label, which defaults to `kiln-review`.
|
|
54
|
+
*/
|
|
55
|
+
reviewLabel: z.string().min(1).optional(),
|
|
51
56
|
});
|
|
52
57
|
/** Repo map shared by the config file and the settings API. */
|
|
53
58
|
export const KilnReposSchema = z.record(z.string().regex(REPO_NAME, "must start with a letter or digit and contain only letters, digits, dots, underscores, and hyphens"), RepoEntrySchema);
|
|
@@ -135,6 +140,10 @@ const TRIGGER_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
135
140
|
* `schedule` uses `cron` (5 fields, local time). `jira` uses `jql` or
|
|
136
141
|
* `label` / `status` (compiled to JQL) and `every` (default `5m`).
|
|
137
142
|
* `pr` is the built-in Kiln-owned pull request source.
|
|
143
|
+
* On `review-requests`, `label` is the GitHub label a pull request must
|
|
144
|
+
* carry (default `kiln-review`). `maxNew` is how many new review jobs one
|
|
145
|
+
* fire may create (default 3). `maxQueued` is how many review jobs may be
|
|
146
|
+
* queued or running at once (default 5).
|
|
138
147
|
*/
|
|
139
148
|
export const TriggerConfigSchema = z
|
|
140
149
|
.object({
|
|
@@ -147,6 +156,10 @@ export const TriggerConfigSchema = z
|
|
|
147
156
|
jql: z.string().min(1).optional(),
|
|
148
157
|
label: z.string().min(1).optional(),
|
|
149
158
|
status: z.string().min(1).optional(),
|
|
159
|
+
/** Review jobs created on one fire of `review-requests`. Default 3. */
|
|
160
|
+
maxNew: z.number().int().min(1).optional(),
|
|
161
|
+
/** Queued or running review jobs at once. Default 5. */
|
|
162
|
+
maxQueued: z.number().int().min(1).optional(),
|
|
150
163
|
})
|
|
151
164
|
.strict();
|
|
152
165
|
/** Maps a trigger id to create a job or attach to one. Hot-reloaded with the config. */
|
|
@@ -240,6 +253,11 @@ export const JobSchema = z.object({
|
|
|
240
253
|
issue: z.string().regex(ISSUE_KEY).optional(),
|
|
241
254
|
source: JobSourceSchema.optional(),
|
|
242
255
|
createdAt: z.string().min(1),
|
|
256
|
+
/**
|
|
257
|
+
* Set when a person cancels the job before it starts.
|
|
258
|
+
* Tick does not launch it. Cancelling does not delete a branch.
|
|
259
|
+
*/
|
|
260
|
+
cancelledAt: z.string().min(1).optional(),
|
|
243
261
|
});
|
|
244
262
|
/**
|
|
245
263
|
* Derived from the yak journal, gate files, and (for `deliver=pr`) the
|
|
@@ -255,7 +273,8 @@ export const JobSchema = z.object({
|
|
|
255
273
|
* after-merged dependent. A stacked job unblocks when its one upstream has
|
|
256
274
|
* landed, which includes `in-review`. A job that has used its attempt cap,
|
|
257
275
|
* fix-round cap, or fingerprint breaker is `needs-human`. `dead` is a run
|
|
258
|
-
* whose process died and has not been recorded yet.
|
|
276
|
+
* whose process died and has not been recorded yet. `cancelled` is a job
|
|
277
|
+
* cancelled before it started. Attempts live under
|
|
259
278
|
* the kiln directory, not in this file.
|
|
260
279
|
*/
|
|
261
280
|
export const JOB_STATES = [
|
|
@@ -269,4 +288,5 @@ export const JOB_STATES = [
|
|
|
269
288
|
"dead",
|
|
270
289
|
"in-review",
|
|
271
290
|
"done",
|
|
291
|
+
"cancelled",
|
|
272
292
|
];
|