make webhook

This commit is contained in:
2026-07-13 18:50:38 +02:00
parent 2f128550fc
commit 40c822d3bf
67 changed files with 6286 additions and 2053 deletions
@@ -0,0 +1,211 @@
import { randomUUID } from "node:crypto";
import {
type DatabaseContext,
type Job,
mapJob,
type NewJob,
now,
type Row,
} from "../model.js";
export class JobRepository {
constructor(private readonly context: DatabaseContext) {}
createCommand(input: NewJob): { job: Job; created: boolean } {
return this.context.transaction(() => {
const existing = this.byTriggerKey(input.triggerKey);
if (existing) return { job: existing, created: false };
return { job: this.insert(input), created: true };
});
}
createLabel(input: NewJob): Job | undefined {
if (!input.triggerLabel)
throw new Error("Label job requires triggerLabel");
const label = input.triggerLabel;
return this.context.transaction(() => {
const claim = this.context.db
.prepare(`
SELECT claimed FROM label_claims
WHERE repository_id = $repositoryId AND issue_number = $issueNumber AND label = $label
`)
.get({
$repositoryId: input.repositoryId,
$issueNumber: input.issueNumber,
$label: label,
}) as Row | undefined;
if (Number(claim?.claimed || 0) === 1) return undefined;
const job = this.insert({
...input,
triggerKey: `label:${input.repositoryId}:${input.issueNumber}:${label}:${randomUUID()}`,
});
this.context.db
.prepare(`
INSERT INTO label_claims(repository_id, issue_number, label, claimed, job_id, updated_at)
VALUES ($repositoryId, $issueNumber, $label, 1, $jobId, $now)
ON CONFLICT(repository_id, issue_number, label)
DO UPDATE SET claimed = 1, job_id = excluded.job_id, updated_at = excluded.updated_at
`)
.run({
$repositoryId: input.repositoryId,
$issueNumber: input.issueNumber,
$label: label,
$jobId: job.id,
$now: now(),
});
return job;
});
}
releaseLabel(
repositoryId: number,
issueNumber: number,
label: string,
): void {
this.context.db
.prepare(`
INSERT INTO label_claims(repository_id, issue_number, label, claimed, job_id, updated_at)
VALUES ($repositoryId, $issueNumber, $label, 0, NULL, $now)
ON CONFLICT(repository_id, issue_number, label)
DO UPDATE SET claimed = 0, job_id = NULL, updated_at = excluded.updated_at
`)
.run({
$repositoryId: repositoryId,
$issueNumber: issueNumber,
$label: label,
$now: now(),
});
}
active(repositoryId: number, issueNumber: number): Job | undefined {
const row = this.context.db
.prepare(`
SELECT * FROM jobs
WHERE repository_id = $repositoryId AND issue_number = $issueNumber
AND state IN ('admitted', 'queued', 'running', 'publishing')
ORDER BY created_at DESC LIMIT 1
`)
.get({ $repositoryId: repositoryId, $issueNumber: issueNumber }) as
| Row
| undefined;
return row ? mapJob(row) : undefined;
}
latest(repositoryId: number, issueNumber: number): Job | undefined {
const row = this.context.db
.prepare(`
SELECT * FROM jobs WHERE repository_id = $repositoryId AND issue_number = $issueNumber
ORDER BY created_at DESC LIMIT 1
`)
.get({ $repositoryId: repositoryId, $issueNumber: issueNumber }) as
| Row
| undefined;
return row ? mapJob(row) : undefined;
}
control(
triggerKey: string,
action: "cancel" | "status",
repositoryId: number,
issueNumber: number,
): Job | undefined {
return this.context.transaction(() => {
const existing = this.context.db
.prepare(
"SELECT target_job_id FROM command_receipts WHERE trigger_key = $triggerKey",
)
.get({ $triggerKey: triggerKey }) as Row | undefined;
if (existing)
return existing.target_job_id === null
? undefined
: this.get(String(existing.target_job_id));
const target =
action === "cancel"
? this.active(repositoryId, issueNumber)
: this.latest(repositoryId, issueNumber);
this.context.db
.prepare(`
INSERT INTO command_receipts(trigger_key, action, repository_id, issue_number, target_job_id, created_at)
VALUES ($triggerKey, $action, $repositoryId, $issueNumber, $targetJobId, $now)
`)
.run({
$triggerKey: triggerKey,
$action: action,
$repositoryId: repositoryId,
$issueNumber: issueNumber,
$targetJobId: target?.id || null,
$now: now(),
});
if (action === "cancel" && target) {
this.context.db
.prepare(`
UPDATE jobs SET cancel_requested = 1,
state = CASE WHEN state IN ('admitted', 'queued') THEN 'cancelled' ELSE state END,
updated_at = $now WHERE id = $id
`)
.run({ $id: target.id, $now: now() });
this.context.audit(
target.id,
"job.cancel-requested",
triggerKey,
);
return this.get(target.id);
}
return target;
});
}
get(id: string): Job | undefined {
const row = this.context.db
.prepare("SELECT * FROM jobs WHERE id = $id")
.get({ $id: id }) as Row | undefined;
return row ? mapJob(row) : undefined;
}
private insert(input: NewJob): Job {
const id = randomUUID();
const time = now();
this.context.db
.prepare(`
INSERT INTO jobs
(id, repository_id, issue_number, mode, trigger_kind, trigger_key, trigger_label,
actor_id, actor_login, instruction, state, created_at, updated_at)
VALUES ($id, $repositoryId, $issueNumber, $mode, $triggerKind, $triggerKey, $triggerLabel,
$actorId, $actorLogin, $instruction, 'admitted', $now, $now)
`)
.run({
$id: id,
$repositoryId: input.repositoryId,
$issueNumber: input.issueNumber,
$mode: input.mode,
$triggerKind: input.triggerKind,
$triggerKey: input.triggerKey,
$triggerLabel: input.triggerLabel || null,
$actorId: input.actorId,
$actorLogin: input.actorLogin,
$instruction: input.instruction || "",
$now: time,
});
this.context.db
.prepare(`
INSERT INTO outbox(id, job_id, kind, status, available_at, created_at, updated_at)
VALUES ($id, $jobId, 'claim', 'pending', $now, $now, $now)
`)
.run({ $id: randomUUID(), $jobId: id, $now: time });
this.context.audit(
id,
"job.admitted",
`${input.triggerKind}:${input.actorLogin}`,
);
const job = this.get(id);
if (!job) throw new Error(`Inserted job ${id} was not found`);
return job;
}
private byTriggerKey(triggerKey: string): Job | undefined {
const row = this.context.db
.prepare("SELECT * FROM jobs WHERE trigger_key = $key")
.get({ $key: triggerKey }) as Row | undefined;
return row ? mapJob(row) : undefined;
}
}