From 96fdc2496b530aa21217eb360874073c4e9d3359 Mon Sep 17 00:00:00 2001 From: parbez Date: Tue, 8 Sep 2026 17:00:05 +0530 Subject: [PATCH 1/2] fix: use the token owner for GitHub reactions and git identity use_github_token workflows were leaving eyes reactions stuck because removal only looked up opencode-agent[bot]. --- .../opencode/src/cli/cmd/github.handler.ts | 23 +++++++++++------ .../opencode/src/cli/cmd/github.shared.ts | 10 ++++++++ packages/opencode/src/cli/cmd/github.ts | 8 +++++- .../opencode/test/cli/github-action.test.ts | 25 ++++++++++++++++++- packages/web/src/content/docs/github.mdx | 2 ++ 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/github.handler.ts b/packages/opencode/src/cli/cmd/github.handler.ts index fcf44279ce7f..e4119ebbc3f9 100644 --- a/packages/opencode/src/cli/cmd/github.handler.ts +++ b/packages/opencode/src/cli/cmd/github.handler.ts @@ -33,7 +33,7 @@ import { setTimeout as sleep } from "node:timers/promises" import { Process } from "@/util/process" import { parseGitHubRemote } from "@/util/repository" import { Effect } from "effect" -import { extractResponseText, formatPromptTooLargeError } from "./github.shared" +import { DEFAULT_AGENT_USERNAME, extractResponseText, formatPromptTooLargeError, resolveAgentUsername } from "./github.shared" type GitHubAuthor = { login: string @@ -140,7 +140,6 @@ type IssueQueryResponse = { } } -const AGENT_USERNAME = "opencode-agent[bot]" const AGENT_REACTION = "eyes" const WORKFLOW_FILE = ".github/workflows/opencode.yml" @@ -438,6 +437,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: let shareId: string | undefined let exitCode = 0 let githubClientReady = false + let agentUsername = DEFAULT_AGENT_USERNAME type PromptFiles = Awaited>["promptFiles"] const triggerCommentId = isCommentEvent ? (payload as IssueCommentEvent | PullRequestReviewCommentEvent).comment.id @@ -487,6 +487,15 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: headers: { authorization: `token ${appToken}` }, }) githubClientReady = true + agentUsername = await resolveAgentUsername(async () => { + try { + const { data } = await octoRest.rest.users.getAuthenticated() + if (data.login) return data.login + } catch {} + const viewer = await octoGraph<{ viewer: { login: string } }>(`query { viewer { login } }`) + return viewer.viewer?.login + }) + console.log("Acting as", agentUsername) const { userPrompt, promptFiles } = await getUserPrompt() if (!useGithubToken) { @@ -1036,8 +1045,8 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64") await gitRun(["config", "--local", config, `AUTHORIZATION: basic ${newCredentials}`]) - await gitRun(["config", "--global", "user.name", AGENT_USERNAME]) - await gitRun(["config", "--global", "user.email", `${AGENT_USERNAME}@users.noreply.github.com`]) + await gitRun(["config", "--global", "user.name", agentUsername]) + await gitRun(["config", "--global", "user.email", `${agentUsername}@users.noreply.github.com`]) } async function restoreGitConfig() { @@ -1223,7 +1232,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: content: AGENT_REACTION, }) - const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME) + const eyesReaction = reactions.data.find((r) => r.user?.login === agentUsername) if (!eyesReaction) return return await octoRest.rest.reactions.deleteForPullRequestComment({ @@ -1241,7 +1250,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: content: AGENT_REACTION, }) - const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME) + const eyesReaction = reactions.data.find((r) => r.user?.login === agentUsername) if (!eyesReaction) return return await octoRest.rest.reactions.deleteForIssueComment({ @@ -1259,7 +1268,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: content: AGENT_REACTION, }) - const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME) + const eyesReaction = reactions.data.find((r) => r.user?.login === agentUsername) if (!eyesReaction) return await octoRest.rest.reactions.deleteForIssue({ diff --git a/packages/opencode/src/cli/cmd/github.shared.ts b/packages/opencode/src/cli/cmd/github.shared.ts index 157d0156fb00..852c31a51252 100644 --- a/packages/opencode/src/cli/cmd/github.shared.ts +++ b/packages/opencode/src/cli/cmd/github.shared.ts @@ -21,6 +21,16 @@ export function extractResponseText(parts: SessionV1.Part[]): string | null { * Formats a PROMPT_TOO_LARGE error message with details about files in the prompt. * Content is base64 encoded, so we calculate original size by multiplying by 0.75. */ +export const DEFAULT_AGENT_USERNAME = "opencode-agent[bot]" + +export async function resolveAgentUsername(getLogin: () => Promise): Promise { + try { + const login = await getLogin() + if (login) return login + } catch {} + return DEFAULT_AGENT_USERNAME +} + export function formatPromptTooLargeError(files: { filename: string; content: string }[]): string { const fileDetails = files.length > 0 diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index eccbb375c6c2..11e992d357a0 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -2,7 +2,13 @@ import { Effect } from "effect" import { cmd } from "./cmd" import { effectCmd } from "../effect-cmd" -export { extractResponseText, formatPromptTooLargeError, parseGitHubRemote } from "./github.shared" +export { + DEFAULT_AGENT_USERNAME, + extractResponseText, + formatPromptTooLargeError, + parseGitHubRemote, + resolveAgentUsername, +} from "./github.shared" export const GithubInstallCommand = effectCmd({ command: "install", diff --git a/packages/opencode/test/cli/github-action.test.ts b/packages/opencode/test/cli/github-action.test.ts index 57567d8c9bf4..dbe4356cefec 100644 --- a/packages/opencode/test/cli/github-action.test.ts +++ b/packages/opencode/test/cli/github-action.test.ts @@ -1,6 +1,11 @@ import { test, expect, describe } from "bun:test" import { SessionV1 } from "@opencode-ai/core/v1/session" -import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github" +import { + DEFAULT_AGENT_USERNAME, + extractResponseText, + formatPromptTooLargeError, + resolveAgentUsername, +} from "../../src/cli/cmd/github" import type { MessageV2 } from "../../src/session/message-v2" import { SessionID, MessageID, PartID } from "../../src/session/schema" @@ -197,3 +202,21 @@ describe("formatPromptTooLargeError", () => { expect(result).toInclude("img3.gif (9 KB)") }) }) + +describe("resolveAgentUsername", () => { + test("uses the authenticated login", async () => { + await expect(resolveAgentUsername(async () => "spartans-bot[bot]")).resolves.toBe("spartans-bot[bot]") + }) + + test("falls back when lookup fails", async () => { + await expect( + resolveAgentUsername(async () => { + throw new Error("denied") + }), + ).resolves.toBe(DEFAULT_AGENT_USERNAME) + }) + + test("falls back when login is missing", async () => { + await expect(resolveAgentUsername(async () => undefined)).resolves.toBe(DEFAULT_AGENT_USERNAME) + }) +}) diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx index 08cb27fff019..30f9b3d7f6f1 100644 --- a/packages/web/src/content/docs/github.mdx +++ b/packages/web/src/content/docs/github.mdx @@ -109,6 +109,8 @@ Or you can set it up manually. `id-token: write` is not required in this mode because OIDC exchange is skipped. To use a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) or another GitHub App token, store it as a secret and pass that secret as `GITHUB_TOKEN` instead. + Eyes reactions and git commits use the token owner (`github-actions[bot]`, your GitHub App bot, or the PAT user), not `opencode-agent[bot]`. + --- ## Supported Events From e72b223defef7a757997ef08d2b5dd2183d13abb Mon Sep 17 00:00:00 2001 From: parbez Date: Tue, 8 Sep 2026 17:22:02 +0530 Subject: [PATCH 2/2] refactor: inline token owner lookup The extra helper only wrapped a default-plus-overwrite. Do that in the handler. --- .../opencode/src/cli/cmd/github.handler.ts | 19 ++++++++------ .../opencode/src/cli/cmd/github.shared.ts | 10 -------- packages/opencode/src/cli/cmd/github.ts | 8 +----- .../opencode/test/cli/github-action.test.ts | 25 +------------------ 4 files changed, 13 insertions(+), 49 deletions(-) diff --git a/packages/opencode/src/cli/cmd/github.handler.ts b/packages/opencode/src/cli/cmd/github.handler.ts index e4119ebbc3f9..75411efbf003 100644 --- a/packages/opencode/src/cli/cmd/github.handler.ts +++ b/packages/opencode/src/cli/cmd/github.handler.ts @@ -33,7 +33,7 @@ import { setTimeout as sleep } from "node:timers/promises" import { Process } from "@/util/process" import { parseGitHubRemote } from "@/util/repository" import { Effect } from "effect" -import { DEFAULT_AGENT_USERNAME, extractResponseText, formatPromptTooLargeError, resolveAgentUsername } from "./github.shared" +import { extractResponseText, formatPromptTooLargeError } from "./github.shared" type GitHubAuthor = { login: string @@ -140,6 +140,7 @@ type IssueQueryResponse = { } } +const AGENT_USERNAME = "opencode-agent[bot]" const AGENT_REACTION = "eyes" const WORKFLOW_FILE = ".github/workflows/opencode.yml" @@ -437,7 +438,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: let shareId: string | undefined let exitCode = 0 let githubClientReady = false - let agentUsername = DEFAULT_AGENT_USERNAME + let agentUsername = AGENT_USERNAME type PromptFiles = Awaited>["promptFiles"] const triggerCommentId = isCommentEvent ? (payload as IssueCommentEvent | PullRequestReviewCommentEvent).comment.id @@ -487,14 +488,16 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: headers: { authorization: `token ${appToken}` }, }) githubClientReady = true - agentUsername = await resolveAgentUsername(async () => { + try { + const { data } = await octoRest.rest.users.getAuthenticated() + if (data.login) agentUsername = data.login + } catch {} + if (agentUsername === AGENT_USERNAME) { try { - const { data } = await octoRest.rest.users.getAuthenticated() - if (data.login) return data.login + const viewer = await octoGraph<{ viewer: { login: string } }>(`query { viewer { login } }`) + if (viewer.viewer?.login) agentUsername = viewer.viewer.login } catch {} - const viewer = await octoGraph<{ viewer: { login: string } }>(`query { viewer { login } }`) - return viewer.viewer?.login - }) + } console.log("Acting as", agentUsername) const { userPrompt, promptFiles } = await getUserPrompt() diff --git a/packages/opencode/src/cli/cmd/github.shared.ts b/packages/opencode/src/cli/cmd/github.shared.ts index 852c31a51252..157d0156fb00 100644 --- a/packages/opencode/src/cli/cmd/github.shared.ts +++ b/packages/opencode/src/cli/cmd/github.shared.ts @@ -21,16 +21,6 @@ export function extractResponseText(parts: SessionV1.Part[]): string | null { * Formats a PROMPT_TOO_LARGE error message with details about files in the prompt. * Content is base64 encoded, so we calculate original size by multiplying by 0.75. */ -export const DEFAULT_AGENT_USERNAME = "opencode-agent[bot]" - -export async function resolveAgentUsername(getLogin: () => Promise): Promise { - try { - const login = await getLogin() - if (login) return login - } catch {} - return DEFAULT_AGENT_USERNAME -} - export function formatPromptTooLargeError(files: { filename: string; content: string }[]): string { const fileDetails = files.length > 0 diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 11e992d357a0..eccbb375c6c2 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -2,13 +2,7 @@ import { Effect } from "effect" import { cmd } from "./cmd" import { effectCmd } from "../effect-cmd" -export { - DEFAULT_AGENT_USERNAME, - extractResponseText, - formatPromptTooLargeError, - parseGitHubRemote, - resolveAgentUsername, -} from "./github.shared" +export { extractResponseText, formatPromptTooLargeError, parseGitHubRemote } from "./github.shared" export const GithubInstallCommand = effectCmd({ command: "install", diff --git a/packages/opencode/test/cli/github-action.test.ts b/packages/opencode/test/cli/github-action.test.ts index dbe4356cefec..57567d8c9bf4 100644 --- a/packages/opencode/test/cli/github-action.test.ts +++ b/packages/opencode/test/cli/github-action.test.ts @@ -1,11 +1,6 @@ import { test, expect, describe } from "bun:test" import { SessionV1 } from "@opencode-ai/core/v1/session" -import { - DEFAULT_AGENT_USERNAME, - extractResponseText, - formatPromptTooLargeError, - resolveAgentUsername, -} from "../../src/cli/cmd/github" +import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github" import type { MessageV2 } from "../../src/session/message-v2" import { SessionID, MessageID, PartID } from "../../src/session/schema" @@ -202,21 +197,3 @@ describe("formatPromptTooLargeError", () => { expect(result).toInclude("img3.gif (9 KB)") }) }) - -describe("resolveAgentUsername", () => { - test("uses the authenticated login", async () => { - await expect(resolveAgentUsername(async () => "spartans-bot[bot]")).resolves.toBe("spartans-bot[bot]") - }) - - test("falls back when lookup fails", async () => { - await expect( - resolveAgentUsername(async () => { - throw new Error("denied") - }), - ).resolves.toBe(DEFAULT_AGENT_USERNAME) - }) - - test("falls back when login is missing", async () => { - await expect(resolveAgentUsername(async () => undefined)).resolves.toBe(DEFAULT_AGENT_USERNAME) - }) -})