diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 18e6cf7acb4..b9c4630a3db 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,6 +5,7 @@ on: branches: - dev - production + - beta workflow_dispatch: concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -15,7 +16,7 @@ permissions: jobs: deploy: - if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production') + if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production' || github.ref_name == 'beta') runs-on: ubuntu-latest environment: ${{ github.ref_name }} steps: @@ -28,6 +29,7 @@ jobs: node-version: "24" - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + if: github.ref_name != 'beta' with: role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }} role-session-name: opencode-${{ github.run_id }} diff --git a/infra/app.ts b/infra/app.ts index 45b2c5ccc8b..651059cb90f 100644 --- a/infra/app.ts +++ b/infra/app.ts @@ -1,4 +1,5 @@ import { domain } from "./stage" +import { createWebApp } from "./webapp" const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID") const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY") @@ -59,12 +60,4 @@ new sst.cloudflare.x.Astro("Web", { }, }) -new sst.cloudflare.StaticSite("WebApp", { - domain: "app." + domain, - path: "packages/app", - build: { - // Preserve Sentry credentials and run source-map uploads on every deployment. - command: "bun run build", - output: "./dist", - }, -}) +createWebApp("app." + domain) diff --git a/infra/webapp.ts b/infra/webapp.ts new file mode 100644 index 00000000000..7110a7b60b4 --- /dev/null +++ b/infra/webapp.ts @@ -0,0 +1,18 @@ +export function createWebApp(domain: string) { + return new sst.cloudflare.StaticSite("WebApp", { + domain, + path: "packages/app", + environment: + $app.stage === "beta" + ? { + OPENCODE_CHANNEL: "beta", + VITE_SENTRY_ENVIRONMENT: "beta", + } + : undefined, + build: { + // Preserve Sentry credentials and run source-map uploads on every deployment. + command: "bun run build", + output: "./dist", + }, + }) +} diff --git a/packages/app/README.md b/packages/app/README.md index c8011e88ec5..01c36687488 100644 --- a/packages/app/README.md +++ b/packages/app/README.md @@ -71,4 +71,25 @@ Environment options: ## Deployment -You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) +The `deploy` GitHub Actions workflow uses SST to deploy the web app from these branches in `anomalyco/opencode`: + +| Branch | Site | +| ------------ | --------------------- | +| `dev` | `app.dev.opencode.ai` | +| `production` | `app.opencode.ai` | +| `beta` | `beta.opencode.ai` | + +Changes merged into `v2` reach the beta site when they are promoted to `beta`. The beta SST stage deploys +only the web app, using the same `WebApp` StaticSite definition as production. It sets the build channel +and Sentry environment to `beta` without deploying the API, console, database, or billing infrastructure. + +The hosted app defaults to `http://localhost:49374`, matching the managed V2 service. Saved server selections +override this default. Connecting still requires the service's credentials. + +The workflow reuses the repository's `CLOUDFLARE_API_TOKEN` and web Sentry settings. The Cloudflare token +must cover SST's R2 state storage, KV assets, Workers, and custom-domain management in the account that +owns `opencode.ai`. The beta GitHub environment must allow deployments from the `beta` branch; it does not +need AWS credentials. + +SST manages the beta site's custom domain. The first deployment creates its DNS record and TLS certificate. +Do not create a CNAME for `beta.opencode.ai` first, because it would conflict with the Workers custom domain. diff --git a/packages/app/src/runtime/platform/web.ts b/packages/app/src/runtime/platform/web.ts index dc163fa7a03..fa9cab3cbdf 100644 --- a/packages/app/src/runtime/platform/web.ts +++ b/packages/app/src/runtime/platform/web.ts @@ -53,7 +53,7 @@ export function createWebPlatform(version: string) { } function getCurrentServerUrl() { - if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" + if (location.hostname.includes("opencode.ai")) return "http://localhost:49374" if (import.meta.env.DEV) return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` return location.origin diff --git a/sst.config.ts b/sst.config.ts index b245a51324c..21fb25f3bf3 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -7,27 +7,35 @@ export default $config({ removal: input?.stage === "production" ? "retain" : "remove", protect: ["production"].includes(input?.stage), home: "cloudflare", - providers: { - aws: { - version: "7.30.0", - region: "us-east-1", - profile: process.env.GITHUB_ACTIONS - ? undefined - : input.stage === "production" - ? "opencode-production" - : "opencode-dev", - }, - stripe: { - version: "0.0.28", - apiKey: process.env.STRIPE_SECRET_KEY!, - }, - random: "4.19.2", - planetscale: "0.4.1", - honeycomb: "0.49.0", - }, + providers: + input.stage === "beta" + ? {} + : { + aws: { + version: "7.30.0", + region: "us-east-1", + profile: process.env.GITHUB_ACTIONS + ? undefined + : input.stage === "production" + ? "opencode-production" + : "opencode-dev", + }, + stripe: { + version: "0.0.28", + apiKey: process.env.STRIPE_SECRET_KEY!, + }, + random: "4.19.2", + planetscale: "0.4.1", + honeycomb: "0.49.0", + }, } }, async run() { + if ($app.stage === "beta") { + const { createWebApp } = await import("./infra/webapp.js") + return { WebAppUrl: createWebApp("beta.opencode.ai").url } + } + const stage = await import("./infra/stage.js") await import("./infra/app.js") const lake = stage.deployAws ? await import("./infra/lake.js") : undefined