CD #691
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Continuous Deployment — OrangeCat → bitbaum (self-hosted Hetzner). | |
| # | |
| # Runs ONLY after the CI workflow succeeds on main, builds the standalone bundle | |
| # off-box (the box OOMs on build), and ships it via the atomic-swap+rollback | |
| # deploy verb (scripts/deploy-selfhost.sh). The deploy half the manual runbook | |
| # used to do by hand — this closes the tested→live seam. | |
| # | |
| # DISABLED until secrets are configured: every deploy step is gated on | |
| # `steps.guard.outputs.ok`, which is false unless SELFHOST_SSH_KEY exists. So | |
| # merging this file is a safe no-op; CD activates the moment the key is added. | |
| # | |
| # Required repo secrets (Settings → Secrets and variables → Actions): | |
| # SELFHOST_SSH_KEY private SSH key whose public half is in the box's | |
| # authorized_keys for the OC_BOX user (default root). Treat | |
| # as a deploy credential; rotate if leaked. | |
| # SELFHOST_KNOWN_HOSTS output of `ssh-keyscan 167.233.22.31` (pins the host). | |
| # NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY / | |
| # NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY real self-host values, baked at build. | |
| # Optional repo variables (vars.*): | |
| # OC_BOX (default root@167.233.22.31) | |
| # | |
| # Note: OrangeCat's build does not require live-DB access (no blocking SSG DB | |
| # queries), so unlike fleetcrown/scripts/hetzner/deploy.sh this needs no tunnel. | |
| name: CD | |
| on: | |
| workflow_run: | |
| workflows: ['CI'] | |
| types: [completed] | |
| branches: [main] | |
| workflow_dispatch: {} # allow manual redeploy of current main | |
| concurrency: | |
| group: cd-selfhost | |
| cancel-in-progress: false # never interrupt a deploy mid-swap | |
| jobs: | |
| deploy: | |
| # Only on green CI (or a manual dispatch). | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| github.event.workflow_run.conclusion == 'success' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| env: | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} | |
| NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY }} | |
| # OrangeCat's public donation addresses, baked into the client bundle for | |
| # the /support founding-supporter page. Repo *variables* (not secrets) — | |
| # these are public receiving addresses, meant to be shown. Unset → the | |
| # page shows its graceful "opens shortly" state. (NEXT_PUBLIC_* must be set | |
| # HERE at build time; the box's runtime .env can't reach the client bundle.) | |
| NEXT_PUBLIC_LIGHTNING_ADDRESS: ${{ vars.NEXT_PUBLIC_LIGHTNING_ADDRESS }} | |
| NEXT_PUBLIC_BITCOIN_ADDRESS: ${{ vars.NEXT_PUBLIC_BITCOIN_ADDRESS }} | |
| # Flips the Cat Credits UI from "Activating" to "Live" (build-time, client | |
| # bundle). Set the repo variable to true only once PLATFORM_NWC_URI exists | |
| # in the box's runtime .env — the UI must never claim Live before the | |
| # server can actually receive. Server-side receive is gated independently | |
| # on PLATFORM_NWC_URI (runtime), so this flag is display-only. | |
| NEXT_PUBLIC_CAT_CREDITS_LIVE: ${{ vars.NEXT_PUBLIC_CAT_CREDITS_LIVE }} | |
| # Voice input on create forms and onboarding. This was missing here for | |
| # its whole life, which meant the flag could never be true in production: | |
| # NEXT_PUBLIC_* is compiled into the client bundle, so setting it in the | |
| # box's runtime .env does nothing. The mic on every create form was | |
| # unreachable dead code — off by omission, not by decision. | |
| NEXT_PUBLIC_FEATURE_VOICE_INPUT: ${{ vars.NEXT_PUBLIC_FEATURE_VOICE_INPUT }} | |
| # FleetCrown feedback widget token — must match ci.yml's build env so a | |
| # cd-side rebuild bakes the same embed into prerendered pages. Public by | |
| # design (ships in page source): repo variable, not a secret. | |
| FLEETCROWN_FEEDBACK_TOKEN: ${{ vars.FLEETCROWN_FEEDBACK_TOKEN }} | |
| OC_BOX: ${{ vars.OC_BOX || 'root@167.233.22.31' }} | |
| steps: | |
| - name: Guard — is the deploy key configured? | |
| id: guard | |
| env: | |
| KEY: ${{ secrets.SELFHOST_SSH_KEY }} | |
| run: | | |
| if [ -n "$KEY" ]; then | |
| echo "ok=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "ok=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::SELFHOST_SSH_KEY not set — CD is dormant. Add it to enable deploys." | |
| fi | |
| - name: Checkout | |
| if: steps.guard.outputs.ok == 'true' | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| if: steps.guard.outputs.ok == 'true' | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| if: steps.guard.outputs.ok == 'true' | |
| run: npm ci | |
| - name: Download standalone artifact from the triggering CI run | |
| # Ship the EXACT artifact CI built + tested (tested == shipped), and skip | |
| # the rebuild. Best-effort: if the download fails — or this is a manual | |
| # dispatch with no CI run behind it — the next step falls back to building, | |
| # so a broken artifact path can never break deploys (worst case = today). | |
| if: steps.guard.outputs.ok == 'true' && github.event_name == 'workflow_run' | |
| continue-on-error: true | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: standalone-${{ github.event.workflow_run.head_sha }} | |
| path: .next/standalone | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build standalone if no artifact (fallback / manual dispatch) | |
| if: steps.guard.outputs.ok == 'true' | |
| env: | |
| NODE_ENV: production | |
| SELF_HOST: '1' | |
| run: | | |
| if [ -f .next/standalone/server.js ]; then | |
| echo "Using the CI-built standalone artifact — skipping rebuild." | |
| else | |
| echo "No artifact (manual dispatch or download failed) — building standalone." | |
| npm run build | |
| fi | |
| - name: Configure SSH | |
| if: steps.guard.outputs.ok == 'true' | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.SELFHOST_SSH_KEY }}" > ~/.ssh/id_deploy | |
| chmod 600 ~/.ssh/id_deploy | |
| if [ -n "${{ secrets.SELFHOST_KNOWN_HOSTS }}" ]; then | |
| echo "${{ secrets.SELFHOST_KNOWN_HOSTS }}" > ~/.ssh/known_hosts | |
| else | |
| ssh-keyscan -H 167.233.22.31 >> ~/.ssh/known_hosts 2>/dev/null | |
| fi | |
| cat > ~/.ssh/config <<'EOF' | |
| Host bitbaum | |
| HostName 167.233.22.31 | |
| IdentityFile ~/.ssh/id_deploy | |
| IdentitiesOnly yes | |
| EOF | |
| - name: Deploy (atomic swap + rollback) | |
| if: steps.guard.outputs.ok == 'true' | |
| run: | | |
| eval "$(ssh-agent -s)" | |
| ssh-add ~/.ssh/id_deploy | |
| bash scripts/deploy-selfhost.sh --no-build |