Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions packages/opencode/src/cli/cmd/github.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { exec } from "child_process"
import { Filesystem } from "@/util/filesystem"
import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda"
import { Octokit } from "@octokit/rest"
import { graphql } from "@octokit/graphql"
import * as core from "@actions/core"
import * as github from "@actions/github"
import type { Context } from "@actions/github/lib/context"
Expand Down Expand Up @@ -33,7 +31,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 { createGithubClients, extractResponseText, formatPromptTooLargeError } from "./github.shared"

type GitHubAuthor = {
login: string
Expand Down Expand Up @@ -429,8 +427,8 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"

let appToken: string
let octoRest: Octokit
let octoGraph: typeof graphql
let octoRest: ReturnType<typeof createGithubClients>["rest"]
let octoGraph: ReturnType<typeof createGithubClients>["graph"]
let gitConfig: string
let session: { id: SessionID; title: string; version: string }
let shareId: string | undefined
Expand Down Expand Up @@ -479,10 +477,9 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
const actionToken = isMock ? args.token! : await getOidcToken()
appToken = await exchangeForAppToken(actionToken)
}
octoRest = new Octokit({ auth: appToken })
octoGraph = graphql.defaults({
headers: { authorization: `token ${appToken}` },
})
const clients = createGithubClients(appToken)
octoRest = clients.rest
octoGraph = clients.graph

const { userPrompt, promptFiles } = await getUserPrompt()
if (!useGithubToken) {
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/src/cli/cmd/github.shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { SessionV1 } from "@opencode-ai/core/v1/session"
import { graphql } from "@octokit/graphql"
import { Octokit } from "@octokit/rest"

export { parseGitHubRemote } from "@/util/repository"

Expand Down Expand Up @@ -28,3 +30,18 @@ export function formatPromptTooLargeError(files: { filename: string; content: st
: ""
return `PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.${fileDetails}`
}

export function createGithubClients(auth: string) {
const restBaseUrl = (process.env["GITHUB_API_URL"] ?? "https://api.github.com").replace(/\/+$/, "")
const graphqlBaseUrl = (process.env["GITHUB_GRAPHQL_URL"] ?? "https://api.github.com")
.replace(/\/+$/, "")
.replace(/\/graphql$/, "")

return {
rest: new Octokit({ auth, baseUrl: restBaseUrl }),
graph: graphql.defaults({
baseUrl: graphqlBaseUrl,
headers: { authorization: `token ${auth}` },
}),
}
}
92 changes: 92 additions & 0 deletions packages/opencode/test/cli/github-endpoints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { afterEach, describe, expect, test } from "bun:test"
import { createGithubClients } from "../../src/cli/cmd/github.shared"

const endpointCases = [
{
name: "uses GitHub.com when both endpoint variables are unset",
restBaseUrl: undefined,
graphqlBaseUrl: undefined,
expectedURLs: ["https://api.github.com/repos/acme/widgets", "https://api.github.com/graphql"],
},
{
name: "uses GITHUB_API_URL only for REST and removes trailing slashes",
restBaseUrl: "https://github.example.test/api/v3///",
graphqlBaseUrl: undefined,
expectedURLs: ["https://github.example.test/api/v3/repos/acme/widgets", "https://api.github.com/graphql"],
},
{
name: "uses GITHUB_GRAPHQL_URL only for GraphQL",
restBaseUrl: undefined,
graphqlBaseUrl: "https://github.example.test/api/graphql",
expectedURLs: ["https://api.github.com/repos/acme/widgets", "https://github.example.test/api/graphql"],
},
{
name: "uses independent REST and GraphQL endpoints when both are set",
restBaseUrl: "https://github.example.test/api/v3",
graphqlBaseUrl: "https://github.example.test/api/graphql",
expectedURLs: [
"https://github.example.test/api/v3/repos/acme/widgets",
"https://github.example.test/api/graphql",
],
},
{
name: "does not duplicate a terminal GraphQL path when removing trailing slashes",
restBaseUrl: undefined,
graphqlBaseUrl: "https://github.example.test/api/graphql///",
expectedURLs: ["https://api.github.com/repos/acme/widgets", "https://github.example.test/api/graphql"],
},
{
name: "preserves a custom GraphQL base before the client appends its path",
restBaseUrl: undefined,
graphqlBaseUrl: "https://github.example.test/custom///",
expectedURLs: ["https://api.github.com/repos/acme/widgets", "https://github.example.test/custom/graphql"],
},
] as const

const originalGithubApiUrl = process.env["GITHUB_API_URL"]
const originalGithubGraphqlUrl = process.env["GITHUB_GRAPHQL_URL"]
const originalFetch = globalThis.fetch

function setEndpoint(variable: "GITHUB_API_URL" | "GITHUB_GRAPHQL_URL", value: string | undefined) {
if (value === undefined) {
delete process.env[variable]
return
}
process.env[variable] = value
}

afterEach(() => {
setEndpoint("GITHUB_API_URL", originalGithubApiUrl)
setEndpoint("GITHUB_GRAPHQL_URL", originalGithubGraphqlUrl)
globalThis.fetch = originalFetch
})

describe("createGithubClients", () => {
for (const endpointCase of endpointCases) {
test(endpointCase.name, async () => {
setEndpoint("GITHUB_API_URL", endpointCase.restBaseUrl)
setEndpoint("GITHUB_GRAPHQL_URL", endpointCase.graphqlBaseUrl)
const requests: Request[] = []
globalThis.fetch = Object.assign(
async (...[input, init]: Parameters<typeof fetch>) => {
const request = new Request(input, init)
requests.push(request)
return new Response(JSON.stringify({ data: { repository: {} } }), {
headers: { "content-type": "application/json" },
})
},
{ preconnect: originalFetch.preconnect },
)
const clients = createGithubClients("endpoint-test-token")

await clients.rest.rest.repos.get({ owner: "acme", repo: "widgets" })
await clients.graph('query { repository(owner: "acme", name: "widgets") { id } }')

expect(requests.map((request) => request.url)).toEqual([...endpointCase.expectedURLs])
expect(requests.map((request) => request.headers.get("authorization"))).toEqual([
"token endpoint-test-token",
"token endpoint-test-token",
])
})
}
})
Loading