diff --git a/github/README.md b/github/README.md index 47213a30978..5a25b5b254b 100644 --- a/github/README.md +++ b/github/README.md @@ -42,7 +42,7 @@ This will walk you through installing the GitHub app, creating the workflow, and ### Manual Setup -1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository. +1. Install the GitHub app . Make sure it is installed on the target repository. 2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`. ```yml @@ -76,9 +76,82 @@ This will walk you through installing the GitHub app, creating the workflow, and 3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys. +## Custom Provider Configuration + +You can configure custom providers using an `opencode.json` config file in your repository root: + +### Basic Usage with Config File + +1. Create `.github/opencode.json` in your repository: + + ```json + { + "model": "custom-provider/my-model", + "provider": { + "custom-provider": { + "npm": "@ai-sdk/openai-compatible", + "models": { + "my-model": { + "name": "My Custom Model" + } + }, + "options": { + "apiKey": "{env:CUSTOM_API_KEY}", + "baseURL": "{env:CUSTOM_BASE_URL}" + } + } + } + } + ``` + +2. Update your workflow to use the config: + + ```yml + - name: Run opencode + uses: sst/opencode/github@latest + env: + CUSTOM_API_KEY: ${{ secrets.CUSTOM_API_KEY }} + CUSTOM_BASE_URL: ${{ secrets.CUSTOM_BASE_URL }} + # No model parameter needed - will automatically use .github/opencode.json + ``` + +### Advanced Configuration + +You can also specify a custom config file path and pass environment variables: + +```yml +- name: Run opencode + uses: sst/opencode/github@latest + with: + config: opencode-ci.json + config_env: | + CUSTOM_API_KEY=${{ secrets.CUSTOM_API_KEY }} + CUSTOM_BASE_URL=https://api.example.com + DEBUG=true + # model parameter will override config if specified +``` + +### Action Inputs + +| Input | Description | Required | Default | +| ------------ | ------------------------------------------------------------- | -------- | ----------------------- | +| `model` | Model to use (overrides config file) | No | - | +| `config` | Path to opencode config file | No | Auto-discovery | +| `config_env` | Environment variables for config (multiline key=value format) | No | - | +| `share` | Share the opencode session | No | `true` for public repos | + +### Config File Discovery + +The action will automatically look for config files in this order: + +1. Path specified in `config` input +2. `.github/opencode.json` +3. `opencode.json` in repository root +4. `.opencode/config.json` in repository root + ## Support -This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues. +This is an early release. If you encounter issues or have feedback, please create an issue at . ## Development @@ -122,7 +195,7 @@ Replace: - `"number":4` with the GitHub issue id - `"body":"hey opencode, summarize thread"` with comment body -### Issue comment with image attachment. +### Issue comment with image attachment ``` --event '{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image ![Image](https://github.com/user-attachments/assets/xxxxxxxx)"}}}' diff --git a/github/action.yml b/github/action.yml index 0b7367ded42..c75f833720c 100644 --- a/github/action.yml +++ b/github/action.yml @@ -6,8 +6,16 @@ branding: inputs: model: - description: "Model to use" - required: true + description: "Model to use (overrides config file)" + required: false + + opencode_config: + description: "Path to opencode config file (defaults to .github/opencode.json)" + required: false + + config_env: + description: "Environment variables for config (multiline key=value format)" + required: false share: description: "Share the opencode session (defaults to true for public repos)" @@ -26,4 +34,6 @@ runs: run: opencode github run env: MODEL: ${{ inputs.model }} + OPENCODE_CONFIG: ${{ inputs.config }} + CONFIG_ENV: ${{ inputs.config_env }} SHARE: ${{ inputs.share }} diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 3c18d1c5f8c..a15402ba2ad 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -19,6 +19,7 @@ import { Identifier } from "../../id/id" import { Provider } from "../../provider/provider" import { Bus } from "../../bus" import { MessageV2 } from "../../session/message-v2" +import { Config } from "../../config/config" type GitHubAuthor = { login: string @@ -129,7 +130,7 @@ export const GithubCommand = cmd({ command: "github", describe: "manage GitHub agent", builder: (yargs) => yargs.command(GithubInstallCommand).command(GithubRunCommand).demandCommand(), - async handler() {}, + async handler() { }, }) export const GithubInstallCommand = cmd({ @@ -364,7 +365,8 @@ export const GithubRunCommand = cmd({ process.exit(1) } - const { providerID, modelID } = normalizeModel() + await loadConfiguration() + const { providerID, modelID } = await normalizeModel() const runId = normalizeRunId() const share = normalizeShare() const { owner, repo } = context.repo @@ -481,9 +483,91 @@ export const GithubRunCommand = cmd({ } process.exit(exitCode) - function normalizeModel() { + function parseConfigEnvironment(): Record { + const configEnv = process.env["CONFIG_ENV"] + if (!configEnv) return {} + + try { + // Parse key=value format only + const result: Record = {} + configEnv.split("\n").forEach((line) => { + const trimmed = line.trim() + if (trimmed && !trimmed.startsWith("#")) { + const [key, ...valueParts] = trimmed.split("=") + if (key && valueParts.length > 0) { + result[key.trim()] = valueParts.join("=").trim() + } + } + }) + + return result + } catch (error) { + console.warn("Failed to parse CONFIG_ENV, ignoring:", error) + return {} + } + } + + async function loadConfiguration() { + // Parse and set additional environment variables + const additionalEnv = parseConfigEnvironment() + for (const [key, value] of Object.entries(additionalEnv)) { + process.env[key] = value + console.log(`Set environment variable: ${key}`) + } + + // Load config file if specified or discover default + const configPath = process.env["OPENCODE_CONFIG"] + if (configPath) { + console.log(`Using config from OPENCODE_CONFIG: ${configPath}`) + try { + // Check if config file exists + const fs = await import("fs") + const path = await import("path") + const fullPath = path.resolve(configPath) + if (!fs.existsSync(fullPath)) { + throw new Error(`Config file not found: ${fullPath}`) + } + console.log(`Config file found: ${fullPath}`) + } catch (error) { + console.warn(`Config file error: ${error instanceof Error ? error.message : String(error)}`) + } + } else { + // Auto-discover config files and set OPENCODE_CONFIG + const fs = await import("fs") + const path = await import("path") + const defaultConfigs = [".github/opencode.json", "opencode.json", ".opencode/config.json"] + + for (const defaultConfig of defaultConfigs) { + if (fs.existsSync(defaultConfig)) { + const fullPath = path.resolve(defaultConfig) + console.log(`Found default config: ${defaultConfig}`) + console.log(`Setting OPENCODE_CONFIG to: ${fullPath}`) + process.env["OPENCODE_CONFIG"] = fullPath + break + } + } + } + } + + async function normalizeModel() { const value = process.env["MODEL"] - if (!value) throw new Error(`Environment variable "MODEL" is not set`) + + // If MODEL is not set, try to get default from config + if (!value) { + try { + const cfg = await Config.get() + if (cfg.model) { + console.log(`Using default model from config: ${cfg.model}`) + const { providerID, modelID } = Provider.parseModel(cfg.model) + if (!providerID.length || !modelID.length) + throw new Error(`Invalid model ${cfg.model}. Model must be in the format "provider/model".`) + return { providerID, modelID } + } + } catch (error) { + console.warn(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`) + } + throw new Error(`Environment variable "MODEL" is not set and no default model found in config`) + } const { providerID, modelID } = Provider.parseModel(value) @@ -694,18 +778,18 @@ export const GithubRunCommand = cmd({ async function exchangeForAppToken(token: string) { const response = token.startsWith("github_pat_") ? await fetch("https://api.opencode.ai/exchange_github_app_token_with_pat", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ owner, repo }), - }) + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ owner, repo }), + }) : await fetch("https://api.opencode.ai/exchange_github_app_token", { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }) + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + }) if (!response.ok) { const responseJson = (await response.json()) as { error?: string }