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