Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

push-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.

push-workflow-sync.tsBlame121 lines · 1 contributor
61897c4ccanty labs1/**
2 * Sync `.gluecron/workflows/*.yml` from a push into the `workflows` table,
3 * then enqueue a run for every non-disabled workflow whose `on:` triggers
4 * include `push`.
5 *
6 * This is the missing half of "push-triggered CI": nothing previously
7 * wrote rows into `workflows` on push, so the table only ever got
8 * populated by manual/test insertion, and `on: push` triggers never
9 * actually fired automatically — only `workflow_dispatch` (manual/MCP),
10 * PR slash commands, and `on: schedule` (autopilot tick) worked. Discovered
11 * 2026-07-15 while auditing why a sibling platform's Gluecron migration
12 * mirror never produced a running CI pipeline.
13 *
14 * Best-effort per file: a broken workflow YAML is recorded in `errors`
15 * and skipped, never thrown — a bug here must not break the push itself.
16 */
17
18import { db } from "../db";
19import { workflows } from "../db/schema";
20import { getTree as realGetTree, getBlob as realGetBlob } from "../git/repository";
21import { parseWorkflow } from "./workflow-parser";
22import { enqueueRun as realEnqueueRun } from "./workflow-runner";
23
24const WORKFLOWS_DIR = ".gluecron/workflows";
25
26export interface PushWorkflowSyncResult {
27 synced: number;
28 enqueued: number;
29 errors: string[];
30}
31
32export async function syncAndEnqueuePushWorkflows(
33 opts: {
34 owner: string;
35 repo: string;
36 repositoryId: string;
37 branch: string;
38 commitSha: string;
39 triggeredBy?: string | null;
40 },
41 // Injectable for tests — avoids mock.module() on ../git/repository and
42 // ./workflow-runner, both of which are imported by dozens of unrelated
43 // test files and would leak a global mock across the whole test run.
44 deps: {
45 getTree: typeof realGetTree;
46 getBlob: typeof realGetBlob;
47 enqueueRun: typeof realEnqueueRun;
48 } = { getTree: realGetTree, getBlob: realGetBlob, enqueueRun: realEnqueueRun }
49): Promise<PushWorkflowSyncResult> {
50 const result: PushWorkflowSyncResult = { synced: 0, enqueued: 0, errors: [] };
51
52 // getTree never throws — it returns [] for a missing path/exec failure.
53 const entries = await deps.getTree(opts.owner, opts.repo, opts.commitSha, WORKFLOWS_DIR);
54 const ymlFiles = entries.filter(
55 (e) => e.type === "blob" && /\.ya?ml$/i.test(e.name)
56 );
57
58 for (const file of ymlFiles) {
59 const path = `${WORKFLOWS_DIR}/${file.name}`;
60 try {
61 const blob = await deps.getBlob(opts.owner, opts.repo, opts.commitSha, path);
62 if (!blob || blob.isBinary) continue;
63
64 const parseResult = parseWorkflow(blob.content);
65 if (!parseResult.ok) {
66 result.errors.push(`${file.name}: ${parseResult.error}`);
67 continue;
68 }
69 const { workflow } = parseResult;
70
71 const [row] = await db
72 .insert(workflows)
73 .values({
74 repositoryId: opts.repositoryId,
75 name: workflow.name || file.name,
76 path,
77 yaml: blob.content,
78 parsed: JSON.stringify(workflow),
79 onEvents: JSON.stringify(workflow.on),
80 })
81 .onConflictDoUpdate({
82 target: [workflows.repositoryId, workflows.path],
83 set: {
84 name: workflow.name || file.name,
85 yaml: blob.content,
86 parsed: JSON.stringify(workflow),
87 onEvents: JSON.stringify(workflow.on),
88 updatedAt: new Date(),
89 },
90 })
91 .returning({ id: workflows.id, disabled: workflows.disabled });
92
93 if (!row) continue;
94 result.synced += 1;
95
96 if (!row.disabled && workflow.on.includes("push")) {
97 try {
98 await deps.enqueueRun({
99 workflowId: row.id,
100 repositoryId: opts.repositoryId,
101 event: "push",
102 ref: opts.branch,
103 commitSha: opts.commitSha,
104 triggeredBy: opts.triggeredBy ?? null,
105 });
106 result.enqueued += 1;
107 } catch (err) {
108 result.errors.push(
109 `enqueue ${file.name}: ${err instanceof Error ? err.message : String(err)}`
110 );
111 }
112 }
113 } catch (err) {
114 result.errors.push(
115 `${file.name}: ${err instanceof Error ? err.message : String(err)}`
116 );
117 }
118 }
119
120 return result;
121}