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.
| 91b054e | 1 | /** |
| 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 | ||
| 30 | import { and, eq } from "drizzle-orm"; | |
| 31 | import { db } from "../db"; | |
| 32 | import { workflows } from "../db/schema"; | |
| 33 | import { enqueueRun as realEnqueueRun } from "./workflow-runner"; | |
| 34 | ||
| 35 | export interface PrWorkflowSyncResult { | |
| 36 | enqueued: number; | |
| 37 | errors: string[]; | |
| 38 | } | |
| 39 | ||
| 40 | async 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 | ||
| 51 | export 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 | } |