chore: move the data site off sst

the stats site, its og worker and the sync workflow deploy from services/data with cf as
opencode-data-<stage>, opencode-data-og-<stage> and opencode-data-sync-<stage>. the beta
web app and the whole sst setup (sst.config.ts, infra/, sst-env.d.ts, the sst dependency)
are gone, so nothing deploys through sst anymore (security finding #920).
This commit is contained in:
Dax Raad
2026-11-10 22:46:24 -05:00
Unverified
parent 39457ccfb3
commit 36f559b0c6
70 changed files with 497 additions and 678 deletions
+49 -52
View File
@@ -5,7 +5,6 @@ on:
branches:
- dev
- production
- beta
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -14,58 +13,8 @@ permissions:
contents: read
jobs:
deploy:
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production' || github.ref_name == 'beta')
runs-on: blacksmith-4vcpu-ubuntu-2404
environment: ${{ github.ref_name }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
# beta has no branch, environment or AWS role. Running it again needs a role trusting environment `beta` and
# opencode/beta/CLOUDFLARE_API_TOKEN and opencode/beta/PLANETSCALE_SERVICE_TOKEN.
- name: Configure AWS credentials
if: github.ref_name != 'beta'
uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
with:
role-to-assume: arn:aws:iam::604439173067:role/github/opencode-${{ github.ref_name }}
role-session-name: opencode-${{ github.run_id }}
aws-region: us-east-1
- name: Read deploy secrets
if: github.ref_name != 'beta'
uses: aws-actions/aws-secretsmanager-get-secrets@2297f9a879480a9e3af9b293ed15c70caf8e1c88 # v3.0.2
with:
secret-ids: |
CLOUDFLARE_API_TOKEN,opencode/${{ github.ref_name }}/CLOUDFLARE_API_TOKEN
PLANETSCALE_SERVICE_TOKEN,opencode/${{ github.ref_name }}/PLANETSCALE_SERVICE_TOKEN
parse-json-secrets: false
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ github.token }}
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ vars.PLANETSCALE_SERVICE_TOKEN_NAME }}
# Only the beta stage's web app uploads sourcemaps; packages/app skips the upload without a token.
SENTRY_AUTH_TOKEN: ${{ github.ref_name == 'beta' && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
SENTRY_RELEASE: web@${{ github.sha }}
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
VITE_SENTRY_RELEASE: web@${{ github.sha }}
# The SST stack owned api.opencode.ai until this push removes its Api Worker, so this job runs after the
# SST deploy to take the hostname without a custom-domain conflict.
legacy:
needs: deploy
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
runs-on: blacksmith-4vcpu-ubuntu-2404
environment: ${{ github.ref_name }}
permissions:
@@ -106,3 +55,51 @@ jobs:
jq --arg name "$name" '{($name): .SecretString}'
done | jq -s add > "$secrets"
bun run deploy:$STAGE --secrets-file "$secrets"
# The /data site, its OG image Worker and the sync Workflow (services/data).
data:
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
runs-on: blacksmith-4vcpu-ubuntu-2404
environment: ${{ github.ref_name }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
with:
role-to-assume: arn:aws:iam::604439173067:role/github/opencode-${{ github.ref_name }}
role-session-name: opencode-${{ github.run_id }}
aws-region: us-east-1
- name: Read deploy secrets
uses: aws-actions/aws-secretsmanager-get-secrets@2297f9a879480a9e3af9b293ed15c70caf8e1c88 # v3.0.2
with:
secret-ids: CLOUDFLARE_API_TOKEN,opencode/${{ github.ref_name }}/CLOUDFLARE_API_TOKEN
parse-json-secrets: false
# The Workers' secrets are read here, not in the step above, so only this step ever has them. They reach cf
# through files in the runner's temp directory, never argv, and are removed when the step ends.
- name: Deploy
working-directory: services/data
env:
STAGE: ${{ github.ref_name }}
# An explicit bash runs with pipefail, so a failed secret read fails the step.
shell: bash
run: |
umask 077
secrets="$RUNNER_TEMP/data-secrets.json"
trap 'rm -f "$secrets" "$secrets".*' EXIT
for name in DATA_DATABASE_HOST DATA_DATABASE_USERNAME DATA_DATABASE_PASSWORD R2_SQL_AUTH_TOKEN DATA_HIDDEN_MODELS; do
aws secretsmanager get-secret-value --secret-id "opencode/$STAGE/$name" --output json |
jq --arg name "$name" '{($name): .SecretString}'
done | jq -s add > "$secrets"
bun run deploy:$STAGE --secrets-file "$secrets"
-1
View File
@@ -1,7 +1,6 @@
.DS_Store
node_modules
.worktrees
.sst
.env
.env.local
.idea
+15 -43
View File
@@ -24,7 +24,6 @@
"oxlint": "1.60.0",
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "catalog:",
"turbo": "2.10.2",
},
},
@@ -684,7 +683,6 @@
"nitro": "3.0.260903-beta",
"satori": "0.63.2",
"solid-js": "catalog:",
"sst": "catalog:",
"vite": "8.2.4",
},
"devDependencies": {
@@ -702,7 +700,6 @@
"@planetscale/database": "1.19.0",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"sst": "catalog:",
},
"devDependencies": {
"@tsconfig/node22": "catalog:",
@@ -719,7 +716,6 @@
"dependencies": {
"@opencode/stats-core": "workspace:*",
"effect": "catalog:",
"sst": "catalog:",
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
@@ -869,6 +865,19 @@
"jsqr": "1.4.0",
},
},
"services/data": {
"name": "@opencode/data",
"devDependencies": {
"@cloudflare/build-output-utils": "0.8.5",
"@cloudflare/config": "0.23.0",
"@opencode/stats-app": "workspace:*",
"@opencode/stats-server": "workspace:*",
"@types/bun": "catalog:",
"@typescript/native": "catalog:",
"cf": "1.0.0-beta.12",
"typescript": "catalog:",
},
},
"services/files": {
"name": "@opencode/files",
"devDependencies": {
@@ -1065,7 +1074,6 @@
"solid-js": "1.9.17",
"solid-list": "0.3.0",
"solid-sonner": "0.3.1",
"sst": "4.13.1",
"string-width": "7.2.0",
"tailwindcss": "4.1.11",
"typescript": "npm:@typescript/typescript6@6.0.2",
@@ -1136,8 +1144,6 @@
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1057.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.47", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-5MliYkp2u0+2arTp5fZIaxl+xmm90LEKv/VeSxhfNQW4t0fvWJrNO429/jchWQenNoDRrOGE59VfbuZUfwFujg=="],
"@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-node": "^3.972.80", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-jG2rB6LY2rx3XT55axgH3C0aIv1egvI0kqPYC6jfghVp82kWssU7R/1dJxXr0PnPLv4wq+WXRyLEmjyKURUjFQ=="],
"@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="],
"@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.68", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Fo2RC5hZmEVhnVLMzNjYL6Iu78F4wDuaNtkgzRVU4DlUaFhcXxsnxWBOTH2drxZ59OHSipQ1LndfkCWPxGBJHA=="],
@@ -1172,8 +1178,6 @@
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="],
"@aws/durable-execution-sdk-js": ["@aws/durable-execution-sdk-js@1.0.2", "", { "dependencies": { "@aws-sdk/client-lambda": "^3.943.0" } }, "sha512-KIYBVqV9gArkWdPEDYOjJqLlO+NecmCXOXadNBlOspJTmqztwuiyb6aZc468bYWSGuL4Me/gyTIK97ubkwFNCw=="],
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
@@ -1792,6 +1796,8 @@
"@opencode/core": ["@opencode/core@workspace:packages/core"],
"@opencode/data": ["@opencode/data@workspace:services/data"],
"@opencode/desktop": ["@opencode/desktop@workspace:packages/desktop"],
"@opencode/files": ["@opencode/files@workspace:services/files"],
@@ -3728,8 +3734,6 @@
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
@@ -3738,8 +3742,6 @@
"ocache": ["ocache@0.3.1", "", {}, "sha512-qeBrhQ0ly6tgXsakgNwLx6diRUppJZIcV5OjVGa5yp9HzztutkaGDDXJqVMo1fKWz5fNDtTp3iPU2fuhHf0+Lg=="],
"oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="],
"on-finished": ["on-finished@2.4.2", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -3752,8 +3754,6 @@
"openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="],
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
"own-keys": ["own-keys@1.0.2", "", { "dependencies": { "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg=="],
"oxc-parser": ["oxc-parser@0.141.0", "", { "dependencies": { "@oxc-project/types": "^0.141.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.141.0", "@oxc-parser/binding-android-arm64": "0.141.0", "@oxc-parser/binding-darwin-arm64": "0.141.0", "@oxc-parser/binding-darwin-x64": "0.141.0", "@oxc-parser/binding-freebsd-x64": "0.141.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.141.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.141.0", "@oxc-parser/binding-linux-arm64-gnu": "0.141.0", "@oxc-parser/binding-linux-arm64-musl": "0.141.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.141.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.141.0", "@oxc-parser/binding-linux-riscv64-musl": "0.141.0", "@oxc-parser/binding-linux-s390x-gnu": "0.141.0", "@oxc-parser/binding-linux-x64-gnu": "0.141.0", "@oxc-parser/binding-linux-x64-musl": "0.141.0", "@oxc-parser/binding-openharmony-arm64": "0.141.0", "@oxc-parser/binding-wasm32-wasi": "0.141.0", "@oxc-parser/binding-win32-arm64-msvc": "0.141.0", "@oxc-parser/binding-win32-ia32-msvc": "0.141.0", "@oxc-parser/binding-win32-x64-msvc": "0.141.0" } }, "sha512-uFkGGr1KMWd6aWv9UAqooYrN78trw8MWWmoPvgWokfBEUq1+eiIQ+qfj3wokhy0fxtZWZk+0dHoS7/yRTJtd6w=="],
@@ -4132,24 +4132,6 @@
"ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="],
"sst": ["sst@4.13.1", "", { "dependencies": { "@aws/durable-execution-sdk-js": "1.0.2", "aws4fetch": "1.0.18", "jose": "5.2.3", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "4.13.1", "sst-darwin-x64": "4.13.1", "sst-linux-arm64": "4.13.1", "sst-linux-x64": "4.13.1", "sst-linux-x86": "4.13.1", "sst-win32-arm64": "4.13.1", "sst-win32-x64": "4.13.1", "sst-win32-x86": "4.13.1" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-FKzhWuqV5NPhFWH7b+Ktf5+Mvox6DCMc8iOrjaS3mkjnnmyEcOZq7HNx2/ZlIcBwgOZE9sCPR3ot1zXtXkXzZg=="],
"sst-darwin-arm64": ["sst-darwin-arm64@4.13.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGwfgZGOsulU6iGeZJtxP5BpSZFsRUy1dG/yTqJa++n1P1APWXZC64u2qRHm6rdwLAjEeWaCQ9k7ouKgn0zh5Q=="],
"sst-darwin-x64": ["sst-darwin-x64@4.13.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-RcKLI2oAY+Wk5fWUF0gJFKvwMSpid3qw9L3OcfgFL2ymZH652ybLpQPBxtoJ95FdyknWIMnYf6ZK9C2jnH5eaw=="],
"sst-linux-arm64": ["sst-linux-arm64@4.13.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Lp/8BWhH8KZnb50xPd8A6bkZ7QqCH3Dar9l16TPwuAmPxxRUlkmg42h+ACZ9n02vSJl8y/WLnV4JB6SgJpRFVg=="],
"sst-linux-x64": ["sst-linux-x64@4.13.1", "", { "os": "linux", "cpu": "x64" }, "sha512-f4eHOsApoP+CpUzLUGzfoTBYsqqr6nl/M0gjYsVNi1T+6v92lc84AhQ4DoDZx033QggyxkqRezj8jOkjkKxa7w=="],
"sst-linux-x86": ["sst-linux-x86@4.13.1", "", { "os": "linux", "cpu": "none" }, "sha512-5OHdtPa0GmozDpBPdsD9Tv+SGnpjJSoTXptgK59ivQg3UYNWBBotidvEyUJs1MU9B/3+I+Fmw32zsF2O+ZtPgg=="],
"sst-win32-arm64": ["sst-win32-arm64@4.13.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-qAUuCQj6wDdZ84a9W4FhnRTHYlEnYRHGYFqO3cmMImbnAQEoKSQR1XuWqt0Ev4RT3kU4Lz/fgvJjGkR7WUstLg=="],
"sst-win32-x64": ["sst-win32-x64@4.13.1", "", { "os": "win32", "cpu": "x64" }, "sha512-r+83NMwpe4MLAvkVoOxi1KNs6nIqilFucsTr+VN1PgknsiVBY1emITjSWS2jDJJMuH8xJXL5xJzF/l5rcKWErg=="],
"sst-win32-x86": ["sst-win32-x86@4.13.1", "", { "os": "win32", "cpu": "none" }, "sha512-YPxBVdac/MsrzwlC6pF0NrrvMcmfdBLYjv7MbzHc5jNh1FQ1WPh6bdWQqgv0KD9EQTNLLEkej0beydgUvcCWJg=="],
"standardwebhooks": ["standardwebhooks@1.1.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ=="],
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
@@ -4808,10 +4790,6 @@
"npm-package-arg/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.141.0", "", {}, "sha512-S4as7z0j0xQkXcJlyY5ehntwK8/wRkQb9Cyqw+J/N2rkWGQGK0SxD6X6DhQTc7qsxVTBxXbxZtBJh3mr3PtIzQ=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
@@ -4862,10 +4840,6 @@
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -5162,8 +5136,6 @@
"nitro/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.14", "", { "os": "win32", "cpu": "x64" }, "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ=="],
"openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
-9
View File
@@ -1,9 +0,0 @@
export const domain = (() => {
if ($app.stage === "production") return "opencode.ai"
if ($app.stage === "dev") return "dev.opencode.ai"
return `${$app.stage}.dev.opencode.ai`
})()
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
export const awsStage = $app.stage === "production" ? "production" : "dev"
export const deployStats = $app.stage === awsStage
-215
View File
@@ -1,215 +0,0 @@
import { execFileSync } from "node:child_process"
import { readdirSync } from "node:fs"
import path from "node:path"
import { LOCALES } from "../packages/stats/app/src/lib/language"
import { domain, zoneID } from "./stage"
////////////////
// DATABASE
////////////////
const cluster = planetscale.getDatabaseOutput({
name: "opencode-stats",
organization: "anomalyco",
})
const branch =
$app.stage === "production"
? planetscale.getBranchOutput({
name: "production",
organization: cluster.organization,
database: cluster.name,
})
: new planetscale.Branch("StatsDatabaseBranch", {
database: cluster.name,
organization: cluster.organization,
name: $app.stage,
parentBranch: "production",
})
const password = new planetscale.Password("StatsDatabasePassword", {
name: $app.stage,
database: cluster.name,
organization: cluster.organization,
branch: branch.name,
})
const databaseUrl = $interpolate`mysql://${password.username.apply(encodeURIComponent)}:${password.plaintext.apply(
encodeURIComponent,
)}@${password.accessHostUrl}/${cluster.name}`
export const database = new sst.Linkable("StatsDatabase", {
properties: {
host: password.accessHostUrl,
database: cluster.name,
username: password.username,
password: password.plaintext,
port: 3306,
url: databaseUrl,
},
})
new sst.x.DevCommand("StatsStudio", {
link: [database],
environment: {
DATABASE_URL: databaseUrl,
},
dev: {
command: "bun db:studio",
directory: "packages/stats/core",
autostart: false,
},
})
////////////////
// APP
////////////////
export const publicUrl = `https://${domain}/data`
const accountId = "15d29c8639fd3733b1b5486a2acfd968"
// The private Worker that renders the pages' Open Graph images (packages/stats/app/src/og/worker.ts). Its Wasm and font
// modules can't ship in the Stats Worker, which SST uploads as one file, so it is its own Worker with no URL or route,
// reached only through the Stats Worker's OG_WORKER Service Binding. It is built here, before any resource reads its
// files. `sst dev` deploys no Worker.
const ogWorker = $dev ? undefined : createOgWorker()
const app = new sst.cloudflare.x.SolidStart("Stats", {
path: "packages/stats/app",
buildCommand: "bun run build",
link: [database],
environment: {
PUBLIC_URL: publicUrl,
},
transform: {
server: (args) => {
args.url = false
if (!ogWorker) return
args.transform = {
worker: (worker) => {
// The deployment's script name, so the binding waits for the OG Worker to have a live version.
worker.bindings = $util
.all([$output(worker.bindings ?? []), ogWorker.scriptName])
.apply(([bindings, service]) => [...bindings, { type: "service", name: "OG_WORKER", service }])
},
}
},
},
})
function createOgWorker() {
const root = path.join($cli.paths.root, "packages/stats/app")
execFileSync("bun", ["run", "build:og"], { cwd: root, stdio: "inherit" })
const dir = path.join(root, ".og-worker")
const types: Record<string, string> = {
".mjs": "application/javascript+module",
".wasm": "application/wasm",
".bin": "application/octet-stream",
}
const worker = new cloudflare.Worker("StatsOg", {
accountId,
name: `${$app.name}-${$app.stage}-stats-og`,
subdomain: { enabled: false, previewsEnabled: false },
observability: { enabled: true },
})
const version = new cloudflare.WorkerVersion("StatsOgVersion", {
accountId,
workerId: worker.id,
mainModule: "index.mjs",
compatibilityDate: "2026-07-01",
modules: readdirSync(dir).map((name) => ({
name,
contentFile: path.join(dir, name),
contentType: types[path.extname(name)],
})),
})
return new cloudflare.WorkersDeployment("StatsOgDeployment", {
accountId,
scriptName: worker.name,
strategy: "percentage",
versions: [{ percentage: 100, versionId: version.id }],
})
}
// The site serves itself under /data and /<locale>/data, and these routes are its only way in: it has no custom domain
// or workers.dev URL. An exact `/data` pattern misses `/data?query`, so each pattern is a `/data*` prefix. Route patterns
// can't wildcard the middle of a path, hence one per locale. `sst dev` deploys no Worker.
const appServer = app.nodes.server
if (appServer) {
for (const prefix of ["data", ...LOCALES.map((locale) => `${locale}/data`)]) {
new cloudflare.WorkersRoute(`StatsRoute${prefix.replaceAll("/", "")}`, {
zoneId: zoneID,
pattern: `${domain}/${prefix}*`,
script: appServer.nodes.worker.scriptName,
})
}
}
////////////////
// SERVICES
////////////////
const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
properties: {
dataset: "zen",
},
})
// Comma-separated model IDs kept off the public stats site. A secret, so excluding
// an unreleased model does not publish its name in git.
const statsHiddenModels = new sst.Secret("StatsHiddenModels", "")
const r2SqlAuthToken = new sst.Secret("R2SqlAuthToken")
const r2Sql = new sst.Linkable("R2Sql", {
properties: {
accountId,
bucket: `platform-${$app.stage}-lake`,
namespace: "inference",
table: "generation",
},
})
// One instance per run: a plan step, then one step per stat period and retention
// cohort. See packages/stats/README.md for triggering a run by hand.
export const statsSyncWorkflow = new sst.cloudflare.Workflow("StatsSyncWorkflow", {
handler: "packages/stats/server/src/workflow.ts",
className: "StatsSyncWorkflow",
link: [database, r2Sql, r2SqlAuthToken, statsHiddenModels, statsSyncConfig],
transform: {
worker: {
transform: {
worker: (args) => {
// Raise CPU time from the 30-second default to the 5-minute maximum for
// ranking a full week of geo rows.
args.limits = { cpuMs: 300_000 }
args.observability = { enabled: true }
},
},
},
},
})
// Schedules must match packages/stats/server/src/schedule.ts, which maps the
// daily one to a history run.
if (!$dev)
new sst.cloudflare.Cron("StatsSyncCron", {
schedules: ["5 * * * *", "20 3 * * *"],
worker: {
handler: "packages/stats/server/src/cron.ts",
link: [statsSyncWorkflow],
transform: {
worker: (args) => {
args.observability = { enabled: true }
},
},
},
})
new sst.x.DevCommand("StatsSync", {
link: [database, r2Sql, r2SqlAuthToken, statsHiddenModels, statsSyncConfig],
dev: {
command: "bun src/sync.ts",
directory: "packages/stats/server",
autostart: false,
},
})
-21
View File
@@ -1,21 +0,0 @@
export function createWebApp(domain: string) {
// Workers Static Assets uploads files as-is, up to 25 MiB each. StaticSite stored them base64-encoded in KV, which
// pushed the 20 MB Word preview engine past KV's 25 MiB value limit.
return new sst.cloudflare.StaticSiteV2("WebApp", {
domain,
path: "packages/app",
environment:
$app.stage === "beta"
? {
OPENCODE_CHANNEL: "beta",
VITE_OPENCODE_SERVER_MODE: "none",
VITE_SENTRY_ENVIRONMENT: "beta",
}
: undefined,
build: {
// Preserve Sentry credentials and run source-map uploads on every deployment.
command: "bun run build",
output: "./dist",
},
})
}
-3
View File
@@ -13,7 +13,6 @@
"dev:vite:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev:vite \"$@\" --server \"$(opencode service status)\"' --",
"dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev",
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
"dev:site": "bun run --cwd services/site dev",
"lint": "oxlint && ast-grep scan -c script/ast-grep/gui-plugins/sgconfig.yml && bun script/sdk-docs.ts",
"lint:changed": "bun script/lint-changed.ts",
@@ -91,7 +90,6 @@
"@typescript/native": "npm:typescript@7.1.0-beta",
"zod": "4.6.7",
"remeda": "2.26.0",
"sst": "4.13.1",
"shiki": "4.4.3",
"solid-list": "0.3.0",
"string-width": "7.2.0",
@@ -123,7 +121,6 @@
"oxlint": "1.60.0",
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "catalog:",
"turbo": "2.10.2"
},
"dependencies": {
+2 -21
View File
@@ -69,24 +69,13 @@ Environment options:
- `PLAYWRIGHT_PORT` (managed dev or static server port, default: `3000`)
- `PLAYWRIGHT_BASE_URL` (use an externally managed app instead of starting a server; otherwise defaults to `http://127.0.0.1:<PLAYWRIGHT_PORT>`)
## Deployment
The `deploy` GitHub Actions workflow uses SST to deploy the web app to `beta.opencode.ai` on every push to the
`beta` branch in `anomalyco/opencode`. The `dev` and `production` stages do not deploy the web app.
`beta` is the team's dogfooding branch: `dev` merged in by hand, plus commits pushed directly to it. Do not rebase or
force-push it. Each push also runs the `release` workflow, which ships the same commit as the OpenCode Beta desktop app
(see `packages/desktop/README.md`).
The beta SST stage deploys only the web app, using the `WebApp` StaticSiteV2 definition in `infra/webapp.ts`. It sets
the build channel and Sentry environment to `beta` without deploying the API, console, database, or billing
infrastructure.
## Server mode
`VITE_OPENCODE_SERVER_MODE` controls which server the web build provides at startup:
| Mode | Initial server |
| ------------------ | ------------------------------------------------------------------------------------ |
| `none` | No initial server. The beta deployment uses this mode. |
| `none` | No initial server. |
| `origin` (default) | The current page's origin. CLI builds explicitly use this mode for `opencode serve`. |
In Vite development mode, `origin` uses `VITE_OPENCODE_SERVER_HOST` / `VITE_OPENCODE_SERVER_PORT`
@@ -116,11 +105,3 @@ HTTPS-to-HTTP error instead of the generic connection failure. HTTP servers on `
browser local-network permissions can allow some HTTP LAN connections.
QR scanning is enabled only in a browser-reported secure context with camera support and an available
video input; insecure pages and unavailable cameras show an explanation beside the disabled action.
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.
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+1 -1
View File
@@ -37,7 +37,7 @@ default. `bun dev --download-server <version>` instead downloads that CLI versio
## Beta channel
Every push to the `beta` branch publishes OpenCode Beta (`ai.opencode.desktop.beta`) as version `0.0.0-beta-<run>` to
the `beta` update channel. Beta publishes only the desktop app and `beta.opencode.ai`; the CLI and SDK packages ship
the `beta` update channel. Beta publishes only the desktop app; the CLI and SDK packages ship
from `dev`. Install it from `https://opencode.ai/update/api/beta/desktop/opencode/<asset>`, for example
`opencode-desktop-mac-arm64.dmg` or `opencode-desktop-win-x64.exe`. Installed builds update in place on every platform.
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
@@ -4,7 +4,7 @@
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
@@ -4,7 +4,7 @@
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+1 -1
View File
@@ -1 +1 @@
To start the stats site locally, run `bun dev:stats` from the repo root.
To start the stats site locally, run `bun run --cwd packages/stats/app dev` with the database variables in README.md exported.
+12 -9
View File
@@ -6,40 +6,43 @@ Stats is a separate site from the console. Runtime, database, and domain service
- `app`: SolidStart frontend/site.
- `core`: Effect services, app config, Drizzle schema/migrations, and stats domains.
- `server`: the `StatsSyncWorkflow` Workflow, the `StatsSyncCron` Worker that starts it, and a local sync script.
- `server`: the `StatsSyncWorkflow` Workflow, the cron handler that starts it, and a local sync script.
`services/data` builds and deploys them as the `opencode-data-<stage>`, `opencode-data-og-<stage>` and `opencode-data-sync-<stage>` Workers.
## Commands
- `bun run dev:stats` from the repo root starts the SolidStart app.
- `bun run --cwd packages/stats/app dev` starts the SolidStart app (see Stats sync for its environment).
- `bun run --cwd packages/stats/app typecheck` typechecks the site.
- `bun run --cwd packages/stats/core typecheck` typechecks the Effect/database package.
- `bun run --cwd packages/stats/server typecheck` typechecks the sync Workers.
## Stats sync
The sync reads aggregates from R2 SQL and writes them to the stats PlanetScale database. It runs on the `dev` and `production` stages as a Cloudflare Workflow, `StatsSyncWorkflow`, defined in `infra/stats.ts`.
The sync reads aggregates from R2 SQL and writes them to the stats PlanetScale database. It runs on the `dev` and `production` stages as a Cloudflare Workflow, `StatsSyncWorkflow`, in the `opencode-data-sync-<stage>` Worker that `services/data` deploys.
Each run has a `plan` step, then one step per stat period (`week 2026-W45`, `day 2026-11-03`) and one per retention cohort (`retention 2026-10-05`). The plan snapshots the model catalog once, so every step attributes models the same way. Steps run one at a time and retry twice with exponential backoff. A rejected query (HTTP 400, 401 or 403) or a result over the 10,000-row limit fails its step after one attempt, since a retry repeats the same R2 SQL spend. A failed step does not stop the others, but the run ends as errored and logs each failed step with its error. Because queries run per period, expect an hourly run to take about 15 to 25 minutes and a history run about 2 to 3.5 hours. These are estimates until measured on a deployed stage.
The `StatsSyncCron` Worker starts runs on two schedules:
The same Worker's cron handler starts runs on two schedules:
- `5 * * * *`: an hourly run of the current ISO week and the most recent complete retention cohort, with instance ID `stats-hourly-YYYY-MM-DDTHH`.
- `20 3 * * *`: a daily history run of the rest of the 56-day display window, with instance ID `stats-history-YYYY-MM-DD`.
Times and IDs are in UTC. Before starting a run, the Cron checks this slot's ID and the recent IDs of the same kind: the five previous hours for an hourly run, the two previous days for a history run. It skips the run if any of them is queued, running, waiting or paused, or if this slot's instance already exists, and logs the reason. Hourly and history runs can overlap, since they cover different periods and their writes are idempotent.
Times and IDs are in UTC. Before starting a run, the cron handler checks this slot's ID and the recent IDs of the same kind: the five previous hours for an hourly run, the two previous days for a history run. It skips the run if any of them is queued, running, waiting or paused, or if this slot's instance already exists, and logs the reason. Hourly and history runs can overlap, since they cover different periods and their writes are idempotent.
To start a run by hand, find the Workflow's name with `bunx wrangler workflows list` (it is `opencode-<stage>-statssyncworkflowworkflow-<suffix>`), then trigger it with an ID from the Cron's convention so scheduled runs of the same kind see it and wait:
To start a run by hand, trigger the `opencode-data-sync-<stage>` Workflow with an ID from the cron handler's convention so scheduled runs of the same kind see it and wait:
```sh
bunx wrangler workflows trigger <workflow-name> '{"history":true}' --id stats-history-2026-11-03
cf workflows instances create opencode-data-sync-dev --instance-id stats-history-2026-11-03 --params '{"history":true}'
cf workflows instances get stats-history-2026-11-03 --workflow-name opencode-data-sync-dev
```
Omit `history` for an hourly run. If the ID already exists, that slot has run; wait for the next one. You can also trigger a run from the Workflow's page in the Cloudflare dashboard; without params it runs as an hourly pass.
To run one sync pass on your machine against a stage's resources, run `bun sst shell --stage=<stage> -- bun run --cwd packages/stats/server sync`, adding `--history` for a history pass. `sst dev` also offers it as the `StatsSync` command.
To run one sync pass on your machine against a stage's resources, export `STAGE`, `DATA_DATABASE_HOST`, `DATA_DATABASE_USERNAME`, `DATA_DATABASE_PASSWORD`, `R2_SQL_AUTH_TOKEN` and `DATA_HIDDEN_MODELS` (AWS Secrets Manager `opencode/<stage>/*`; `packages/stats/core/src/resource.ts` reads them), then run `bun run --cwd packages/stats/server sync`, adding `--history` for a history pass. `bun run --cwd packages/stats/app dev` reads the same database variables.
## Public page caching
Stats HTML, JSON, and Markdown use a one-minute browser cache and a five-minute shared cache. Expired responses must revalidate instead of serving stale snapshots.
The Worker has no custom domain. `infra/stats.ts` routes `/data*` and `/<locale>/data*` on the main host straight to it, so its responses skip the Cloudflare cache and reach browsers with these headers unchanged. Static assets are uploaded under `/data`, and the build moves Nitro's `_headers` to the assets root so they stay immutable.
The Worker has no custom domain. `services/data/cloudflare.config.ts` routes `/data*` and `/<locale>/data*` on the main host straight to it, so its responses skip the Cloudflare cache and reach browsers with these headers unchanged. Static assets are uploaded under `/data`, and the build moves Nitro's `_headers` to the assets root so they stay immutable.
-1
View File
@@ -35,7 +35,6 @@
"nitro": "3.0.260903-beta",
"satori": "0.63.2",
"solid-js": "catalog:",
"sst": "catalog:",
"vite": "8.2.4"
},
"devDependencies": {
+2 -2
View File
@@ -1,5 +1,5 @@
// Builds the StatsOg Worker (src/og/worker.ts) into .og-worker: the bundle, the Wasm engines, and the site's OpenTUI
// Mono fonts as TrueType (Satori can't read WOFF2). infra/stats.ts runs this and uploads every file there. It is
// Builds the OG Worker (src/og/worker.ts) into .og-worker: the bundle, the Wasm engines, and the site's OpenTUI
// Mono fonts as TrueType (Satori can't read WOFF2). services/data/script/build.ts runs this and deploys every file there. It is
// outside .output because the site's own build replaces that directory. test/og.test.ts builds it too, so PR CI
// catches a broken build before the deploy does.
import { mkdir, rm } from "node:fs/promises"
+5 -5
View File
@@ -1,5 +1,5 @@
import type { createMiddleware } from "@solidjs/start/middleware"
import { Resource } from "sst/resource"
import { Resource } from "@opencode/stats-core/resource"
// Only production may be indexed: dev.opencode.ai and the personal stages run unreleased builds, and
// search engines must never list them in place of opencode.ai. Every response there asks them not
@@ -9,12 +9,12 @@ import { Resource } from "sst/resource"
type Middleware = ReturnType<typeof createMiddleware>[number]
/**
* The headers every response from this stage carries: none in production, or outside SST (`vite dev` on its own),
* where no App is linked. SST links the App to the build as well as the Worker.
* The headers every response from this stage carries: none in production, or when STAGE is unset (`vite dev` on its
* own). services/data sets STAGE for the build as well as on the Worker.
*/
export function stageHeaders(): Record<string, string> {
const stage = "App" in Resource ? Resource.App.stage : "production"
return stage === "production" ? {} : { "X-Robots-Tag": "noindex, nofollow" }
if (!process.env.STAGE) return {}
return Resource.App.stage === "production" ? {} : { "X-Robots-Tag": "noindex, nofollow" }
}
// h3 adds the prepared headers to whatever a route returns, and the error headers to a status of 400 or more.
@@ -8,7 +8,7 @@ export { waitUntil } from "cloudflare:workers"
type Fetcher = { fetch(request: Request): Promise<Response> }
const bindings = env as { OG_WORKER: Fetcher; ASSETS: Fetcher }
// The private StatsOg Worker renders cards (infra/stats.ts, src/og/worker.ts).
// The private OG Worker renders cards (services/data/og.config.ts, src/og/worker.ts).
export async function renderOgCard(card: OgCard) {
const response = await bindings.OG_WORKER.fetch(
new Request("https://og/render", { method: "POST", body: JSON.stringify(card) }),
+3 -3
View File
@@ -1,6 +1,6 @@
// The private StatsOg Worker (infra/stats.ts). The Stats Worker posts a card to it over a Service Binding and gets a
// PNG back. It is its own Worker because its Wasm and font modules can't ship in the Stats Worker's single-file
// bundle. script/build-og-worker.ts bundles it and writes the modules imported here next to it.
// The private OG Worker (services/data/og.config.ts). The data Worker posts a card to it over a Service Binding and gets a
// PNG back. It is its own Worker so the data Worker doesn't load its Wasm engines and fonts.
// script/build-og-worker.ts bundles it and writes the modules imported here next to it.
import regularLatin from "./opentui-mono-regular-latin.bin"
import regularRest from "./opentui-mono-regular-rest.bin"
import boldLatin from "./opentui-mono-bold-latin.bin"
+1 -1
View File
@@ -19,5 +19,5 @@
"~/*": ["./src/*"]
}
},
"include": ["*.ts", "src", "../core/src/resource.d.ts"]
"include": ["*.ts", "src"]
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Resource } from "sst/resource"
import { Resource } from "./src/resource"
import { defineConfig } from "drizzle-kit"
export default defineConfig({
+2 -2
View File
@@ -12,6 +12,7 @@
"./database/*": "./src/database/*.ts",
"./domain/*": "./src/domain/*.ts",
"./r2-sql": "./src/r2-sql.ts",
"./resource": "./src/resource.ts",
"./runtime": "./src/runtime.ts",
"./stat-sync": "./src/stat-sync.ts",
"./sync-range": "./src/sync-range.ts"
@@ -31,8 +32,7 @@
"dependencies": {
"@planetscale/database": "1.19.0",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"sst": "catalog:"
"effect": "catalog:"
},
"devDependencies": {
"@tsconfig/node22": "catalog:",
+1 -1
View File
@@ -1,6 +1,6 @@
import { Config, ConfigProvider, Effect, Layer, Schema } from "effect"
import * as Context from "effect/Context"
import { Resource } from "sst/resource"
import { Resource } from "./resource"
export class AppConfigValue extends Schema.Class<AppConfigValue>("AppConfigValue")({
stage: Schema.NonEmptyString,
+1 -1
View File
@@ -2,7 +2,7 @@ import { Client } from "@planetscale/database"
import { drizzle } from "drizzle-orm/planetscale-serverless"
import { Config, ConfigProvider, Effect, Layer, Option, Schema } from "effect"
import * as Context from "effect/Context"
import { Resource } from "sst/resource"
import { Resource } from "./resource"
export const DatabaseUrl = Schema.NonEmptyString.pipe(Schema.brand("DatabaseUrl"))
export type DatabaseUrl = typeof DatabaseUrl.Type
+3 -2
View File
@@ -2,8 +2,9 @@ import { describe, expect, test } from "bun:test"
import type { ModelStatMetric } from "./model"
import type { RetentionMetricRow } from "./home"
process.env.SST_RESOURCE_App = JSON.stringify({ name: "opencode", stage: "test" })
process.env.SST_RESOURCE_StatsDatabase = JSON.stringify({ url: "mysql://localhost/stats" })
process.env.DATA_DATABASE_HOST = "localhost"
process.env.DATA_DATABASE_USERNAME = "stats"
process.env.DATA_DATABASE_PASSWORD = "stats"
const { buildRetentionEntries, buildStatsHomeData, normalizeStatRows } = await import("./home")
+1 -1
View File
@@ -1,6 +1,6 @@
import { Client } from "@planetscale/database"
import { Effect } from "effect"
import { Resource } from "sst/resource"
import { Resource } from "../resource"
import type { ModelStatMetric } from "./model"
import { RETIRED_STAT_PROVIDERS, statProvider } from "./model-normalization"
import { isMissingRetentionTable } from "./retention"
+1 -1
View File
@@ -1,4 +1,4 @@
import { Resource } from "sst/resource"
import { Resource } from "../resource"
import type { R2SqlData } from "../r2-sql"
import type { CatalogIdentity } from "./catalog-identity"
import type { GeoStatAggregate } from "./geo"
@@ -1,5 +1,5 @@
import { Client } from "@planetscale/database"
import { Resource } from "sst/resource"
import { Resource } from "./resource"
const tables = ["geo_stat", "model_stat", "provider_stat"] as const
const checkOnly = process.argv.includes("--check")
+1 -1
View File
@@ -1,5 +1,5 @@
import { Context, Effect, Layer, Schedule, Schema } from "effect"
import { Resource } from "sst/resource"
import { Resource } from "./resource"
const R2_SQL_MAX_ROWS = 10_000
const R2_SQL_TIMEOUT_MS = 10 * 60_000
-34
View File
@@ -1,34 +0,0 @@
import "sst/resource"
declare module "sst/resource" {
export interface Resource {
R2Sql: {
accountId: string
bucket: string
namespace: string
table: string
type: "sst.sst.Linkable"
}
R2SqlAuthToken: {
type: "sst.sst.Secret"
value: string
}
StatsHiddenModels: {
type: "sst.sst.Secret"
value: string
}
StatsSyncConfig: {
dataset: string
type: "sst.sst.Linkable"
}
StatsDatabase: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
url: string
username: string
}
}
}
+61
View File
@@ -0,0 +1,61 @@
// The stage's resources, read from the environment. Deployed Workers (services/data) get them as bindings, which
// nodejs_compat copies into process.env; scripts on your machine read your exported variables. Each value is read
// when used, so importing this module never touches the environment.
const ACCOUNT_ID = "15d29c8639fd3733b1b5486a2acfd968"
function required(name: string) {
const value = process.env[name]
if (!value) throw new Error(`${name} is not set`)
return value
}
function stage() {
return process.env.STAGE === "production" ? "production" : "dev"
}
export const Resource = {
App: {
get stage() {
return stage()
},
},
StatsDatabase: {
database: "opencode-stats",
port: 3306,
get host() {
return required("DATA_DATABASE_HOST")
},
get username() {
return required("DATA_DATABASE_USERNAME")
},
get password() {
return required("DATA_DATABASE_PASSWORD")
},
get url() {
return `mysql://${encodeURIComponent(this.username)}:${encodeURIComponent(this.password)}@${this.host}/${this.database}`
},
},
R2Sql: {
accountId: ACCOUNT_ID,
namespace: "inference",
table: "generation",
get bucket() {
return `platform-${stage()}-lake`
},
},
R2SqlAuthToken: {
get value() {
return required("R2_SQL_AUTH_TOKEN")
},
},
// Comma-separated model IDs kept off the public stats site. A secret, so excluding an unreleased model does not
// publish its name in git.
StatsHiddenModels: {
get value() {
return process.env.DATA_HIDDEN_MODELS ?? ""
},
},
StatsSyncConfig: {
dataset: "zen",
},
}
+1 -9
View File
@@ -4,15 +4,7 @@ import { catalogIdentity, type CatalogSnapshot } from "./domain/catalog-identity
import type { StatPeriodScope } from "./domain/stat"
import type { R2SqlData } from "./r2-sql"
process.env.SST_RESOURCE_App = JSON.stringify({ name: "opencode", stage: "test" })
process.env.SST_RESOURCE_R2Sql = JSON.stringify({
accountId: "account",
bucket: "bucket",
namespace: "inference",
table: "generation",
})
process.env.SST_RESOURCE_StatsSyncConfig = JSON.stringify({ dataset: "zen" })
process.env.SST_RESOURCE_StatsHiddenModels = JSON.stringify({ value: "secret-model" })
process.env.DATA_HIDDEN_MODELS = "secret-model"
const { GeoStatRepo } = await import("./domain/geo")
const { ModelStatRepo } = await import("./domain/model")
+1 -1
View File
@@ -1,5 +1,5 @@
import { DateTime, Effect } from "effect"
import { Resource } from "sst/resource"
import { Resource } from "./resource"
import { DatabaseError } from "./database"
import {
fromCatalogSnapshot,
+1 -2
View File
@@ -13,8 +13,7 @@
},
"dependencies": {
"@opencode/stats-core": "workspace:*",
"effect": "catalog:",
"sst": "catalog:"
"effect": "catalog:"
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
+1 -1
View File
@@ -1,4 +1,4 @@
// Keep in sync with the StatsSyncCron schedules in infra/stats.ts.
// Keep in sync with the schedules in services/data/sync.config.ts.
export const HOURLY_SCHEDULE = "5 * * * *"
export const HISTORY_SCHEDULE = "20 3 * * *"
+3
View File
@@ -0,0 +1,3 @@
// The sync Worker (services/data): the StatsSyncWorkflow and the cron handler that starts its runs.
export { StatsSyncWorkflow } from "./workflow"
export { default } from "./cron"
-4
View File
@@ -69,10 +69,6 @@ export class StatsSyncWorkflow extends WorkflowEntrypoint<unknown, StatsSyncPara
}
}
// SST's generated entry wraps the default export; workerd rejects a module whose
// default export is undefined.
export default {}
function run<A>(effect: Effect.Effect<A, SyncStatsError, Layer.Success<typeof runtimeLayer>>) {
return Effect.runPromise(effect.pipe(Effect.provide(runtimeLayer)))
}
+1 -1
View File
@@ -9,6 +9,6 @@
"noEmit": true,
"types": ["@cloudflare/workers-types", "node"]
},
"include": ["src", "../core/src/resource.d.ts"],
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
-10
View File
@@ -1,10 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+7 -6
View File
@@ -3,12 +3,13 @@
Independently deployed services supporting OpenCode live here. Runtime packages
and clients live in `packages/`.
| Service | Purpose | Deployment |
| ---------------------- | ----------------------------------------- | ---------------------------- |
| [`site`](./site) | The opencode.ai website and V2 docs | `deploy-site.yml`, `cf` |
| [`legacy`](./legacy) | Deprecated: opncd.ai and api.opencode.ai | `deploy.yml`, `cf` |
| [`updates`](./updates) | Release metadata and update selection | `release.yml`, Wrangler |
| [`files`](./files) | Public file distribution at `/files/*` | `deploy-files.yml`, Wrangler |
| Service | Purpose | Deployment |
| ---------------------- | ---------------------------------------- | ---------------------------- |
| [`site`](./site) | The opencode.ai website and V2 docs | `deploy-site.yml`, `cf` |
| [`legacy`](./legacy) | Deprecated: opncd.ai and api.opencode.ai | `deploy.yml`, `cf` |
| [`data`](./data) | The /data site, its OG images and sync | `deploy.yml`, `cf` |
| [`updates`](./updates) | Release metadata and update selection | `release.yml`, Wrangler |
| [`files`](./files) | Public file distribution at `/files/*` | `deploy-files.yml`, Wrangler |
These directories are Bun workspaces. Run package commands from each service's
directory. Repository-wide Turbo commands discover them through `services/*`.
+3
View File
@@ -0,0 +1,3 @@
node_modules
.cloudflare
.wrangler
+24
View File
@@ -0,0 +1,24 @@
# Data
Deploys the opencode.ai/data site from `packages/stats` as three Workers with `cf`:
| Worker | Config | What it is |
| ---------------------------- | ---------------------- | ----------------------------------------------------------------------------- |
| `opencode-data-<stage>` | `cloudflare.config.ts` | The SolidStart site, on `/data*` and `/<locale>/data*` of the stage's host |
| `opencode-data-og-<stage>` | `og.config.ts` | Open Graph images, reached only through the site's `OG_WORKER` binding |
| `opencode-data-sync-<stage>` | `sync.config.ts` | The `opencode-data-sync-<stage>` Workflow and the cron handler that starts it |
`script/build.ts` builds all three into one Build Output tree and `script/deploy.ts` deploys them with
`cf deploy --prebuilt`, the OG Worker first. The `data` job in `.github/workflows/deploy.yml` runs
`deploy:dev` or `deploy:production` and passes the Worker secrets from AWS Secrets Manager
(`opencode/<stage>/DATA_DATABASE_HOST`, `DATA_DATABASE_USERNAME`, `DATA_DATABASE_PASSWORD`,
`R2_SQL_AUTH_TOKEN`, `DATA_HIDDEN_MODELS`) with `--secrets-file`. Each Worker gets only the secrets
its config declares.
The database is the `opencode-stats` PlanetScale database: the `dev` branch for dev and `production`
for production, through the `data-worker-<stage>` password. It is not managed as code.
```bash
bun run build --mode dev
bun run typecheck
```
+42
View File
@@ -0,0 +1,42 @@
import { bindings, defineConfig, triggers } from "cf/config"
import { LOCALES } from "../../packages/stats/app/src/lib/language"
import { ogWorkerName } from "./og.config"
// The data site (packages/stats/app, SolidStart) at /data and /<locale>/data on the main host. `--mode dev` deploys
// opencode-data-dev on dev.opencode.ai; any other mode deploys opencode-data-production on opencode.ai. The routes
// are its only way in: it has no custom domain or workers.dev URL. An exact `/data` pattern misses `/data?query`, so
// each pattern is a `/data*` prefix, and route patterns can't wildcard the middle of a path, hence one per locale.
// script/build.ts builds this, og.config.ts and sync.config.ts into one Build Output tree.
export default defineConfig(({ mode }) => {
const dev = mode === "dev"
const stage = dev ? "dev" : "production"
const host = dev ? "dev.opencode.ai" : "opencode.ai"
return {
// Anomaly
accountId: "15d29c8639fd3733b1b5486a2acfd968",
worker: {
name: `opencode-data-${stage}`,
compatibilityDate: "2026-07-25",
// The Nitro server is built with node compatibility, which also copies string bindings into process.env
// (packages/stats/core/src/resource.ts).
compatibilityFlags: ["nodejs_compat"],
env: {
ASSETS: bindings.assets(),
// The private Worker that renders Open Graph images (og.config.ts).
OG_WORKER: bindings.worker({ worker: ogWorkerName(mode) }),
STAGE: bindings.text(stage),
PUBLIC_URL: bindings.text(`https://${host}/data`),
// Set from AWS Secrets Manager (opencode/<stage>/DATA_*) by the deploy job.
DATA_DATABASE_HOST: bindings.secret(),
DATA_DATABASE_USERNAME: bindings.secret(),
DATA_DATABASE_PASSWORD: bindings.secret(),
},
triggers: ["data", ...LOCALES.map((locale) => `${locale}/data`)].map((prefix) =>
triggers.fetch({ pattern: `${host}/${prefix}*`, zone: "opencode.ai" }),
),
workersDev: false,
previewUrls: false,
observability: { enabled: true },
},
}
})
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "cf/config"
export const ogWorkerName = (mode: string | undefined) => `opencode-data-og-${mode === "dev" ? "dev" : "production"}`
// The private Worker that renders the data pages' Open Graph images (packages/stats/app/src/og/worker.ts). It has no
// URL or route; the data Worker reaches it through its OG_WORKER Service Binding. It is its own Worker so the data
// Worker doesn't load the Wasm engines and fonts it ships as modules (script/build.ts).
export default defineConfig(({ mode }) => ({
// Anomaly
accountId: "15d29c8639fd3733b1b5486a2acfd968",
worker: {
name: ogWorkerName(mode),
compatibilityDate: "2026-07-01",
workersDev: false,
previewUrls: false,
observability: { enabled: true },
},
}))
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/data",
"private": true,
"type": "module",
"license": "MIT",
"scripts": {
"build": "bun script/build.ts",
"deploy:dev": "bun script/deploy.ts --mode dev",
"deploy:production": "bun script/deploy.ts --mode production",
"typecheck": "bun check --no-script",
"typecheck:tsc": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/build-output-utils": "0.8.5",
"@cloudflare/config": "0.23.0",
"@opencode/stats-app": "workspace:*",
"@opencode/stats-server": "workspace:*",
"@types/bun": "catalog:",
"@typescript/native": "catalog:",
"cf": "1.0.0-beta.12",
"typescript": "catalog:"
}
}
+101
View File
@@ -0,0 +1,101 @@
// Builds the data site's three Workers into one Build Output Specification tree (.cloudflare/output) for
// `cf deploy --prebuilt --worker <dir>` (script/deploy.ts):
// - default: the SolidStart site (packages/stats/app, Nitro's cloudflare-module preset) with .output/public as assets
// - og: the Open Graph image Worker, whose Wasm engines and fonts ship as modules
// - sync: the StatsSyncWorkflow and its cron handler (packages/stats/server)
// cf's own build step can't detect these projects, so they deploy prebuilt.
//
// bun script/build.ts [--mode dev|production]
import { cp, mkdir, readdir } from "node:fs/promises"
import path from "node:path"
import {
cleanBuildOutputDir,
getWorkerBundleDir,
writeAssets,
writeRootConfig,
writeWorkerConfig,
} from "@cloudflare/build-output-utils"
import { resolveAndParseConfig } from "@cloudflare/config"
const root = path.resolve(import.meta.dir, "..")
const app = path.resolve(root, "../../packages/stats/app")
const server = path.resolve(root, "../../packages/stats/server")
const modeIndex = process.argv.indexOf("--mode")
const mode = modeIndex === -1 ? "production" : process.argv[modeIndex + 1]
if (mode !== "dev" && mode !== "production") throw new Error(`Unknown --mode ${mode}; use dev or production`)
// STAGE picks the stage's headers for the static assets (packages/stats/app/src/lib/noindex.ts).
const env = { ...process.env, STAGE: mode }
await Bun.$`bun run build`.cwd(app).env(env)
await Bun.$`bun run build:og`.cwd(app).env(env)
// @cloudflare/config's file loader refuses Bun; it resolves an imported config the same way.
async function resolve(file: string) {
const parsed = await resolveAndParseConfig((await import(file)).default, { isPreview: false, mode })
if (!parsed.success) throw new Error(`${file}: ${parsed.error.message}`)
if (!parsed.data.worker) throw new Error(`${file} defines no worker`)
return { ...parsed.data, worker: parsed.data.worker }
}
const site = await resolve("../cloudflare.config")
const og = await resolve("../og.config")
const sync = await resolve("../sync.config")
await cleanBuildOutputDir(root)
await writeRootConfig(
root,
{ accountId: site.accountId, complianceRegion: site.complianceRegion },
{ isPreview: false, mode },
)
// The site: Nitro's server bundle and public directory.
await writeAssets({ root, sourceDirectory: path.join(app, ".output", "public") })
await cp(path.join(app, ".output", "server"), getWorkerBundleDir(root), {
recursive: true,
filter: (file) => !file.endsWith("wrangler.json") && !file.endsWith(".map"),
})
await writeWorkerConfig({
root,
config: site.worker,
manifest: { type: "partial", mainModule: "index.mjs", modules: {} },
})
// The OG Worker: its bundle plus the Wasm and font modules it imports.
const ogFiles = await readdir(path.join(app, ".og-worker"))
await mkdir(getWorkerBundleDir(root, "og"), { recursive: true })
await cp(path.join(app, ".og-worker"), getWorkerBundleDir(root, "og"), { recursive: true })
await writeWorkerConfig({
root,
directoryName: "og",
config: og.worker,
manifest: {
type: "partial",
mainModule: "index.mjs",
modules: Object.fromEntries(
ogFiles.flatMap((file): [string, { type: "wasm" | "data" }][] => {
if (file.endsWith(".wasm")) return [[file, { type: "wasm" }]]
if (file.endsWith(".bin")) return [[file, { type: "data" }]]
return []
}),
),
},
})
// The sync Worker, bundled for workerd.
const result = await Bun.build({
entrypoints: [path.join(server, "src/worker.ts")],
outdir: getWorkerBundleDir(root, "sync"),
naming: "index.mjs",
target: "browser",
format: "esm",
conditions: ["workerd", "worker", "browser"],
external: ["cloudflare:*", "node:*"],
})
if (!result.success) throw new AggregateError(result.logs, "The sync Worker failed to build")
await writeWorkerConfig({
root,
directoryName: "sync",
config: sync.worker,
manifest: { type: "partial", mainModule: "index.mjs", modules: {} },
})
console.log(`data: built ${site.worker.name}, ${og.worker.name} and ${sync.worker.name} (${mode})`)
+49
View File
@@ -0,0 +1,49 @@
// Builds and deploys the data site's Workers: the OG Worker first, since the site binds to it, then the sync Worker,
// then the site. --secrets-file is a JSON object of secret values (the deploy job writes it from AWS Secrets Manager);
// each Worker gets only the secrets its config declares, through a file in the same directory removed afterwards.
//
// bun script/deploy.ts --mode dev|production [--secrets-file <path>]
import { rm } from "node:fs/promises"
import path from "node:path"
import { resolveAndParseConfig } from "@cloudflare/config"
const root = path.resolve(import.meta.dir, "..")
const flag = (name: string) => {
const index = process.argv.indexOf(name)
return index === -1 ? undefined : process.argv[index + 1]
}
const mode = flag("--mode")
if (mode !== "dev" && mode !== "production") throw new Error(`Unknown --mode ${mode}; use dev or production`)
const secretsFile = flag("--secrets-file")
const secrets: Record<string, string> = secretsFile ? await Bun.file(secretsFile).json() : {}
await Bun.$`bun script/build.ts --mode ${mode}`.cwd(root)
for (const [directory, file] of [
["og", "../og.config"],
["sync", "../sync.config"],
["default", "../cloudflare.config"],
] as const) {
const parsed = await resolveAndParseConfig((await import(file)).default, { isPreview: false, mode })
if (!parsed.success) throw new Error(`${file}: ${parsed.error.message}`)
const names = Object.entries(parsed.data.worker?.env ?? {})
.filter(([, binding]) => binding.type === "secret")
.map(([name]) => name)
const missing = names.filter((name) => !secrets[name])
if (secretsFile && missing.length) throw new Error(`${file}: missing secrets ${missing.join(", ")}`)
const name = parsed.data.worker?.name
if (!name) throw new Error(`${file} defines no worker`)
const args = ["--prebuilt", "--mode", mode, "--worker", name]
const scoped = secretsFile && names.length ? `${secretsFile}.${directory}` : undefined
if (scoped) {
await Bun.write(scoped, JSON.stringify(Object.fromEntries(names.map((name) => [name, secrets[name]]))), {
mode: 0o600,
})
args.push("--secrets-file", scoped)
}
try {
await Bun.$`cf deploy ${args}`.cwd(root)
} finally {
if (scoped) await rm(scoped, { force: true })
}
}
+39
View File
@@ -0,0 +1,39 @@
import { bindings, defineConfig, exports, triggers } from "cf/config"
// The Worker that syncs R2 SQL aggregates into the data database (packages/stats/server/src/worker.ts): the
// StatsSyncWorkflow, and the cron handler that starts its runs. The schedules must match
// packages/stats/server/src/schedule.ts, which maps the daily one to a history run.
export default defineConfig(({ mode }) => {
const stage = mode === "dev" ? "dev" : "production"
const name = `opencode-data-sync-${stage}`
return {
// Anomaly
accountId: "15d29c8639fd3733b1b5486a2acfd968",
worker: {
name,
compatibilityDate: "2026-07-25",
// Also copies string bindings into process.env (packages/stats/core/src/resource.ts).
compatibilityFlags: ["nodejs_compat"],
// The 5-minute maximum, for ranking a full week of geo rows.
limits: { cpuMs: 300_000 },
exports: {
// One instance per run: a plan step, then one step per stat period and retention cohort.
StatsSyncWorkflow: exports.workflow({ name }),
},
env: {
StatsSyncWorkflow: bindings.workflow({ name, worker: name, exportName: "StatsSyncWorkflow" }),
STAGE: bindings.text(stage),
// Set from AWS Secrets Manager (opencode/<stage>/*) by the deploy job.
DATA_DATABASE_HOST: bindings.secret(),
DATA_DATABASE_USERNAME: bindings.secret(),
DATA_DATABASE_PASSWORD: bindings.secret(),
R2_SQL_AUTH_TOKEN: bindings.secret(),
DATA_HIDDEN_MODELS: bindings.secret(),
},
triggers: [triggers.scheduled({ schedule: "5 * * * *" }), triggers.scheduled({ schedule: "20 3 * * *" })],
workersDev: false,
previewUrls: false,
observability: { enabled: true },
},
}
})
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"types": ["bun"]
},
"include": ["*.ts", "script"]
}
+2 -3
View File
@@ -13,9 +13,8 @@ One Worker that answers by hostname:
OIDC token for a GitHub App installation token for the V1 GitHub Action
(`POST /exchange_github_app_token`, `src/api.ts`).
`cloudflare.config.ts` defines the Worker. `deploy:dev` and `deploy:production` deploy it with `cf`,
after the SST job in `.github/workflows/deploy.yml`, because the SST stack owned these hostnames until
the Worker moved here. The deploy job reads the `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` Worker
`cloudflare.config.ts` defines the Worker. `deploy:dev` and `deploy:production` deploy it with `cf`
from `.github/workflows/deploy.yml`. The deploy job reads the `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` Worker
secrets from AWS Secrets Manager (`opencode/<stage>/GITHUB_APP_*`) and uploads them with
`--secrets-file`.
-31
View File
@@ -1,31 +0,0 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
declare module "sst" {
export interface Resource {
"ADMIN_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
"Api": import("@cloudflare/workers-types").Service
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Share": import("@cloudflare/workers-types").Service
"WebApp": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
}
}
import "sst"
export {}
-34
View File
@@ -1,34 +0,0 @@
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "opencode",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "cloudflare",
providers:
input.stage === "beta"
? {}
: {
// Production state was written by 6.15.0; the default 6.13 provider can't read it back.
cloudflare: "6.15.0",
planetscale: "0.4.1",
},
}
},
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")
const stats = stage.deployStats ? await import("./infra/stats.js") : undefined
return {
...(stats ? { StatsUrl: stats.publicUrl } : {}),
AwsStage: stage.awsStage,
}
},
})