diff options
| author | Frank <[email protected]> | 2025-07-16 14:58:32 +0800 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-07-16 14:59:53 +0800 |
| commit | a86d42149f52e4cb2b595016d1e81f04a0ecba3b (patch) | |
| tree | f40bde648ad8c8db158863a5981b3ba318cabe3d /packages | |
| parent | 82a36acfe36c112ace91042b68a07b9803a61aba (diff) | |
| download | opencode-a86d42149f52e4cb2b595016d1e81f04a0ecba3b.tar.gz opencode-a86d42149f52e4cb2b595016d1e81f04a0ecba3b.zip | |
wip: github actions
Diffstat (limited to 'packages')
| -rwxr-xr-x | packages/opencode/script/publish-github-action.ts | 8 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/install-github.ts | 244 | ||||
| -rw-r--r-- | packages/opencode/src/index.ts | 2 |
3 files changed, 254 insertions, 0 deletions
diff --git a/packages/opencode/script/publish-github-action.ts b/packages/opencode/script/publish-github-action.ts new file mode 100755 index 000000000..2c4eb7c71 --- /dev/null +++ b/packages/opencode/script/publish-github-action.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env bun + +import { $ } from "bun" + +await $`git tag -d v1` +await $`git push origin :refs/tags/v1` +await $`git tag -a v1 -m "Update v1 to latest"` +await $`git push origin v1` diff --git a/packages/opencode/src/cli/cmd/install-github.ts b/packages/opencode/src/cli/cmd/install-github.ts new file mode 100644 index 000000000..18c295d63 --- /dev/null +++ b/packages/opencode/src/cli/cmd/install-github.ts @@ -0,0 +1,244 @@ +import { $ } from "bun" +import path from "path" +import { exec } from "child_process" +import * as prompts from "@clack/prompts" +import { map, pipe, sortBy, values } from "remeda" +import { UI } from "../ui" +import { cmd } from "./cmd" +import { ModelsDev } from "../../provider/models" +import { App } from "../../app/app" + +const WORKFLOW_FILE = ".github/workflows/opencode.yml" + +export const InstallGithubCommand = cmd({ + command: "install-github", + describe: "install the GitHub agent", + async handler() { + await App.provide({ cwd: process.cwd() }, async () => { + UI.empty() + prompts.intro("Install GitHub agent") + const app = await getAppInfo() + await installGitHubApp() + + const providers = await ModelsDev.get() + const provider = await promptProvider() + const model = await promptModel() + //const key = await promptKey() + + await addWorkflowFiles() + printNextSteps() + + function printNextSteps() { + let step2 + if (provider === "amazon-bedrock") { + step2 = + "Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services" + } else { + const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions` + const env = providers[provider].env + const envStr = + env.length === 1 + ? `\`${env[0]}\` secret` + : `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets` + step2 = `Add ${envStr} for ${providers[provider].name} - ${url}` + } + + prompts.outro( + [ + "Next steps:", + ` 1. Commit "${WORKFLOW_FILE}" file and push`, + ` 2. ${step2}`, + " 3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started", + ].join("\n"), + ) + } + + async function getAppInfo() { + const app = App.info() + if (!app.git) { + prompts.log.error(`Could not find git repository. Please run this command from a git repository.`) + throw new UI.CancelledError() + } + + // Get repo info + const info = await $`git remote get-url origin`.quiet().nothrow().text() + // match https or git pattern + // ie. https://github.com/sst/opencode.git + // ie. [email protected]:sst/opencode.git + const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/) + if (!parsed) { + prompts.log.error(`Could not find git repository. Please run this command from a git repository.`) + throw new UI.CancelledError() + } + const [owner, repo] = parsed[1].split("/") + return { owner, repo, root: app.path.root } + } + + async function promptProvider() { + const priority: Record<string, number> = { + anthropic: 0, + "github-copilot": 1, + openai: 2, + google: 3, + } + let provider = await prompts.select({ + message: "Select provider", + maxItems: 8, + options: [ + ...pipe( + providers, + values(), + sortBy( + (x) => priority[x.id] ?? 99, + (x) => x.name ?? x.id, + ), + map((x) => ({ + label: x.name, + value: x.id, + hint: priority[x.id] === 0 ? "recommended" : undefined, + })), + ), + { + value: "other", + label: "Other", + }, + ], + }) + + if (prompts.isCancel(provider)) throw new UI.CancelledError() + if (provider === "other") { + provider = await prompts.text({ + message: "Enter provider id", + validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"), + }) + if (prompts.isCancel(provider)) throw new UI.CancelledError() + provider = provider.replace(/^@ai-sdk\//, "") + if (prompts.isCancel(provider)) throw new UI.CancelledError() + prompts.log.warn( + `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`, + ) + } + + return provider + } + + async function promptModel() { + const providerData = providers[provider]! + + const model = await prompts.select({ + message: "Select model", + maxItems: 8, + options: pipe( + providerData.models, + values(), + sortBy((x) => x.name ?? x.id), + map((x) => ({ + label: x.name ?? x.id, + value: x.id, + })), + ), + }) + + if (prompts.isCancel(model)) throw new UI.CancelledError() + return model + } + + async function promptKey() { + const key = await prompts.password({ + message: "Enter your API key", + validate: (x) => (x.length > 0 ? undefined : "Required"), + }) + if (prompts.isCancel(key)) throw new UI.CancelledError() + return key + } + + async function installGitHubApp() { + const s = prompts.spinner() + s.start("Installing GitHub app") + + // Get installation + const installation = await getInstallation() + if (installation) return s.stop("GitHub app already installed") + + // Open browser + const url = "https://github.com/apps/opencode-agent" + const command = + process.platform === "darwin" + ? `open "${url}"` + : process.platform === "win32" + ? `start "${url}"` + : `xdg-open "${url}"` + + exec(command, (error) => { + if (error) { + prompts.log.warn(`Could not open browser. Please visit: ${url}`) + } + }) + + // Wait for installation + s.message("Waiting for GitHub app to be installed") + const MAX_RETRIES = 60 + let retries = 0 + do { + const installation = await getInstallation() + if (installation) break + + if (retries > MAX_RETRIES) { + s.stop( + `Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`, + ) + throw new UI.CancelledError() + } + + retries++ + await new Promise((resolve) => setTimeout(resolve, 1000)) + } while (true) + + s.stop("Installed GitHub app") + + async function getInstallation() { + return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`) + .then((res) => res.json()) + .then((data) => data.installation) + } + } + + async function addWorkflowFiles() { + const envStr = + provider === "amazon-bedrock" + ? "" + : `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}` + + await Bun.write( + path.join(app.root, WORKFLOW_FILE), + ` +name: opencode + +on: + issue_comment: + types: [created] + +jobs: + opencode: + if: startsWith(github.event.comment.body, 'hey opencode') + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run opencode + uses: sst/opencode/sdks/github@dev${envStr} + with: + model: ${provider}/${model} +`.trim(), + ) + + prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`) + } + }) + }, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index f0fc9e115..df7f1aa88 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -16,6 +16,7 @@ import { TuiCommand } from "./cli/cmd/tui" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" +import { InstallGithubCommand } from "./cli/cmd/install-github" const cancel = new AbortController() @@ -76,6 +77,7 @@ const cli = yargs(hideBin(process.argv)) .command(ServeCommand) .command(ModelsCommand) .command(StatsCommand) + .command(InstallGithubCommand) .fail((msg) => { if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) { cli.showHelp("log") |
