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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
push-workflow-sync.ts4.1 KB · 121 lines
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;
}