-
Notifications
You must be signed in to change notification settings - Fork 1
agent: @U0AJM7X8FBR Slack Action: Merge test to main in the slack client in the #279
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
recoup-coding-agent
merged 2 commits into
test
from
agent/-u0ajm7x8fbr-slack-action--mer-1773153587086
Mar 10, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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,51 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { mergeGithubBranch } from "../mergeGithubBranch"; | ||
|
|
||
| global.fetch = vi.fn(); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe("mergeGithubBranch", () => { | ||
| it("merges a branch and returns ok", async () => { | ||
| vi.mocked(fetch).mockResolvedValue({ ok: true, status: 201 } as Response); | ||
|
|
||
| const result = await mergeGithubBranch("recoupable/api", "test", "main", "ghp_test"); | ||
|
|
||
| expect(result).toEqual({ ok: true }); | ||
| expect(fetch).toHaveBeenCalledWith( | ||
| "https://api.github.com/repos/recoupable/api/merges", | ||
| expect.objectContaining({ | ||
| method: "POST", | ||
| body: JSON.stringify({ | ||
| base: "main", | ||
| head: "test", | ||
| commit_message: "Merge test into main", | ||
| }), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("returns ok when already up to date (204)", async () => { | ||
| vi.mocked(fetch).mockResolvedValue({ ok: true, status: 204 } as Response); | ||
|
|
||
| const result = await mergeGithubBranch("recoupable/chat", "test", "main", "ghp_test"); | ||
|
|
||
| expect(result).toEqual({ ok: true }); | ||
| }); | ||
|
|
||
| it("returns error message on failure", async () => { | ||
| const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); | ||
| vi.mocked(fetch).mockResolvedValue({ | ||
| ok: false, | ||
| status: 409, | ||
| text: () => Promise.resolve(JSON.stringify({ message: "Merge conflict" })), | ||
| } as any); | ||
|
|
||
| const result = await mergeGithubBranch("recoupable/api", "test", "main", "ghp_test"); | ||
|
|
||
| expect(result).toEqual({ ok: false, message: "Merge conflict" }); | ||
| consoleSpy.mockRestore(); | ||
| }); | ||
| }); |
73 changes: 73 additions & 0 deletions
73
lib/coding-agent/__tests__/onMergeTestToMainAction.test.ts
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,73 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
|
|
||
| const mockMergeGithubBranch = vi.fn(); | ||
| vi.mock("../mergeGithubBranch", () => ({ | ||
| mergeGithubBranch: (...args: unknown[]) => mockMergeGithubBranch(...args), | ||
| })); | ||
|
|
||
| const { registerOnMergeTestToMainAction } = await import( | ||
| "../handlers/onMergeTestToMainAction" | ||
| ); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| process.env.GITHUB_TOKEN = "ghp_test"; | ||
| }); | ||
|
|
||
| function createMockBot() { | ||
| return { onAction: vi.fn() } as any; | ||
| } | ||
|
|
||
| describe("registerOnMergeTestToMainAction", () => { | ||
| it("registers merge_test_to_main: action handler", () => { | ||
| const bot = createMockBot(); | ||
| registerOnMergeTestToMainAction(bot); | ||
| expect(bot.onAction).toHaveBeenCalledWith("merge_test_to_main:", expect.any(Function)); | ||
| }); | ||
|
|
||
| it("merges test to main and posts success", async () => { | ||
| mockMergeGithubBranch.mockResolvedValue({ ok: true }); | ||
|
|
||
| const bot = createMockBot(); | ||
| registerOnMergeTestToMainAction(bot); | ||
| const handler = bot.onAction.mock.calls[0][1]; | ||
|
|
||
| const mockThread = { post: vi.fn() }; | ||
|
|
||
| await handler({ thread: mockThread, actionId: "merge_test_to_main:recoupable/chat" }); | ||
|
|
||
| expect(mockMergeGithubBranch).toHaveBeenCalledWith("recoupable/chat", "test", "main", "ghp_test"); | ||
| expect(mockThread.post).toHaveBeenCalledWith("✅ Merged test → main for recoupable/chat."); | ||
| }); | ||
|
|
||
| it("posts error message on failure", async () => { | ||
| mockMergeGithubBranch.mockResolvedValue({ ok: false, message: "Merge conflict" }); | ||
|
|
||
| const bot = createMockBot(); | ||
| registerOnMergeTestToMainAction(bot); | ||
| const handler = bot.onAction.mock.calls[0][1]; | ||
|
|
||
| const mockThread = { post: vi.fn() }; | ||
|
|
||
| await handler({ thread: mockThread, actionId: "merge_test_to_main:recoupable/api" }); | ||
|
|
||
| expect(mockThread.post).toHaveBeenCalledWith( | ||
| "❌ Failed to merge test → main for recoupable/api: Merge conflict", | ||
| ); | ||
| }); | ||
|
|
||
| it("posts missing token message when GITHUB_TOKEN is not set", async () => { | ||
| delete process.env.GITHUB_TOKEN; | ||
|
|
||
| const bot = createMockBot(); | ||
| registerOnMergeTestToMainAction(bot); | ||
| const handler = bot.onAction.mock.calls[0][1]; | ||
|
|
||
| const mockThread = { post: vi.fn() }; | ||
|
|
||
| await handler({ thread: mockThread, actionId: "merge_test_to_main:recoupable/api" }); | ||
|
|
||
| expect(mockThread.post).toHaveBeenCalledWith("Missing GITHUB_TOKEN — cannot merge branches."); | ||
| expect(mockMergeGithubBranch).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
28 changes: 28 additions & 0 deletions
28
lib/coding-agent/__tests__/parseMergeTestToMainActionId.test.ts
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,28 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { parseMergeTestToMainActionId } from "../parseMergeTestToMainActionId"; | ||
|
|
||
| describe("parseMergeTestToMainActionId", () => { | ||
| it("parses a valid action ID", () => { | ||
| expect(parseMergeTestToMainActionId("merge_test_to_main:recoupable/api")).toBe("recoupable/api"); | ||
| }); | ||
|
|
||
| it("parses action ID with hyphenated repo name", () => { | ||
| expect(parseMergeTestToMainActionId("merge_test_to_main:org/sub-repo")).toBe("org/sub-repo"); | ||
| }); | ||
|
|
||
| it("returns null for missing repo", () => { | ||
| expect(parseMergeTestToMainActionId("merge_test_to_main:")).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for repo without slash", () => { | ||
| expect(parseMergeTestToMainActionId("merge_test_to_main:noslash")).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for wrong prefix", () => { | ||
| expect(parseMergeTestToMainActionId("other_action:repo/name")).toBeNull(); | ||
| }); | ||
|
|
||
| it("returns null for empty string", () => { | ||
| expect(parseMergeTestToMainActionId("")).toBeNull(); | ||
| }); | ||
| }); |
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,22 @@ | ||
| import { Card, CardText, Actions, Button } from "chat"; | ||
|
|
||
| /** | ||
| * Builds a Card with a "Merge test to main" button for a specific repo. | ||
| * | ||
| * @param repo - Full repo identifier (e.g. "recoupable/chat") | ||
| */ | ||
| export function buildMergeTestToMainCard(repo: string) { | ||
| return Card({ | ||
| title: "Merge to Main", | ||
| children: [ | ||
| CardText(`Ready to promote \`test\` → \`main\` for ${repo}.`), | ||
| Actions([ | ||
| Button({ | ||
| id: `merge_test_to_main:${repo}`, | ||
| label: `Merge test → main (${repo})`, | ||
| style: "primary", | ||
| }), | ||
| ]), | ||
| ], | ||
| }); | ||
| } |
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
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,37 @@ | ||
| import type { CodingAgentBot } from "../bot"; | ||
| import { mergeGithubBranch } from "../mergeGithubBranch"; | ||
| import { parseMergeTestToMainActionId } from "../parseMergeTestToMainActionId"; | ||
|
|
||
| /** | ||
| * Registers the "Merge test to main" button action handler on the bot. | ||
| * Merges the test branch into main for the specified repo via the GitHub API. | ||
| * | ||
| * @param bot | ||
| */ | ||
| export function registerOnMergeTestToMainAction(bot: CodingAgentBot) { | ||
| bot.onAction("merge_test_to_main:", async event => { | ||
| const thread = event.thread; | ||
|
|
||
| const repo = parseMergeTestToMainActionId(event.actionId); | ||
| if (!repo) { | ||
| await thread.post("Invalid merge test to main action."); | ||
| return; | ||
| } | ||
|
|
||
| const token = process.env.GITHUB_TOKEN; | ||
| if (!token) { | ||
| await thread.post("Missing GITHUB_TOKEN — cannot merge branches."); | ||
| return; | ||
| } | ||
|
|
||
| const result = await mergeGithubBranch(repo, "test", "main", token); | ||
|
|
||
| if (result.ok === false) { | ||
| const { message } = result; | ||
| await thread.post(`❌ Failed to merge test → main for ${repo}: ${message}`); | ||
| return; | ||
| } | ||
|
|
||
| await thread.post(`✅ Merged test → main for ${repo}.`); | ||
| }); | ||
| } |
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
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,55 @@ | ||
| export interface MergeGithubBranchSuccess { | ||
| ok: true; | ||
| } | ||
|
|
||
| export interface MergeGithubBranchFailure { | ||
| ok: false; | ||
| message: string; | ||
| } | ||
|
|
||
| export type MergeGithubBranchResult = MergeGithubBranchSuccess | MergeGithubBranchFailure; | ||
|
|
||
| /** | ||
| * Merges one branch into another via the GitHub API. | ||
| * | ||
| * @param repo - Full repo identifier (e.g. "recoupable/api") | ||
| * @param head - Branch to merge from (e.g. "test") | ||
| * @param base - Branch to merge into (e.g. "main") | ||
| * @param token - GitHub API token | ||
| */ | ||
| export async function mergeGithubBranch( | ||
| repo: string, | ||
| head: string, | ||
| base: string, | ||
| token: string, | ||
| ): Promise<MergeGithubBranchResult> { | ||
| const [owner, repoName] = repo.split("/"); | ||
| const response = await fetch( | ||
| `https://api.github.com/repos/${owner}/${repoName}/merges`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| Accept: "application/vnd.github+json", | ||
| "X-GitHub-Api-Version": "2022-11-28", | ||
| }, | ||
| body: JSON.stringify({ | ||
| base, | ||
| head, | ||
| commit_message: `Merge ${head} into ${base}`, | ||
| }), | ||
| }, | ||
| ); | ||
|
|
||
| // 201 = merged, 204 = already up to date (both are success) | ||
| if (response.ok) { | ||
| return { ok: true }; | ||
| } | ||
|
|
||
| const errorBody = await response.text(); | ||
| console.error( | ||
| `[coding-agent] branch merge failed for ${repo} (${head} → ${base}): ${response.status} ${errorBody}`, | ||
| ); | ||
| const error = JSON.parse(errorBody); | ||
| return { ok: false, message: error.message }; | ||
| } | ||
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,10 @@ | ||
| /** | ||
| * Parses a merge_test_to_main action ID like "merge_test_to_main:recoupable/api" | ||
| * into the repo string, or null if the format doesn't match. | ||
| */ | ||
| export function parseMergeTestToMainActionId(actionId: string): string | null { | ||
| const prefix = "merge_test_to_main:"; | ||
| if (!actionId.startsWith(prefix)) return null; | ||
| const repo = actionId.slice(prefix.length); | ||
| return repo.includes("/") ? repo : null; | ||
| } |
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.
Missing try-catch around
JSON.parsecan cause unhandled exceptions.If the GitHub API returns a non-JSON response (e.g., HTML error page during outage, proxy errors, or rate-limit pages),
JSON.parse(errorBody)will throw aSyntaxError, crashing the handler and leaving the user without feedback.🐛 Proposed fix with fallback for non-JSON responses
const errorBody = await response.text(); console.error( `[coding-agent] branch merge failed for ${repo} (${head} → ${base}): ${response.status} ${errorBody}`, ); - const error = JSON.parse(errorBody); - return { ok: false, message: error.message }; + try { + const error = JSON.parse(errorBody); + return { ok: false, message: error.message ?? errorBody }; + } catch { + return { ok: false, message: `GitHub API error: ${response.status}` }; + }🤖 Prompt for AI Agents