@lchase/kiln 0.11.0 → 0.12.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 +27 -0
- package/assets/serve.css +20 -0
- package/assets/serve.js +53 -0
- package/dist/cli.js +7 -1
- package/dist/config.js +5 -0
- package/dist/driver.js +48 -13
- package/dist/events.js +59 -2
- package/dist/host.js +53 -0
- package/dist/intake/jira.js +87 -0
- package/dist/jobs.js +4 -3
- package/dist/reconcile.js +7 -3
- package/dist/schemas.js +51 -3
- package/dist/serve-page.js +31 -0
- package/dist/serve.js +13 -4
- package/dist/settings.js +40 -1
- package/dist/triggers.js +787 -0
- package/dist/workflow.js +18 -15
- package/package.json +1 -1
- package/prompts/kiln/review.md +13 -0
- package/workflows/review.yaml +64 -0
package/dist/serve-page.js
CHANGED
|
@@ -110,6 +110,7 @@ ${repos}
|
|
|
110
110
|
${item("/history", "history", "History", "history", board.history.length)}
|
|
111
111
|
${item("/stats", "stats", "Stats", "stats")}
|
|
112
112
|
${item("/report", "report", "Today", "report")}
|
|
113
|
+
${item("/triggers", "triggers", "Triggers", "triggers")}
|
|
113
114
|
${item("/settings", "open-settings", "Settings", "settings")}
|
|
114
115
|
</nav>`;
|
|
115
116
|
}
|
|
@@ -127,8 +128,38 @@ function mainHtml(model, page) {
|
|
|
127
128
|
if (model.section === "job" && model.detail) {
|
|
128
129
|
return `${error}<main class="main detail">${detailHtml(model.detail, model.tab ?? "overview", page.csrf)}</main>`;
|
|
129
130
|
}
|
|
131
|
+
if (model.section === "triggers") {
|
|
132
|
+
return `${error}<main class="main">${triggersHtml(model.triggers ?? [])}</main>`;
|
|
133
|
+
}
|
|
130
134
|
return `${error}<main class="main">${listHtml(model, page.csrf)}</main>`;
|
|
131
135
|
}
|
|
136
|
+
function triggersHtml(rows) {
|
|
137
|
+
const body = rows.length === 0
|
|
138
|
+
? `<p class="empty">No triggers.</p>`
|
|
139
|
+
: rows.map((row) => triggerCard(row)).join("\n");
|
|
140
|
+
return `<header class="list-head"><h2>Triggers</h2><p class="meta">The clock, Jira, and pull request activity can start work. Kiln does not post, approve, or merge.</p><p class="form-error" id="trigger-error" role="alert" hidden></p></header>${body}`;
|
|
141
|
+
}
|
|
142
|
+
function triggerCard(row) {
|
|
143
|
+
const on = row.enabled;
|
|
144
|
+
const bits = [row.kind, row.schedule, on ? "enabled" : "disabled"];
|
|
145
|
+
const next = row.nextFire ? `<p class="meta">next <time datetime="${escapeHtml(row.nextFire)}" data-time>${escapeHtml(row.nextFire)}</time></p>` : "";
|
|
146
|
+
const last = row.lastFire ? `<p class="meta">last fire <time datetime="${escapeHtml(row.lastFire)}" data-time>${escapeHtml(row.lastFire)}</time></p>` : `<p class="meta">last fire none</p>`;
|
|
147
|
+
const error = row.lastError
|
|
148
|
+
? `<p class="err">last error${row.lastErrorAt ? ` <time datetime="${escapeHtml(row.lastErrorAt)}" data-time>${escapeHtml(row.lastErrorAt)}</time>` : ""}: ${escapeHtml(row.lastError)}</p>`
|
|
149
|
+
: "";
|
|
150
|
+
const recent = row.recent.length === 0
|
|
151
|
+
? `<p class="empty">No routed events yet.</p>`
|
|
152
|
+
: `<ul class="recent">${row.recent
|
|
153
|
+
.map((item) => `<li><time datetime="${escapeHtml(item.ts)}" data-time>${escapeHtml(item.ts)}</time> <span class="ev">${escapeHtml(item.event)}</span> ${escapeHtml(item.summary)}</li>`)
|
|
154
|
+
.join("")}</ul>`;
|
|
155
|
+
return `<article class="trigger" id="trigger-${escapeHtml(row.id)}">
|
|
156
|
+
<h3>${escapeHtml(row.id)}</h3>
|
|
157
|
+
<p class="meta">${escapeHtml(bits.join(" · "))}</p>
|
|
158
|
+
${next}${last}${error}
|
|
159
|
+
<button type="button" data-trigger-toggle="${escapeHtml(row.id)}" data-enabled="${on ? "true" : "false"}">${on ? "Disable" : "Enable"}</button>
|
|
160
|
+
${recent}
|
|
161
|
+
</article>`;
|
|
162
|
+
}
|
|
132
163
|
function listHtml(model, csrf) {
|
|
133
164
|
const board = model.board;
|
|
134
165
|
if (model.section === "inbox") {
|
package/dist/serve.js
CHANGED
|
@@ -10,13 +10,14 @@ import { browse } from "./fs-browse.js";
|
|
|
10
10
|
import { branchOf, buildBoard, dayReport, jobDetail, parseSeenQuery, parseStatsWindow, readBranchCommits, readDiffStat, statsFor, unseenChanges, } from "./board-view.js";
|
|
11
11
|
import { appendEvent, eventsForJob, readEvents } from "./events.js";
|
|
12
12
|
import { INVESTIGATION_WORKFLOW } from "./findings.js";
|
|
13
|
-
import {
|
|
13
|
+
import { productionHost } from "./host.js";
|
|
14
14
|
import { intakeJobs } from "./intake-http.js";
|
|
15
15
|
import { jobBasename, packageRoot } from "./paths.js";
|
|
16
16
|
import { SANDBOX_PREFLIGHT_PREFIX } from "./sandbox.js";
|
|
17
17
|
import { renderApp, renderMessagePage, renderTextPage } from "./serve-page.js";
|
|
18
18
|
import { isPaused, pauseFactory, resumeFactory, spendFor } from "./rails.js";
|
|
19
19
|
import { readSettings, saveSettings } from "./settings.js";
|
|
20
|
+
import { triggerRows } from "./triggers.js";
|
|
20
21
|
import { listWorkflowNames } from "./workflow.js";
|
|
21
22
|
const BODY_LIMIT = 65_536;
|
|
22
23
|
const TEXT_LIMIT = 100_000;
|
|
@@ -43,6 +44,7 @@ export function startServe(options) {
|
|
|
43
44
|
let stopped = false;
|
|
44
45
|
let timer;
|
|
45
46
|
let firstTick;
|
|
47
|
+
let triggerStartup = true;
|
|
46
48
|
const shutdown = () => {
|
|
47
49
|
if (stopped)
|
|
48
50
|
return;
|
|
@@ -68,8 +70,10 @@ export function startServe(options) {
|
|
|
68
70
|
const tick = () => {
|
|
69
71
|
if (stopped)
|
|
70
72
|
return;
|
|
73
|
+
const startup = triggerStartup;
|
|
74
|
+
triggerStartup = false;
|
|
71
75
|
refreshLiveConfig(state);
|
|
72
|
-
safeTick(state.config, state.secrets,
|
|
76
|
+
safeTick(state.config, state.secrets, startup);
|
|
73
77
|
};
|
|
74
78
|
firstTick = setTimeout(tick, 0);
|
|
75
79
|
timer = setInterval(tick, options.tickSeconds * 1000);
|
|
@@ -83,9 +87,9 @@ export function loadServeCss() {
|
|
|
83
87
|
}
|
|
84
88
|
return readFileSync(path, "utf8");
|
|
85
89
|
}
|
|
86
|
-
function safeTick(config, secrets,
|
|
90
|
+
function safeTick(config, secrets, triggerStartup) {
|
|
87
91
|
try {
|
|
88
|
-
const report = runTick(config,
|
|
92
|
+
const report = runTick(config, productionHost(), { startup: false, triggerStartup });
|
|
89
93
|
if (report.startsWith(SANDBOX_PREFLIGHT_PREFIX) || report.startsWith(AUTH_PREFLIGHT_PREFIX)) {
|
|
90
94
|
const tickAt = report.indexOf("\ntick ");
|
|
91
95
|
const head = tickAt === -1 ? report : report.slice(0, tickAt);
|
|
@@ -734,6 +738,8 @@ function listSection(path) {
|
|
|
734
738
|
return "report";
|
|
735
739
|
if (path === "/settings")
|
|
736
740
|
return "settings";
|
|
741
|
+
if (path === "/triggers")
|
|
742
|
+
return "triggers";
|
|
737
743
|
return undefined;
|
|
738
744
|
}
|
|
739
745
|
function renderShell(state, css, csrf, section, extra = {}) {
|
|
@@ -751,6 +757,9 @@ function renderShell(state, css, csrf, section, extra = {}) {
|
|
|
751
757
|
...(section === "stats" && extra.window ? { stats: statsDocument(state, extra.window) } : {}),
|
|
752
758
|
...(section === "report" ? { report: reportDocument(state) } : {}),
|
|
753
759
|
...(extra.detail ? { detail: extra.detail, tab: extra.tab ?? "overview" } : {}),
|
|
760
|
+
...(section === "triggers"
|
|
761
|
+
? { triggers: triggerRows(state.config, readEvents(state.config.kilnDir, { warn: () => { } })) }
|
|
762
|
+
: {}),
|
|
754
763
|
});
|
|
755
764
|
}
|
|
756
765
|
function boardPayload(state) {
|
package/dist/settings.js
CHANGED
|
@@ -4,7 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { assertBranchName, assertCheckoutDirectory } from "./config.js";
|
|
6
6
|
import { DEFAULT_BRANCH_TEMPLATE, DEFAULT_PR_TITLE } from "./pr-names.js";
|
|
7
|
-
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_MAX_SAME_FINGERPRINT, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, SettingsWriteSchema, } from "./schemas.js";
|
|
7
|
+
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_MAX_SAME_FINGERPRINT, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, SettingsWriteSchema, TriggerConfigSchema, } from "./schemas.js";
|
|
8
|
+
import { applyTriggerToggles, effectiveTriggers, knownTrigger } from "./triggers.js";
|
|
8
9
|
const DEFAULTS = {
|
|
9
10
|
deliver: "local",
|
|
10
11
|
prTitle: DEFAULT_PR_TITLE,
|
|
@@ -48,6 +49,8 @@ export function saveSettings(configPath, body, home = homedir()) {
|
|
|
48
49
|
return fail(400, "Settings are invalid.", fieldErrors(parsed.error));
|
|
49
50
|
const { revision: _revision, ...editable } = parsed.data;
|
|
50
51
|
const fields = checkoutFields(dirname(configPath), editable, home);
|
|
52
|
+
const triggerFields = triggerToggleFields(read.raw, editable);
|
|
53
|
+
Object.assign(fields, triggerFields);
|
|
51
54
|
if (Object.keys(fields).length > 0)
|
|
52
55
|
return fail(400, "Settings are invalid.", fields);
|
|
53
56
|
const next = mergeSettings(read.raw, editable);
|
|
@@ -74,6 +77,7 @@ function project(path, revision, file) {
|
|
|
74
77
|
...(file.maxSameFingerprint !== undefined ? { maxSameFingerprint: file.maxSameFingerprint } : {}),
|
|
75
78
|
...(file.dailyDollarCap !== undefined ? { dailyDollarCap: file.dailyDollarCap } : {}),
|
|
76
79
|
...(sandbox ? { sandbox } : {}),
|
|
80
|
+
triggers: effectiveTriggers(file.triggers).map(triggerView),
|
|
77
81
|
defaults: DEFAULTS,
|
|
78
82
|
};
|
|
79
83
|
}
|
|
@@ -111,6 +115,8 @@ function mergeSettings(raw, settings) {
|
|
|
111
115
|
setOptional(next, "maxFixRounds", settings.maxFixRounds);
|
|
112
116
|
setOptional(next, "maxSameFingerprint", settings.maxSameFingerprint);
|
|
113
117
|
setOptional(next, "dailyDollarCap", settings.dailyDollarCap);
|
|
118
|
+
if (settings.triggers)
|
|
119
|
+
next.triggers = applyTriggerToggles(storedTriggers(raw.triggers), settings.triggers);
|
|
114
120
|
if (settings.sandbox) {
|
|
115
121
|
const prior = isRecord(raw.sandbox) ? { ...raw.sandbox } : {};
|
|
116
122
|
setOptional(prior, "mode", settings.sandbox.mode);
|
|
@@ -122,6 +128,39 @@ function mergeSettings(raw, settings) {
|
|
|
122
128
|
}
|
|
123
129
|
return next;
|
|
124
130
|
}
|
|
131
|
+
function triggerView(trigger) {
|
|
132
|
+
return {
|
|
133
|
+
id: trigger.id,
|
|
134
|
+
kind: trigger.kind,
|
|
135
|
+
enabled: trigger.enabled !== false,
|
|
136
|
+
...(trigger.cron ? { cron: trigger.cron } : {}),
|
|
137
|
+
...(trigger.every ? { every: trigger.every } : {}),
|
|
138
|
+
...(trigger.repo ? { repo: trigger.repo } : {}),
|
|
139
|
+
...(trigger.jql ? { jql: trigger.jql } : {}),
|
|
140
|
+
...(trigger.label ? { label: trigger.label } : {}),
|
|
141
|
+
...(trigger.status ? { status: trigger.status } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function storedTriggers(value) {
|
|
145
|
+
if (!Array.isArray(value))
|
|
146
|
+
return [];
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const item of value) {
|
|
149
|
+
const parsed = TriggerConfigSchema.safeParse(item);
|
|
150
|
+
if (parsed.success)
|
|
151
|
+
out.push(parsed.data);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function triggerToggleFields(raw, settings) {
|
|
156
|
+
const fields = {};
|
|
157
|
+
const file = storedTriggers(raw.triggers);
|
|
158
|
+
for (const toggle of settings.triggers ?? []) {
|
|
159
|
+
if (!knownTrigger(toggle.id, file))
|
|
160
|
+
fields[`triggers.${toggle.id}`] = `Unknown trigger ${toggle.id}.`;
|
|
161
|
+
}
|
|
162
|
+
return fields;
|
|
163
|
+
}
|
|
125
164
|
function checkoutFields(configDir, settings, home) {
|
|
126
165
|
const fields = {};
|
|
127
166
|
for (const [name, entry] of Object.entries(settings.repos)) {
|