Swadhin Biswas feat(api): add runner, analytics, packages, and security services · 3/6/2026
28f7954
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
import type { APIRoute } from 'astro';
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { z } from 'zod';
import { getDatabase, schema } from "@/db";
import { success, unauthorized, serverError, parseBody, notFound } from '@/lib/api';
import { getUserFromRequest } from '@/lib/auth';
import { eq, and, desc } from "drizzle-orm";
import crypto from 'node:crypto';
import { withErrorHandler } from "@/lib/errors";
import { logger } from "@/lib/logger";
import { canWriteRepo } from "@/lib/permissions";
const triggerSchema = z.object({
repositoryId: z.string()
});
export const POST: APIRoute = withErrorHandler(async ({ request }) => {
const tokenPayload = await getUserFromRequest(request);
if (!tokenPayload) return unauthorized();
const parsed = await parseBody(request, triggerSchema);
if ('error' in parsed) return parsed.error;
const { repositoryId } = parsed.data;
const db = getDatabase() as NodePgDatabase<typeof schema>;
// Fetch Repo for Default Branch
const repo = await db.query.repositories.findFirst({
where: eq(schema.repositories.id, repositoryId)
});
if (!repo) return notFound("Repository not found");
if (!(await canWriteRepo(tokenPayload.userId, repo, { isAdmin: tokenPayload.isAdmin }))) {
return unauthorized("Insufficient permissions");
}
// Reuse existing workflow definition if present.
let workflow = await db.query.workflows.findFirst({
where: and(
eq(schema.workflows.repositoryId, repositoryId),
eq(schema.workflows.path, ".github/workflows/test.yml")
)
});
if (!workflow) {
const workflowId = crypto.randomUUID();
await db.insert(schema.workflows).values({
id: workflowId,
repositoryId,
name: "Test Workflow",
path: ".github/workflows/test.yml",
state: "active"
});
workflow = await db.query.workflows.findFirst({
where: eq(schema.workflows.id, workflowId)
});
}
if (!workflow) return serverError("Failed to initialize workflow");
const latestRun = await db.query.workflowRuns.findFirst({
where: eq(schema.workflowRuns.workflowId, workflow.id),
orderBy: [desc(schema.workflowRuns.runNumber)]
});
const nextRunNumber = (latestRun?.runNumber ?? 0) + 1;
// Create Run
const runId = crypto.randomUUID();
await db.insert(schema.workflowRuns).values({
id: runId,
workflowId: workflow.id,
repositoryId,
runNumber: nextRunNumber,
name: "Manual Test Run",
status: "queued",
event: "workflow_dispatch",
headSha: "HEAD",
headBranch: repo.defaultBranch,
triggeredById: tokenPayload.userId
});
// Create Job
const jobId = crypto.randomUUID();
await db.insert(schema.workflowJobs).values({
id: jobId,
runId,
name: "test-job",
status: "queued"
});
// Create Step
await db.insert(schema.workflowSteps).values({
id: crypto.randomUUID(),
jobId,
number: 1,
name: "Run Echo",
run: "echo 'Hello from OpenCodeHub Self-Hosted Runner!' && uptime",
status: "queued"
});
logger.info({ userId: tokenPayload.userId, repositoryId, runId }, "Workflow run triggered manually");
return success({ message: "Test job queued", jobId });
});