Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

pr-workflow-sync.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

pr-workflow-sync.tsBlame107 lines · 1 contributor
91b054eccanty labs1/**
2 * Enqueue `on: pull_request` workflow runs when a PR is opened.
3 *
4 * `.gluecron/workflows/*.yml` files with `on: pull_request` (or the
5 * `[pull_request]` / `{pull_request: {...}}` shapes — all normalised by
6 * `normaliseOn()` in workflow-parser.ts) were parsed and stored into the
7 * `workflows` table on every push (see push-workflow-sync.ts), but nothing
8 * ever called `enqueueRun()` with `event: "pull_request"` — every call site
9 * only ever passed "push", "workflow_dispatch", "manual", or "schedule". A
10 * workflow declaring `on: pull_request` was silently dead: parsed, stored,
11 * never run, no error anywhere. This module is the missing wiring, called
12 * from the PR-creation handler (src/routes/pulls.tsx).
13 *
14 * Workflows are already synced into the `workflows` table by push (the
15 * `.gluecron/workflows/` dir only changes via push), so unlike
16 * push-workflow-sync.ts this module does NOT re-read the git tree — it just
17 * queries the existing table for non-disabled workflows whose `on:` events
18 * include `pull_request`.
19 *
20 * Scope: PR **open** only, mirroring the task that introduced this file.
21 * GitHub also fires `on: pull_request` for `synchronize` (new commits pushed
22 * to an open PR's head branch) and `closed` — those are NOT wired here; a
23 * follow-up would hook the same query into the PR-synchronize and close
24 * paths.
25 *
26 * Best-effort: a broken row or a failing enqueue is recorded in `errors`
27 * and skipped, never thrown — a bug here must not break PR creation.
28 */
29
30import { and, eq } from "drizzle-orm";
31import { db } from "../db";
32import { workflows } from "../db/schema";
33import { enqueueRun as realEnqueueRun } from "./workflow-runner";
34
35export interface PrWorkflowSyncResult {
36 enqueued: number;
37 errors: string[];
38}
39
40async function loadPullRequestEligibleWorkflows(
41 repositoryId: string
42): Promise<Array<{ id: string; onEvents: string }>> {
43 return db
44 .select({ id: workflows.id, onEvents: workflows.onEvents })
45 .from(workflows)
46 .where(
47 and(eq(workflows.repositoryId, repositoryId), eq(workflows.disabled, false))
48 );
49}
50
51export async function enqueuePullRequestWorkflows(
52 opts: {
53 repositoryId: string;
54 headBranch: string;
55 headSha: string;
56 triggeredBy?: string | null;
57 },
58 // Injectable for tests — avoids mock.module() on ../db or ./workflow-runner,
59 // both of which are imported by dozens of unrelated test files and would
60 // leak a global mock across the whole test run (same rationale as
61 // push-workflow-sync.ts / codeowners.ts's requiredOwnersApproved).
62 deps: {
63 enqueueRun: typeof realEnqueueRun;
64 loadWorkflows: typeof loadPullRequestEligibleWorkflows;
65 } = { enqueueRun: realEnqueueRun, loadWorkflows: loadPullRequestEligibleWorkflows }
66): Promise<PrWorkflowSyncResult> {
67 const result: PrWorkflowSyncResult = { enqueued: 0, errors: [] };
68
69 let rows: Array<{ id: string; onEvents: string }>;
70 try {
71 rows = await deps.loadWorkflows(opts.repositoryId);
72 } catch (err) {
73 result.errors.push(
74 `query: ${err instanceof Error ? err.message : String(err)}`
75 );
76 return result;
77 }
78
79 for (const row of rows) {
80 let onEvents: unknown;
81 try {
82 onEvents = JSON.parse(row.onEvents);
83 } catch {
84 result.errors.push(`${row.id}: malformed onEvents JSON`);
85 continue;
86 }
87 if (!Array.isArray(onEvents) || !onEvents.includes("pull_request")) continue;
88
89 try {
90 await deps.enqueueRun({
91 workflowId: row.id,
92 repositoryId: opts.repositoryId,
93 event: "pull_request",
94 ref: opts.headBranch,
95 commitSha: opts.headSha,
96 triggeredBy: opts.triggeredBy ?? null,
97 });
98 result.enqueued += 1;
99 } catch (err) {
100 result.errors.push(
101 `enqueue ${row.id}: ${err instanceof Error ? err.message : String(err)}`
102 );
103 }
104 }
105
106 return result;
107}