Files
spoon/packages/backend/convex/github.ts
T
Gabriel Brown 2dfa97ee4f
Build and Push Next App / quality (push) Failing after 48s
Build and Push Next App / build-next (push) Has been skipped
Add agent workflows & stuff
2026-06-21 21:15:15 -05:00

215 lines
5.6 KiB
TypeScript

import { ConvexError, v } from 'convex/values';
import type { Id } from './_generated/dataModel';
import {
internalMutation,
internalQuery,
mutation,
query,
} from './_generated/server';
import { getRequiredUserId } from './model';
export const getInstallUrl = query({
args: {},
handler: () => {
const slug = process.env.GITHUB_APP_SLUG;
if (!slug) return null;
return `https://github.com/apps/${slug}/installations/new`;
},
});
export const getConnection = query({
args: {},
handler: async (ctx) => {
const userId = await getRequiredUserId(ctx);
return await ctx.db
.query('gitConnections')
.withIndex('by_user_provider', (q) =>
q.eq('userId', userId).eq('provider', 'github'),
)
.first();
},
});
export const connectInstallation = mutation({
args: { installationId: v.string() },
handler: async (ctx, { installationId }) => {
const userId = await getRequiredUserId(ctx);
const trimmedInstallationId = installationId.trim();
if (!trimmedInstallationId) {
throw new ConvexError('GitHub installation ID is required.');
}
const now = Date.now();
const existing = await ctx.db
.query('gitConnections')
.withIndex('by_user_provider', (q) =>
q.eq('userId', userId).eq('provider', 'github'),
)
.first();
const patch = {
provider: 'github' as const,
displayName: `GitHub installation ${trimmedInstallationId}`,
installationId: trimmedInstallationId,
scopes: [
'metadata:read',
'administration:write',
'contents:write',
'pull_requests:write',
],
status: 'active' as const,
updatedAt: now,
};
if (existing) {
await ctx.db.patch(existing._id, patch);
return existing._id;
}
return await ctx.db.insert('gitConnections', {
userId,
...patch,
connectedAt: now,
});
},
});
export const getConnectionForUser = internalQuery({
args: { userId: v.id('users') },
handler: async (ctx, { userId }) => {
return await ctx.db
.query('gitConnections')
.withIndex('by_user_provider', (q) =>
q.eq('userId', userId).eq('provider', 'github'),
)
.first();
},
});
export const upsertConnectionForUser = internalMutation({
args: {
userId: v.id('users'),
providerAccountId: v.optional(v.string()),
displayName: v.string(),
username: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
installationId: v.string(),
},
handler: async (ctx, args) => {
const now = Date.now();
const existing = await ctx.db
.query('gitConnections')
.withIndex('by_user_provider', (q) =>
q.eq('userId', args.userId).eq('provider', 'github'),
)
.first();
const patch = {
providerAccountId: args.providerAccountId,
displayName: args.displayName,
username: args.username,
avatarUrl: args.avatarUrl,
installationId: args.installationId,
scopes: [
'metadata:read',
'administration:write',
'contents:write',
'pull_requests:write',
'checks:write',
'statuses:write',
'issues:write',
],
status: 'active' as const,
updatedAt: now,
};
if (existing) {
await ctx.db.patch(existing._id, patch);
return existing._id;
}
return await ctx.db.insert('gitConnections', {
userId: args.userId,
provider: 'github',
...patch,
connectedAt: now,
});
},
});
export const createForkSpoonRecord = internalMutation({
args: {
ownerId: v.id('users'),
name: v.string(),
description: v.optional(v.string()),
upstreamOwner: v.string(),
upstreamRepo: v.string(),
upstreamDefaultBranch: v.string(),
upstreamUrl: v.string(),
forkOwner: v.string(),
forkRepo: v.string(),
forkDefaultBranch: v.string(),
forkUrl: v.string(),
visibility: v.union(
v.literal('public'),
v.literal('private'),
v.literal('internal'),
v.literal('unknown'),
),
connectionId: v.optional(v.id('gitConnections')),
},
handler: async (ctx, args): Promise<Id<'spoons'>> => {
const now = Date.now();
const spoonId = await ctx.db.insert('spoons', {
ownerId: args.ownerId,
name: args.name,
description: args.description,
provider: 'github',
upstreamOwner: args.upstreamOwner,
upstreamRepo: args.upstreamRepo,
upstreamDefaultBranch: args.upstreamDefaultBranch,
upstreamUrl: args.upstreamUrl,
forkOwner: args.forkOwner,
forkRepo: args.forkRepo,
forkDefaultBranch: args.forkDefaultBranch,
forkUrl: args.forkUrl,
visibility: args.visibility,
maintenanceMode: 'watch',
syncCadence: 'daily',
productionRefStrategy: 'default_branch',
status: 'active',
syncStatus: 'unknown',
connectionId: args.connectionId,
createdAt: now,
updatedAt: now,
});
await ctx.db.insert('spoonSettings', {
spoonId,
ownerId: args.ownerId,
autoRefreshEnabled: true,
autoReviewEnabled: true,
autoSyncEnabled: false,
requireAiLowRiskForSync: true,
requireCleanCompareForSync: true,
ignoredFilePatterns: [],
importantFilePatterns: [],
createdAt: now,
updatedAt: now,
});
await ctx.db.insert('syncRuns', {
spoonId,
ownerId: args.ownerId,
kind: 'manual_check',
status: 'clean',
summary: `Created GitHub fork ${args.forkOwner}/${args.forkRepo} from ${args.upstreamOwner}/${args.upstreamRepo}.`,
createdAt: now,
updatedAt: now,
});
return spoonId;
},
});