diff --git a/.github/workflows/studio-deploy.yml b/.github/workflows/studio-deploy.yml new file mode 100644 index 000000000..14ec32b8a --- /dev/null +++ b/.github/workflows/studio-deploy.yml @@ -0,0 +1,160 @@ +name: Deploy studio.geolibre.app + +# Publishes the gated instance at https://studio.geolibre.app on every push to +# main. +# +# studio.geolibre.app is a *separate* private repository that only hosts the +# built site, so this cannot use actions/deploy-pages — that action deploys the +# Pages site of the repository it runs in. Instead the build is force-pushed as +# a single orphan commit to that repository's `gh-pages` branch, which its Pages +# site serves. Keeping the workflow here means the deployment is versioned +# alongside the code it ships. +# +# This is the same app as web.geolibre.app with the Clerk sign-in gate switched +# on. It is a client-side gate, not a server boundary: see docs/getting-started.md. + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + ref: + description: Ref to build and publish (branch, tag, or SHA) + default: main + +permissions: + contents: read + +concurrency: + # Never let two deploys race for the same force-pushed branch. + group: studio-deploy + cancel-in-progress: true + +jobs: + deploy: + name: Build and publish + # Forks have neither the deploy token nor a reason to publish. + if: github.repository == 'opengeos/GeoLibre' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.sha }} + # Nothing here uses the checkout's token — npm ci fetches from the + # registry and the publish step carries its own credentials — so do + # not leave it behind in .git/config. + persist-credentials: false + + - name: Record the commit that was built + id: built + # $GITHUB_SHA is the SHA of the ref chosen in the "Run workflow" + # dropdown, not the `ref` input typed into the form, so on a manual + # rollback it names main's tip while the build is of something older. + # Resolve what was actually checked out — `ref` may also be a branch or + # tag name rather than a SHA — and label the deployment with that. + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: lts/* + cache: npm + cache-dependency-path: package-lock.json + + - name: Install frontend dependencies + run: npm ci + + - name: Build the gated web app + run: npm run build -w geolibre-desktop + env: + # No GEOLIBRE_APP_BASE: this site is served from the root of its own + # domain, unlike the /demo/ subpath pages.yml publishes. + # + # The Clerk publishable key, which turns the sign-in gate on. It is + # public by design (it encodes only the Frontend API hostname), so a + # repository variable would do; a secret keeps rotation in one place. + # Unset means no gate, so a missing secret publishes an open app — + # the guard step below fails the run instead. + VITE_GEOLIBRE_CLERK_PUBLISHABLE_KEY: ${{ secrets.VITE_GEOLIBRE_CLERK_PUBLISHABLE_KEY }} + # Clerk's waitlist form. Off unless the repository variable + # GEOLIBRE_CLERK_WAITLIST is set to 1, because the instance is + # invite-only (Clerk sign-up mode "Restricted") by default and a + # waitlist form there would collect requests nobody can approve. An + # unset variable expands to "", which the app reads as unset. Flip it + # in Settings → Variables — no code change, and set the Clerk + # instance's sign-up mode to Waitlist to match. + VITE_GEOLIBRE_CLERK_WAITLIST: ${{ vars.GEOLIBRE_CLERK_WAITLIST }} + # The AI assistant and the NASA OPERA disaster-news lookup. Pages + # cannot proxy, so these are cross-origin calls to the Worker and + # https://studio.geolibre.app must be in its ALLOWED_ORIGINS. + VITE_GEOLIBRE_AI_URL: https://ai.geolibre.app + VITE_GEOLIBRE_AI_MODEL: ${{ vars.VITE_GEOLIBRE_AI_MODEL }} + # Optional third-party keys, as in pages.yml. Each is baked into the + # bundle and readable by any signed-in visitor; the matching features + # hide themselves when unset. + VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }} + VITE_PROTOMAPS_API_KEY: ${{ secrets.VITE_PROTOMAPS_API_KEY }} + VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }} + VITE_MAPILLARY_ACCESS_TOKEN: ${{ secrets.VITE_MAPILLARY_ACCESS_TOKEN || vars.VITE_MAPILLARY_ACCESS_TOKEN }} + VITE_GEOLIBRE_COLLAB_URL: wss://collab.geolibre.app + + - name: Verify the gate is in the bundle + # The point of this deployment is that it is gated. A missing or + # misspelled key builds cleanly and publishes an *open* instance, so + # confirm the configured key reached the bundle before anything is + # pushed. + # + # Matches the key's exact value, not its `pk_test_`/`pk_live_` prefix: + # @clerk/shared ships those prefixes as literals in its own key + # validation, so they appear in the bundle whether or not a key is + # configured, and a prefix check would pass on an ungated build. + env: + CLERK_KEY: ${{ secrets.VITE_GEOLIBRE_CLERK_PUBLISHABLE_KEY }} + run: | + if [ -z "$CLERK_KEY" ]; then + echo "::error::VITE_GEOLIBRE_CLERK_PUBLISHABLE_KEY is unset — refusing to publish an ungated studio.geolibre.app." + exit 1 + fi + if ! grep -rqsF -- "$CLERK_KEY" apps/geolibre-desktop/dist/assets; then + echo "::error::The configured Clerk key is not in the build — refusing to publish an ungated studio.geolibre.app." + exit 1 + fi + + - name: Prepare the site for GitHub Pages + run: | + cd apps/geolibre-desktop/dist + # Pages serves this branch through its legacy (Jekyll) pipeline, which + # silently drops files and directories beginning with an underscore. + touch .nojekyll + # Keeps the custom domain if the Pages settings are ever reset. + echo "studio.geolibre.app" > CNAME + # Published Pages sites may be no larger than 1 GB. The build is + # ~200 MB today, most of it the two DuckDB-WASM binaries; fail early + # rather than publish a site Pages will reject. + size=$(du -sm . | cut -f1) + echo "Built site: ${size} MB" + if [ "$size" -gt 900 ]; then + echo "::error::Site is ${size} MB, at the 1 GB GitHub Pages limit." + exit 1 + fi + + - name: Publish to opengeos/studio.geolibre.app + env: + # Fine-grained PAT with Contents: write on opengeos/studio.geolibre.app. + STUDIO_DEPLOY_TOKEN: ${{ secrets.STUDIO_DEPLOY_TOKEN }} + BUILT_SHA: ${{ steps.built.outputs.sha }} + run: | + cd apps/geolibre-desktop/dist + git init -q -b gh-pages + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -q -m "Deploy ${BUILT_SHA} from ${GITHUB_REPOSITORY}" + # A fresh orphan commit force-pushed each time, so the hosting + # repository stays the size of one build rather than accumulating a + # ~200 MB commit per merge to main. + git push -q --force \ + "https://x-access-token:${STUDIO_DEPLOY_TOKEN}@github.com/opengeos/studio.geolibre.app.git" \ + gh-pages + echo "Published ${BUILT_SHA} to https://studio.geolibre.app" diff --git a/workers/ai-proxy/worker-configuration.d.ts b/workers/ai-proxy/worker-configuration.d.ts index d987ca9b6..407a0b339 100644 --- a/workers/ai-proxy/worker-configuration.d.ts +++ b/workers/ai-proxy/worker-configuration.d.ts @@ -1,9 +1,9 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: d305911522b75e606a41762aa46b28e3) +// Generated by Wrangler by running `wrangler types` (hash: c930d16aa86422cf711cded8b5fc487a) // Runtime types generated with workerd@1.20260730.1 2026-07-26 nodejs_compat interface __BaseEnv_Env { AI_RATE_LIMITER: RateLimit; - ALLOWED_ORIGINS: "https://geolibre.app,https://web.geolibre.app,https://viewer.geolibre.app,http://localhost:5173,http://localhost:1420,tauri://localhost,http://tauri.localhost"; + ALLOWED_ORIGINS: "https://geolibre.app,https://web.geolibre.app,https://viewer.geolibre.app,https://studio.geolibre.app,http://localhost:5173,http://localhost:1420,tauri://localhost,http://tauri.localhost"; ALLOWED_MODELS: "openai/gpt-5.5,anthropic/claude-opus-5,anthropic/claude-sonnet-5,google/gemini-3.6-flash,google/gemini-3.5-flash,google/gemini-3.5-flash-lite"; DEFAULT_MODEL: "openai/gpt-5.5"; AI_GATEWAY_ID: "default"; diff --git a/workers/ai-proxy/wrangler.jsonc b/workers/ai-proxy/wrangler.jsonc index 3e62d7fed..1263fe856 100644 --- a/workers/ai-proxy/wrangler.jsonc +++ b/workers/ai-proxy/wrangler.jsonc @@ -14,11 +14,12 @@ "required": [ "CF_AI_GATEWAY_TOKEN", "CLOUDFLARE_ACCOUNT_ID", - "GEOLIBRE_AI_PROXY_TOKEN" + "GEOLIBRE_AI_PROXY_TOKEN", + "TAVILY_API_KEY" ] }, "vars": { - "ALLOWED_ORIGINS": "https://geolibre.app,https://web.geolibre.app,https://viewer.geolibre.app,http://localhost:5173,http://localhost:1420,tauri://localhost,http://tauri.localhost", + "ALLOWED_ORIGINS": "https://geolibre.app,https://web.geolibre.app,https://viewer.geolibre.app,https://studio.geolibre.app,http://localhost:5173,http://localhost:1420,tauri://localhost,http://tauri.localhost", "ALLOWED_MODELS": "openai/gpt-5.5,anthropic/claude-opus-5,anthropic/claude-sonnet-5,google/gemini-3.6-flash,google/gemini-3.5-flash,google/gemini-3.5-flash-lite", "DEFAULT_MODEL": "openai/gpt-5.5", "AI_GATEWAY_ID": "default",