Merge branch 'puter/issue-5-gitea-lifecycle' into puter/issue-14-integration
# Conflicts: # bun.lock # packages/agents/package.json # packages/agents/src/agents/project-manager.ts # packages/backend/convex/agentWorkspace.ts
This commit is contained in:
@@ -278,3 +278,111 @@ export const setStatus = mutation({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const lifecycleStatus = v.union(
|
||||
v.literal("no_changes"),
|
||||
v.literal("committed"),
|
||||
v.literal("pushed"),
|
||||
v.literal("pull_request_open"),
|
||||
v.literal("failed")
|
||||
);
|
||||
|
||||
const pullRequestMetadata = v.object({
|
||||
baseBranch: v.string(),
|
||||
branch: v.string(),
|
||||
number: v.number(),
|
||||
status: v.union(v.literal("open"), v.literal("closed"), v.literal("merged")),
|
||||
url: v.string(),
|
||||
});
|
||||
|
||||
export const recordGiteaLifecycle = mutation({
|
||||
args: {
|
||||
baseBranch: v.string(),
|
||||
branch: v.string(),
|
||||
commitSha: v.optional(v.string()),
|
||||
error: v.optional(v.string()),
|
||||
issueId: v.id("projectIssues"),
|
||||
pullRequest: v.optional(pullRequestMetadata),
|
||||
status: lifecycleStatus,
|
||||
token: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
requireAgent(args.token);
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
if (!issue) {
|
||||
throw new ConvexError("Issue not found");
|
||||
}
|
||||
const run = await ctx.db
|
||||
.query("projectWorkRuns")
|
||||
.withIndex("by_issue", (q) => q.eq("issueId", issue._id))
|
||||
.unique();
|
||||
if (!run) {
|
||||
throw new ConvexError("Agent work run not found");
|
||||
}
|
||||
|
||||
const events = await ctx.db
|
||||
.query("projectEvents")
|
||||
.withIndex("by_issue_and_createdAt", (q) => q.eq("issueId", issue._id))
|
||||
.order("desc")
|
||||
.take(100);
|
||||
const existing = events.find((event) => {
|
||||
if (!event.kind.startsWith("gitea.")) {
|
||||
return false;
|
||||
}
|
||||
const data = event.data as {
|
||||
branch?: unknown;
|
||||
commitSha?: unknown;
|
||||
status?: unknown;
|
||||
};
|
||||
return (
|
||||
data.branch === args.branch &&
|
||||
data.commitSha === args.commitSha &&
|
||||
data.status === args.status
|
||||
);
|
||||
});
|
||||
if (existing) {
|
||||
return existing.data;
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
const data = {
|
||||
baseBranch: args.baseBranch,
|
||||
branch: args.branch,
|
||||
...(args.commitSha === undefined ? {} : { commitSha: args.commitSha }),
|
||||
...(args.error === undefined ? {} : { error: args.error.slice(0, 2000) }),
|
||||
...(args.pullRequest === undefined
|
||||
? {}
|
||||
: { pullRequest: args.pullRequest }),
|
||||
status: args.status,
|
||||
};
|
||||
const artifact = await ctx.db
|
||||
.query("projectArtifacts")
|
||||
.withIndex("by_project_and_path", (q) =>
|
||||
q.eq("projectId", issue.projectId).eq("path", "artifacts.md")
|
||||
)
|
||||
.unique();
|
||||
if (artifact) {
|
||||
const pullRequestLine = args.pullRequest
|
||||
? `- Pull request: [#${args.pullRequest.number}](${args.pullRequest.url}) (${args.pullRequest.status})\n`
|
||||
: "";
|
||||
const commitLine = args.commitSha ? `- Commit: ${args.commitSha}\n` : "";
|
||||
await ctx.db.patch("projectArtifacts", artifact._id, {
|
||||
content: `${artifact.content}\n## Git lifecycle\n\n- Status: ${args.status}\n- Branch: ${args.branch}\n- Base branch: ${args.baseBranch}\n${commitLine}${pullRequestLine}${args.error ? `- Error: ${args.error.slice(0, 1000)}\n` : ""}`,
|
||||
revision: artifact.revision + 1,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
}
|
||||
await ctx.db.insert("projectEvents", {
|
||||
createdAt: timestamp,
|
||||
data,
|
||||
issueId: issue._id,
|
||||
kind:
|
||||
args.pullRequest === undefined
|
||||
? "gitea.lifecycle.updated"
|
||||
: "gitea.pull_request.created",
|
||||
projectId: issue.projectId,
|
||||
runId: run._id,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -223,4 +223,63 @@ describe("projectEvents", () => {
|
||||
const events = await eventsForProject(t, projectId);
|
||||
expect(events.some((e) => e.kind === "agent.completed")).toBe(false);
|
||||
});
|
||||
|
||||
test("Gitea lifecycle persists PR metadata in an event and artifact", async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const projectId = await createTestProject(t);
|
||||
const issueId = await t
|
||||
.withIdentity(identityA)
|
||||
.mutation(api.projectIssues.create, {
|
||||
body: "A representative issue body long enough to pass validation",
|
||||
projectId,
|
||||
title: "Representative issue",
|
||||
});
|
||||
await t.mutation(api.agentWorkspace.ensureRun, {
|
||||
daemonId: "daemon-1",
|
||||
issueId,
|
||||
token: FLUE_DB_TOKEN,
|
||||
});
|
||||
|
||||
await t.mutation(api.agentWorkspace.recordGiteaLifecycle, {
|
||||
baseBranch: "main",
|
||||
branch: "work/issue-1",
|
||||
commitSha: "abc123",
|
||||
issueId,
|
||||
pullRequest: {
|
||||
baseBranch: "main",
|
||||
branch: "work/issue-1",
|
||||
number: 5,
|
||||
status: "open",
|
||||
url: "https://git.openputer.com/puter/zopu-code/pulls/5",
|
||||
},
|
||||
status: "pull_request_open",
|
||||
token: FLUE_DB_TOKEN,
|
||||
});
|
||||
|
||||
const events = await eventsForProject(t, projectId);
|
||||
const event = events.find(
|
||||
(item) => item.kind === "gitea.pull_request.created"
|
||||
);
|
||||
expect(event?.data).toMatchObject({
|
||||
branch: "work/issue-1",
|
||||
commitSha: "abc123",
|
||||
pullRequest: {
|
||||
number: 5,
|
||||
status: "open",
|
||||
},
|
||||
status: "pull_request_open",
|
||||
});
|
||||
|
||||
const artifacts = await t.query((ctx) =>
|
||||
ctx.db
|
||||
.query("projectArtifacts")
|
||||
.withIndex("by_project_and_path", (q) =>
|
||||
q.eq("projectId", projectId).eq("path", "artifacts.md")
|
||||
)
|
||||
.unique()
|
||||
);
|
||||
expect(artifacts?.content).toContain(
|
||||
"https://git.openputer.com/puter/zopu-code/pulls/5"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,3 +191,21 @@ export const list = query({
|
||||
.take(100);
|
||||
},
|
||||
});
|
||||
|
||||
export const events = query({
|
||||
args: { projectId: v.id("projects") },
|
||||
handler: async (ctx, args) => {
|
||||
try {
|
||||
await requireProjectMember(ctx, args.projectId);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return await ctx.db
|
||||
.query("projectEvents")
|
||||
.withIndex("by_project_and_createdAt", (q) =>
|
||||
q.eq("projectId", args.projectId)
|
||||
)
|
||||
.order("desc")
|
||||
.take(100);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user