Files

179 lines
5.4 KiB
TypeScript

import { v } from "convex/values"
import { api, internal } from "./_generated/api"
import { internalMutation, internalQuery, mutation, query } from "./_generated/server"
export function normalizeBuildKey(resolvedSourceSha: string, targetEnv: string, platformRoot?: string): string {
const p = (platformRoot ?? "").trim().replace(/\//g, "_")
const t = targetEnv.replace(/\//g, "_")
if (!p) return `${resolvedSourceSha}_${t}`
return `${resolvedSourceSha}_${p}_${t}`
}
export const getById = query({
args: { id: v.id("repoBuilds") },
handler: async (ctx, args) => ctx.db.get(args.id),
})
export const getByIdInternal = internalQuery({
args: { id: v.id("repoBuilds") },
handler: async (ctx, args) => ctx.db.get(args.id),
})
export const getByBuildKey = query({
args: { buildKey: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("repoBuilds")
.withIndex("by_buildKey", q => q.eq("buildKey", args.buildKey))
.first()
},
})
export const ensureBuild = mutation({
args: {
owner: v.string(),
repo: v.string(),
ref: v.string(),
resolvedSourceSha: v.string(),
targetEnv: v.string(),
platformRoot: v.optional(v.string()),
},
handler: async (ctx, args) => {
const platformRoot = args.platformRoot ?? ""
const buildKey = normalizeBuildKey(args.resolvedSourceSha, args.targetEnv, platformRoot)
const existing = await ctx.db
.query("repoBuilds")
.withIndex("by_buildKey", q => q.eq("buildKey", buildKey))
.first()
if (existing) {
if (existing.status === "failed") {
return { buildId: existing._id, status: "failed" as const, reused: true as const }
}
if (existing.status === "succeeded") {
return { buildId: existing._id, status: "succeeded" as const, reused: true as const }
}
if (existing.status === "queued" || existing.status === "running") {
return { buildId: existing._id, status: existing.status, reused: true as const }
}
}
const now = Date.now()
const buildId = await ctx.db.insert("repoBuilds", {
owner: args.owner,
repo: args.repo,
ref: args.ref,
resolvedSourceSha: args.resolvedSourceSha,
platformRoot: platformRoot || undefined,
targetEnv: args.targetEnv,
buildKey,
status: "queued",
startedAt: now,
updatedAt: now,
})
await ctx.scheduler.runAfter(0, api.actions.dispatchRepoBuild, { buildId })
return { buildId, status: "queued" as const, reused: false as const }
},
})
export const patchFromWebhook = internalMutation({
args: {
buildId: v.id("repoBuilds"),
status: v.union(v.literal("running"), v.literal("succeeded"), v.literal("failed")),
githubRunId: v.optional(v.number()),
r2ObjectKey: v.optional(v.string()),
errorSummary: v.optional(v.string()),
},
handler: async (ctx, args) => {
const patch: Record<string, unknown> = {
status: args.status,
updatedAt: Date.now(),
}
if (args.githubRunId !== undefined) patch.githubRunId = args.githubRunId
if (args.r2ObjectKey !== undefined) patch.r2ObjectKey = args.r2ObjectKey
if (args.errorSummary !== undefined) patch.errorSummary = args.errorSummary
if (args.status === "succeeded" || args.status === "failed") {
patch.completedAt = Date.now()
patch.ciProgressStep = undefined
patch.ciProgressTotal = undefined
patch.ciProgressLabel = undefined
}
await ctx.db.patch(args.buildId, patch)
},
})
export const patchCiProgress = internalMutation({
args: {
buildId: v.id("repoBuilds"),
stepIndex: v.number(),
stepTotal: v.number(),
label: v.string(),
},
handler: async (ctx, args) => {
const doc = await ctx.db.get(args.buildId)
if (!doc) {
return
}
if (doc.status === "succeeded" || doc.status === "failed") {
return
}
if (args.stepTotal < 1 || args.stepIndex < 1 || args.stepIndex > args.stepTotal) {
return
}
await ctx.db.patch(args.buildId, {
ciProgressStep: args.stepIndex,
ciProgressTotal: args.stepTotal,
ciProgressLabel: args.label,
updatedAt: Date.now(),
})
},
})
export const logBuildDispatchError = internalMutation({
args: { buildId: v.id("repoBuilds"), message: v.string() },
handler: async (ctx, args) => {
await ctx.db.patch(args.buildId, {
status: "failed",
errorSummary: args.message,
updatedAt: Date.now(),
completedAt: Date.now(),
ciProgressStep: undefined,
ciProgressTotal: undefined,
ciProgressLabel: undefined,
})
},
})
/** Re-queue a failed build (same Convex doc + webhook id) after dispatch or CI flakiness. */
export const retryBuild = mutation({
args: { buildId: v.id("repoBuilds") },
handler: async (ctx, args) => {
const doc = await ctx.db.get(args.buildId)
if (!doc) {
throw new Error("Build not found")
}
if (doc.status !== "failed") {
throw new Error(`Cannot retry unless status is failed (got ${doc.status})`)
}
const now = Date.now()
await ctx.db.replace(args.buildId, {
owner: doc.owner,
repo: doc.repo,
ref: doc.ref,
resolvedSourceSha: doc.resolvedSourceSha,
platformRoot: doc.platformRoot,
targetEnv: doc.targetEnv,
buildKey: doc.buildKey,
status: "queued",
startedAt: now,
updatedAt: now,
})
await ctx.scheduler.runAfter(0, api.actions.dispatchRepoBuild, { buildId: args.buildId })
return { ok: true as const }
},
})