-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add updatePRTask with git auth and env vars #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sweetmantech
merged 4 commits into
main
from
sweetmantech/myc-4425-fix-updateprtask-add-git-auth-env-vars-and-error-handling
Mar 6, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1c36999
feat: add updatePRTask with git auth and env vars
sweetmantech 1c111bb
merge: sync with main after PR #41 merged
sweetmantech 3203291
refactor: simplify updatePRTask payload from prs array to single repo
sweetmantech 0974662
refactor: replace logger.log with logStep in updatePRTask
sweetmantech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { z } from "zod"; | ||
|
|
||
| export const updatePRPayloadSchema = z.object({ | ||
| feedback: z.string().min(1, "feedback is required"), | ||
| snapshotId: z.string().min(1, "snapshotId is required"), | ||
| branch: z.string().min(1, "branch is required"), | ||
| repo: z.string().min(1, "repo is required"), | ||
| callbackThreadId: z.string().min(1, "callbackThreadId is required"), | ||
| }); | ||
|
|
||
| export type UpdatePRPayload = z.infer<typeof updatePRPayloadSchema>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
|
|
||
| const mockRun = vi.fn(); | ||
| vi.mock("@trigger.dev/sdk/v3", () => ({ | ||
| logger: { log: vi.fn(), error: vi.fn() }, | ||
| metadata: { set: vi.fn(), append: vi.fn() }, | ||
| tags: { add: vi.fn() }, | ||
| schemaTask: (config: { run: unknown }) => { | ||
| mockRun.mockImplementation(config.run as (...args: unknown[]) => unknown); | ||
| return config; | ||
| }, | ||
| })); | ||
|
|
||
| const mockSandboxCreate = vi.fn(); | ||
| const mockSandboxStop = vi.fn(); | ||
| const mockSandboxSnapshot = vi.fn().mockResolvedValue({ snapshotId: "snap_new" }); | ||
|
|
||
| vi.mock("@vercel/sandbox", () => ({ | ||
| Sandbox: { | ||
| create: (...args: unknown[]) => mockSandboxCreate(...args), | ||
| }, | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/getVercelSandboxCredentials", () => ({ | ||
| getVercelSandboxCredentials: vi.fn().mockReturnValue({ | ||
| token: "tok", teamId: "team", projectId: "proj", | ||
| }), | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/installOpenClaw", () => ({ | ||
| installOpenClaw: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/setupOpenClaw", () => ({ | ||
| setupOpenClaw: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/runOpenClawAgent", () => ({ | ||
| runOpenClawAgent: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "done", stderr: "" }), | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/notifyCodingAgentCallback", () => ({ | ||
| notifyCodingAgentCallback: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/logStep", () => ({ | ||
| logStep: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/configureGitAuth", () => ({ | ||
| configureGitAuth: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("../../sandboxes/getSandboxEnv", () => ({ | ||
| getSandboxEnv: vi.fn().mockReturnValue({ | ||
| RECOUP_API_KEY: "test-key", | ||
| RECOUP_ACCOUNT_ID: "coding-agent", | ||
| GITHUB_TOKEN: "test-gh-token", | ||
| }), | ||
| })); | ||
|
|
||
| // Import after mocks | ||
| await import("../updatePRTask"); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockSandboxCreate.mockResolvedValue({ | ||
| sandboxId: "sbx-456", | ||
| stop: mockSandboxStop, | ||
| snapshot: mockSandboxSnapshot, | ||
| }); | ||
| }); | ||
|
|
||
| describe("updatePRTask", () => { | ||
| const basePayload = { | ||
| feedback: "Make the button blue instead", | ||
| snapshotId: "snap_old", | ||
| branch: "agent/fix-bug-123", | ||
| repo: "recoupable/api", | ||
| callbackThreadId: "slack:C123:123.456", | ||
| }; | ||
|
|
||
| it("resumes sandbox from snapshot", async () => { | ||
| await mockRun(basePayload); | ||
|
|
||
| expect(mockSandboxCreate).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| source: { type: "snapshot", snapshotId: "snap_old" }, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("runs OpenClaw agent with feedback prompt", async () => { | ||
| const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); | ||
|
|
||
| await mockRun(basePayload); | ||
|
|
||
| expect(runOpenClawAgent).toHaveBeenCalledWith( | ||
| expect.anything(), | ||
| expect.objectContaining({ | ||
| message: expect.stringContaining("Make the button blue instead"), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("delegates push to agent instead of using runGitCommand", async () => { | ||
| const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); | ||
|
|
||
| await mockRun(basePayload); | ||
|
|
||
| // Should be called twice: once for feedback, once for push | ||
| expect(runOpenClawAgent).toHaveBeenCalledTimes(2); | ||
| expect(runOpenClawAgent).toHaveBeenCalledWith( | ||
| expect.anything(), | ||
| expect.objectContaining({ | ||
| label: "Push feedback changes", | ||
| message: expect.stringContaining("git push"), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("notifies callback with updated status and new snapshot", async () => { | ||
| const { notifyCodingAgentCallback } = await import("../../sandboxes/notifyCodingAgentCallback"); | ||
|
|
||
| await mockRun(basePayload); | ||
|
|
||
| expect(notifyCodingAgentCallback).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| threadId: "slack:C123:123.456", | ||
| status: "updated", | ||
| snapshotId: "snap_new", | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("stops sandbox in finally block", async () => { | ||
| await mockRun(basePayload); | ||
| expect(mockSandboxStop).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it("configures git auth before running agent", async () => { | ||
| const { configureGitAuth } = await import("../../sandboxes/configureGitAuth"); | ||
|
|
||
| await mockRun(basePayload); | ||
|
|
||
| expect(configureGitAuth).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it("passes sandbox env to both agent calls", async () => { | ||
| const { runOpenClawAgent } = await import("../../sandboxes/runOpenClawAgent"); | ||
| const { getSandboxEnv } = await import("../../sandboxes/getSandboxEnv"); | ||
|
|
||
| await mockRun(basePayload); | ||
|
|
||
| expect(getSandboxEnv).toHaveBeenCalledWith("coding-agent"); | ||
|
|
||
| const expectedEnv = { | ||
| RECOUP_API_KEY: "test-key", | ||
| RECOUP_ACCOUNT_ID: "coding-agent", | ||
| GITHUB_TOKEN: "test-gh-token", | ||
| }; | ||
|
|
||
| // Both agent calls should receive env | ||
| const calls = vi.mocked(runOpenClawAgent).mock.calls; | ||
| expect(calls[0][1]).toEqual(expect.objectContaining({ env: expectedEnv })); | ||
| expect(calls[1][1]).toEqual(expect.objectContaining({ env: expectedEnv })); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { metadata, schemaTask } from "@trigger.dev/sdk/v3"; | ||
| import { Sandbox } from "@vercel/sandbox"; | ||
| import { getVercelSandboxCredentials } from "../sandboxes/getVercelSandboxCredentials"; | ||
| import { installOpenClaw } from "../sandboxes/installOpenClaw"; | ||
| import { setupOpenClaw } from "../sandboxes/setupOpenClaw"; | ||
| import { runOpenClawAgent } from "../sandboxes/runOpenClawAgent"; | ||
| import { notifyCodingAgentCallback } from "../sandboxes/notifyCodingAgentCallback"; | ||
| import { logStep } from "../sandboxes/logStep"; | ||
| import { configureGitAuth } from "../sandboxes/configureGitAuth"; | ||
| import { getSandboxEnv } from "../sandboxes/getSandboxEnv"; | ||
| import { updatePRPayloadSchema } from "../schemas/updatePRSchema"; | ||
|
|
||
| const CODING_AGENT_ACCOUNT_ID = "coding-agent"; | ||
|
|
||
| /** | ||
| * Background task that resumes a sandbox from a snapshot, applies feedback | ||
| * via the AI agent, and delegates push of updates to the agent. | ||
| */ | ||
| export const updatePRTask = schemaTask({ | ||
| id: "update-pr", | ||
| schema: updatePRPayloadSchema, | ||
| maxDuration: 60 * 15, | ||
| retry: { | ||
| maxAttempts: 0, | ||
| }, | ||
| run: async (payload) => { | ||
| const { feedback, snapshotId, branch, repo, callbackThreadId } = payload; | ||
| const { token, teamId, projectId } = getVercelSandboxCredentials(); | ||
|
|
||
| logStep("Resuming sandbox from snapshot"); | ||
|
|
||
| const sandbox = await Sandbox.create({ | ||
| token, | ||
| teamId, | ||
| projectId, | ||
| source: { type: "snapshot", snapshotId }, | ||
| timeoutMs: 30 * 60 * 1000, | ||
| }); | ||
|
|
||
| logStep("Sandbox resumed", false, { sandboxId: sandbox.sandboxId, snapshotId }); | ||
|
|
||
| try { | ||
| logStep("Ensuring OpenClaw is running"); | ||
| await installOpenClaw(sandbox); | ||
| await setupOpenClaw(sandbox, CODING_AGENT_ACCOUNT_ID); | ||
| await configureGitAuth(sandbox); | ||
|
|
||
| const env = getSandboxEnv(CODING_AGENT_ACCOUNT_ID); | ||
|
|
||
| logStep("Running AI agent with feedback"); | ||
| await runOpenClawAgent(sandbox, { | ||
| label: "Apply feedback", | ||
| message: `The following feedback was given on the existing changes on branch "${branch}":\n\n${feedback}\n\nPlease make the requested changes.`, | ||
| env, | ||
| }); | ||
|
|
||
| logStep("Pushing updates via agent"); | ||
| await runOpenClawAgent(sandbox, { | ||
| label: "Push feedback changes", | ||
| env, | ||
| message: [ | ||
| `Stage, commit, and push the feedback changes to the existing PR branch.`, | ||
| ``, | ||
| `Repo: ${repo}, branch: ${branch}`, | ||
| ``, | ||
| `Steps:`, | ||
| `1. cd into the submodule directory for ${repo}`, | ||
| `2. git add -A`, | ||
| `3. git commit -m "agent: address feedback"`, | ||
| `4. git push origin ${branch}`, | ||
| ].join("\n"), | ||
| }); | ||
|
|
||
| logStep("Taking new snapshot"); | ||
| const newSnapshot = await sandbox.snapshot(); | ||
|
|
||
| logStep("Notifying bot"); | ||
| await notifyCodingAgentCallback({ | ||
| threadId: callbackThreadId, | ||
| status: "updated", | ||
| snapshotId: newSnapshot.snapshotId, | ||
| }); | ||
|
|
||
| metadata.set("currentStep", "Complete"); | ||
|
|
||
| return { snapshotId: newSnapshot.snapshotId }; | ||
| } finally { | ||
| logStep("Stopping sandbox", false, { sandboxId: sandbox.sandboxId }); | ||
| await sandbox.stop(); | ||
| } | ||
| }, | ||
| }); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agent call failures are not explicitly handled.
If
runOpenClawAgentthrows or returns a non-zero exit code, the task will fail without notifying the callback about the error. Sinceretry.maxAttempts: 0is set, users may be left without feedback on what happened.Consider checking the exit code and notifying on failure:
🛡️ Suggested: Handle agent failure
logStep("Running AI agent with feedback"); - await runOpenClawAgent(sandbox, { + const feedbackResult = await runOpenClawAgent(sandbox, { label: "Apply feedback", message: `The following feedback was given on the existing changes on branch "${branch}":\n\n${feedback}\n\nPlease make the requested changes.`, env, }); + if (feedbackResult.exitCode !== 0) { + await notifyCodingAgentCallback({ + threadId: callbackThreadId, + status: "failed", + error: feedbackResult.stderr || "Agent failed to apply feedback", + }); + throw new Error("Agent failed to apply feedback"); + }📝 Committable suggestion
🤖 Prompt for AI Agents