Skip to content

Commit a86d421

Browse files
author
Frank
committed
wip: github actions
1 parent 82a36ac commit a86d421

4 files changed

Lines changed: 443 additions & 237 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bun
2+
3+
import { $ } from "bun"
4+
5+
await $`git tag -d v1`
6+
await $`git push origin :refs/tags/v1`
7+
await $`git tag -a v1 -m "Update v1 to latest"`
8+
await $`git push origin v1`
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import { $ } from "bun"
2+
import path from "path"
3+
import { exec } from "child_process"
4+
import * as prompts from "@clack/prompts"
5+
import { map, pipe, sortBy, values } from "remeda"
6+
import { UI } from "../ui"
7+
import { cmd } from "./cmd"
8+
import { ModelsDev } from "../../provider/models"
9+
import { App } from "../../app/app"
10+
11+
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
12+
13+
export const InstallGithubCommand = cmd({
14+
command: "install-github",
15+
describe: "install the GitHub agent",
16+
async handler() {
17+
await App.provide({ cwd: process.cwd() }, async () => {
18+
UI.empty()
19+
prompts.intro("Install GitHub agent")
20+
const app = await getAppInfo()
21+
await installGitHubApp()
22+
23+
const providers = await ModelsDev.get()
24+
const provider = await promptProvider()
25+
const model = await promptModel()
26+
//const key = await promptKey()
27+
28+
await addWorkflowFiles()
29+
printNextSteps()
30+
31+
function printNextSteps() {
32+
let step2
33+
if (provider === "amazon-bedrock") {
34+
step2 =
35+
"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"
36+
} else {
37+
const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions`
38+
const env = providers[provider].env
39+
const envStr =
40+
env.length === 1
41+
? `\`${env[0]}\` secret`
42+
: `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets`
43+
step2 = `Add ${envStr} for ${providers[provider].name} - ${url}`
44+
}
45+
46+
prompts.outro(
47+
[
48+
"Next steps:",
49+
` 1. Commit "${WORKFLOW_FILE}" file and push`,
50+
` 2. ${step2}`,
51+
" 3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started",
52+
].join("\n"),
53+
)
54+
}
55+
56+
async function getAppInfo() {
57+
const app = App.info()
58+
if (!app.git) {
59+
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
60+
throw new UI.CancelledError()
61+
}
62+
63+
// Get repo info
64+
const info = await $`git remote get-url origin`.quiet().nothrow().text()
65+
// match https or git pattern
66+
// ie. https://github.com/sst/opencode.git
67+
// ie. git@github.com:sst/opencode.git
68+
const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/)
69+
if (!parsed) {
70+
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
71+
throw new UI.CancelledError()
72+
}
73+
const [owner, repo] = parsed[1].split("/")
74+
return { owner, repo, root: app.path.root }
75+
}
76+
77+
async function promptProvider() {
78+
const priority: Record<string, number> = {
79+
anthropic: 0,
80+
"github-copilot": 1,
81+
openai: 2,
82+
google: 3,
83+
}
84+
let provider = await prompts.select({
85+
message: "Select provider",
86+
maxItems: 8,
87+
options: [
88+
...pipe(
89+
providers,
90+
values(),
91+
sortBy(
92+
(x) => priority[x.id] ?? 99,
93+
(x) => x.name ?? x.id,
94+
),
95+
map((x) => ({
96+
label: x.name,
97+
value: x.id,
98+
hint: priority[x.id] === 0 ? "recommended" : undefined,
99+
})),
100+
),
101+
{
102+
value: "other",
103+
label: "Other",
104+
},
105+
],
106+
})
107+
108+
if (prompts.isCancel(provider)) throw new UI.CancelledError()
109+
if (provider === "other") {
110+
provider = await prompts.text({
111+
message: "Enter provider id",
112+
validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
113+
})
114+
if (prompts.isCancel(provider)) throw new UI.CancelledError()
115+
provider = provider.replace(/^@ai-sdk\//, "")
116+
if (prompts.isCancel(provider)) throw new UI.CancelledError()
117+
prompts.log.warn(
118+
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
119+
)
120+
}
121+
122+
return provider
123+
}
124+
125+
async function promptModel() {
126+
const providerData = providers[provider]!
127+
128+
const model = await prompts.select({
129+
message: "Select model",
130+
maxItems: 8,
131+
options: pipe(
132+
providerData.models,
133+
values(),
134+
sortBy((x) => x.name ?? x.id),
135+
map((x) => ({
136+
label: x.name ?? x.id,
137+
value: x.id,
138+
})),
139+
),
140+
})
141+
142+
if (prompts.isCancel(model)) throw new UI.CancelledError()
143+
return model
144+
}
145+
146+
async function promptKey() {
147+
const key = await prompts.password({
148+
message: "Enter your API key",
149+
validate: (x) => (x.length > 0 ? undefined : "Required"),
150+
})
151+
if (prompts.isCancel(key)) throw new UI.CancelledError()
152+
return key
153+
}
154+
155+
async function installGitHubApp() {
156+
const s = prompts.spinner()
157+
s.start("Installing GitHub app")
158+
159+
// Get installation
160+
const installation = await getInstallation()
161+
if (installation) return s.stop("GitHub app already installed")
162+
163+
// Open browser
164+
const url = "https://github.com/apps/opencode-agent"
165+
const command =
166+
process.platform === "darwin"
167+
? `open "${url}"`
168+
: process.platform === "win32"
169+
? `start "${url}"`
170+
: `xdg-open "${url}"`
171+
172+
exec(command, (error) => {
173+
if (error) {
174+
prompts.log.warn(`Could not open browser. Please visit: ${url}`)
175+
}
176+
})
177+
178+
// Wait for installation
179+
s.message("Waiting for GitHub app to be installed")
180+
const MAX_RETRIES = 60
181+
let retries = 0
182+
do {
183+
const installation = await getInstallation()
184+
if (installation) break
185+
186+
if (retries > MAX_RETRIES) {
187+
s.stop(
188+
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
189+
)
190+
throw new UI.CancelledError()
191+
}
192+
193+
retries++
194+
await new Promise((resolve) => setTimeout(resolve, 1000))
195+
} while (true)
196+
197+
s.stop("Installed GitHub app")
198+
199+
async function getInstallation() {
200+
return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
201+
.then((res) => res.json())
202+
.then((data) => data.installation)
203+
}
204+
}
205+
206+
async function addWorkflowFiles() {
207+
const envStr =
208+
provider === "amazon-bedrock"
209+
? ""
210+
: `\n env:${providers[provider].env.map((e) => `\n ${e}: \${{ secrets.${e} }}`).join("")}`
211+
212+
await Bun.write(
213+
path.join(app.root, WORKFLOW_FILE),
214+
`
215+
name: opencode
216+
217+
on:
218+
issue_comment:
219+
types: [created]
220+
221+
jobs:
222+
opencode:
223+
if: startsWith(github.event.comment.body, 'hey opencode')
224+
runs-on: ubuntu-latest
225+
permissions:
226+
id-token: write
227+
steps:
228+
- name: Checkout repository
229+
uses: actions/checkout@v4
230+
with:
231+
fetch-depth: 1
232+
233+
- name: Run opencode
234+
uses: sst/opencode/sdks/github@dev${envStr}
235+
with:
236+
model: ${provider}/${model}
237+
`.trim(),
238+
)
239+
240+
prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
241+
}
242+
})
243+
},
244+
})

packages/opencode/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { TuiCommand } from "./cli/cmd/tui"
1616
import { DebugCommand } from "./cli/cmd/debug"
1717
import { StatsCommand } from "./cli/cmd/stats"
1818
import { McpCommand } from "./cli/cmd/mcp"
19+
import { InstallGithubCommand } from "./cli/cmd/install-github"
1920

2021
const cancel = new AbortController()
2122

@@ -76,6 +77,7 @@ const cli = yargs(hideBin(process.argv))
7677
.command(ServeCommand)
7778
.command(ModelsCommand)
7879
.command(StatsCommand)
80+
.command(InstallGithubCommand)
7981
.fail((msg) => {
8082
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
8183
cli.showHelp("log")

0 commit comments

Comments
 (0)