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 `), 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 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).