git / moonbite-network / .github/workflows/deploy.yml · raw

name: Deploy to Production# SECURITY: production deploy is GATED on CI. Previously this ran on every push# to main with no dependency on the test/checksum workflow, so a red CI (or a bad# merge) still root-deployed to the host that serves the wallet and /downloads.# Now it runs only after the "CI" workflow completes SUCCESSFULLY on main, and it# deploys the exact commit CI validated (not whatever origin/main points at now).on:  workflow_run:    workflows: ["CI"]    types: [completed]    branches: [main]  workflow_dispatch:jobs:  deploy:    runs-on: ubuntu-latest    # Deploy only from main, and only when the triggering CI run passed.    # workflow_dispatch has no triggering run, so a deliberate manual deploy is    # allowed through — but still only when dispatched on main, so nobody can    # ship an arbitrary branch to production via the "Run workflow" button.    if: >-      ${{ github.ref == 'refs/heads/main' &&          (github.event_name == 'workflow_dispatch' ||           github.event.workflow_run.conclusion == 'success') }}    steps:      - name: Checkout the CI-validated commit        uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4        with:          ref: ${{ github.event.workflow_run.head_sha || github.sha }}      - name: Deploy to server        env:          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}          # REQUIRED: set this repo secret to the server's SSH host key          # (`ssh-keyscan -H <host>`), pinned out-of-band. The deploy refuses to          # run without it — no TOFU fallback, since that would let a MITM          # between the runner and the host capture the deploy key session.          SERVER_KNOWN_HOSTS: ${{ secrets.SERVER_KNOWN_HOSTS }}          SERVER_HOST: 67.205.154.64          # Non-root forced-command account (deploy/setup-deploy-user.sh). The          # key can only run /usr/local/bin/deploy-dashboard.sh <sha> on the host.          SERVER_USER: deploy          PROJECT_PATH: /opt/moonbite-dashboard        run: |          mkdir -p ~/.ssh          # The host key must be pinned out-of-band. Fail closed otherwise.          if [ -z "$SERVER_KNOWN_HOSTS" ]; then            echo 'SERVER_KNOWN_HOSTS secret is required' && exit 1          fi          echo "$SERVER_KNOWN_HOSTS" >> ~/.ssh/known_hosts          echo "$DEPLOY_KEY" > ~/.ssh/deploy_key          chmod 600 ~/.ssh/deploy_key          # Deploy the EXACT commit CI validated, not the current tip of main.          # The deploy account's authorized_keys forces          # /usr/local/bin/deploy-dashboard.sh; the only thing we may send is the          # 40-hex SHA. There is no shell on the other end (no-pty, restrict).          ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o StrictHostKeyChecking=yes "$SERVER_USER@$SERVER_HOST" "${{ github.event.workflow_run.head_sha || github.sha }}"      - name: Deployment successful        run: echo "Successfully deployed the CI-validated commit to https://moonbite.org"# Further hardening (needs action outside this file, documented here):#   * Add a GitHub branch-protection rule on main requiring the CI check, so a#     red CI cannot be merged in the first place (workflow_run gates deploy, not merge).#   * (DONE 2026-10-09) Deploy runs as the `deploy` user through a forced#     command; see deploy/setup-deploy-user.sh. Re-run that script on the host#     if the key is ever rotated.#   * Deploy only from signed tags (actions are already SHA-pinned).