summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-07-16 14:58:32 +0800
committerFrank <[email protected]>2025-07-16 14:59:53 +0800
commita86d42149f52e4cb2b595016d1e81f04a0ecba3b (patch)
treef40bde648ad8c8db158863a5981b3ba318cabe3d /packages
parent82a36acfe36c112ace91042b68a07b9803a61aba (diff)
downloadopencode-a86d42149f52e4cb2b595016d1e81f04a0ecba3b.tar.gz
opencode-a86d42149f52e4cb2b595016d1e81f04a0ecba3b.zip
wip: github actions
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/opencode/script/publish-github-action.ts8
-rw-r--r--packages/opencode/src/cli/cmd/install-github.ts244
-rw-r--r--packages/opencode/src/index.ts2
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")