name: Deploy to racknerd1 # Deploys ArchNest (frontend + backend + guacd) to racknerd1 via Docker Compose. # # Triggers: # - push to main (automatic) # - manual run from the Actions tab (workflow_dispatch) # # Required GitHub Actions repo secrets (Settings -> Secrets and variables -> Actions): # RACKNERD_HOST - racknerd1 hostname or IP the runner can SSH to # RACKNERD_USER - deploy SSH user (must be in the docker group) # RACKNERD_SSH_KEY - private SSH key (PEM) for that user # RACKNERD_PORT - SSH port (optional, defaults to 22) # # One-time host setup (NOT done by this workflow, see README Deployment section): # - Docker + Docker Compose installed, deploy user in the docker group # - mkdir -p /opt/archnest # - Create /opt/archnest/.env from .env.example with real generated secrets # (ARCHNEST_JWT_SECRET, ARCHNEST_SECRET_KEY, ARCHNEST_GUAC_CRYPT_KEY, ...). # This workflow refuses to deploy if that file is missing, and never # overwrites it, so live secrets/data are safe across deploys. on: push: branches: [main] workflow_dispatch: {} # Prevent overlapping deploys clobbering each other. concurrency: group: deploy-racknerd1 cancel-in-progress: false env: DEPLOY_PATH: /opt/archnest jobs: # Fail fast on build/type errors before touching the server. validate: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: 22 - name: Install + type-check + build frontend run: | npm ci npx tsc --noEmit npm run build - name: Install + type-check + build backend working-directory: backend run: | npm ci npx tsc --noEmit npm run build deploy: needs: validate runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Pre-flight - confirm host .env exists (don't deploy without secrets) uses: appleboy/ssh-action@v1.2.0 with: host: ${{ secrets.RACKNERD_HOST }} username: ${{ secrets.RACKNERD_USER }} key: ${{ secrets.RACKNERD_SSH_KEY }} port: ${{ secrets.RACKNERD_PORT || 22 }} script: | set -e mkdir -p ${{ env.DEPLOY_PATH }} if [ ! -f ${{ env.DEPLOY_PATH }}/.env ]; then echo "::error::Missing ${{ env.DEPLOY_PATH }}/.env on the host." echo "Create it from .env.example with real secrets before deploying." echo "It is intentionally never created/overwritten by this workflow." exit 1 fi echo ".env present - proceeding." - name: Copy repo to racknerd1 uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.RACKNERD_HOST }} username: ${{ secrets.RACKNERD_USER }} key: ${{ secrets.RACKNERD_SSH_KEY }} port: ${{ secrets.RACKNERD_PORT || 22 }} source: "." target: ${{ env.DEPLOY_PATH }} # Keep the host-only .env (and any other untracked host state) intact. rm: false overwrite: true - name: Build, restart, and clean up uses: appleboy/ssh-action@v1.2.0 with: host: ${{ secrets.RACKNERD_HOST }} username: ${{ secrets.RACKNERD_USER }} key: ${{ secrets.RACKNERD_SSH_KEY }} port: ${{ secrets.RACKNERD_PORT || 22 }} command_timeout: 20m script: | set -e cd ${{ env.DEPLOY_PATH }} docker compose up -d --build --remove-orphans docker image prune -f - name: Health check (backend /api/health) uses: appleboy/ssh-action@v1.2.0 with: host: ${{ secrets.RACKNERD_HOST }} username: ${{ secrets.RACKNERD_USER }} key: ${{ secrets.RACKNERD_SSH_KEY }} port: ${{ secrets.RACKNERD_PORT || 22 }} script: | set -e echo "Waiting for backend to become healthy..." for i in $(seq 1 30); do if curl -fsS http://127.0.0.1:4000/api/health >/dev/null 2>&1; then echo "Backend healthy." # Confirm the frontend container is serving too. if curl -fsS http://127.0.0.1:8080/ >/dev/null 2>&1; then echo "Frontend healthy. Deploy succeeded." exit 0 fi echo "Frontend not ready yet..." fi sleep 5 done echo "::error::Health check failed after ~150s. Dumping container status + logs." cd ${{ env.DEPLOY_PATH }} docker compose ps || true docker compose logs --tail=80 || true exit 1