CI/CD - Deploy to VM #5
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
| # Generated by GLM 5.3 NVFP4 | |
| # Deploys the app to the group VM via its self-hosted runner. | |
| # | |
| # One-time VM setup (run once as an admin): | |
| # 1. Node.js >= 22 and pnpm installed system-wide (so systemd can find them): | |
| # sudo npm install -g pnpm@10 | |
| # 2. Sudoers entry so the runner can manage the services (sudo visudo): | |
| # <runner-user> ALL=(root) NOPASSWD: /usr/bin/install, /usr/bin/tee, /usr/bin/systemctl | |
| # 3. PostgreSQL on localhost (pnpm db:setup, see README). | |
| # 4. Apache reverse proxy so the frontend is reachable at [baseurl]/project2 | |
| # (the Nitro server already serves the app under /project2): | |
| # sudo a2enmod proxy_http | |
| # sudo tee /etc/apache2/conf-available/gspotter.conf > /dev/null <<'EOF' | |
| # ProxyPreserveHost On | |
| # ProxyPass /project2 http://localhost:3000/project2 | |
| # ProxyPassReverse /project2 http://localhost:3000/project2 | |
| # EOF | |
| # sudo a2enconf gspotter && sudo systemctl reload apache2 | |
| # Any old static build left in /var/www/html/project2 can be removed. | |
| # | |
| # Result: GraphQL backend on port 3001, SSR frontend on port 3000 (proxied to | |
| # [baseurl]/project2 by Apache), both running as systemd services that restart | |
| # on every deploy and on boot. | |
| name: CI/CD - Deploy to VM | |
| on: | |
| workflow_run: | |
| workflows: ['CI/CD - Linter, formatter and tests'] | |
| types: [completed] | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: production-deploy | |
| cancel-in-progress: false | |
| env: | |
| # Persistent location of the deployed app on the VM | |
| DEPLOY_ROOT: /srv/gspotter | |
| # Public URL of the VM, no trailing slash (baked into the frontend build) | |
| PUBLIC_BASE_URL: http://http://it2810-09.idi.ntnu.no | |
| # Connection string for the VM's PostgreSQL (see pnpm db:setup) | |
| DATABASE_URL: postgresql://gspot:gspotter@localhost:5432/gspot | |
| BACKEND_PORT: 3001 | |
| FRONTEND_PORT: 3000 | |
| jobs: | |
| deploy: | |
| if: ${{ github.event.workflow_run.conclusion == 'success' }} | |
| runs-on: [self-hosted] | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.workflow_run.head_sha }} | |
| - name: Check prerequisites | |
| run: | | |
| set -euo pipefail | |
| for cmd in node pnpm rsync; do | |
| command -v "$cmd" > /dev/null || { | |
| echo "::error::$cmd is not installed on the runner" | |
| exit 1 | |
| } | |
| done | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Write env files | |
| run: | | |
| printf 'DATABASE_URL="%s"\n' "$DATABASE_URL" > apps/backend/.env | |
| cp apps/backend/.env packages/db/.env | |
| - name: Generate Prisma client | |
| run: pnpm --filter @t09-project-2/db run db:generate | |
| - name: Build frontend | |
| run: pnpm --filter @t09-project-2/frontend run build | |
| env: | |
| VITE_GRAPHQL_URL: ${{ env.PUBLIC_BASE_URL }}:${{ env.BACKEND_PORT }}/ | |
| - name: Create deploy root | |
| run: sudo install -d -o "$(id -un)" -g "$(id -gn)" "$DEPLOY_ROOT" | |
| - name: Sync app to deploy root | |
| run: | | |
| rsync -a --delete \ | |
| --exclude .git \ | |
| --exclude node_modules \ | |
| ./ "$DEPLOY_ROOT/" | |
| - name: Install dependencies in deploy root | |
| run: pnpm -C "$DEPLOY_ROOT" install --frozen-lockfile | |
| - name: Apply database schema | |
| run: pnpm -C "$DEPLOY_ROOT" --filter @t09-project-2/db run db:push | |
| - name: Install and restart services | |
| run: | | |
| sudo tee /etc/systemd/system/gspotter-backend.service > /dev/null <<EOF | |
| [Unit] | |
| Description=G-Spotter GraphQL backend (port $BACKEND_PORT) | |
| After=network.target postgresql.service | |
| Wants=postgresql.service | |
| [Service] | |
| User=$(id -un) | |
| Group=$(id -gn) | |
| WorkingDirectory=$DEPLOY_ROOT/apps/backend | |
| ExecStart=/usr/bin/env pnpm start | |
| Restart=always | |
| RestartSec=2 | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| sudo tee /etc/systemd/system/gspotter-frontend.service > /dev/null <<EOF | |
| [Unit] | |
| Description=G-Spotter frontend (port $FRONTEND_PORT) | |
| After=network.target gspotter-backend.service | |
| Wants=gspotter-backend.service | |
| [Service] | |
| User=$(id -un) | |
| Group=$(id -gn) | |
| WorkingDirectory=$DEPLOY_ROOT/apps/frontend/.output | |
| Environment=PORT=$FRONTEND_PORT | |
| ExecStart=/usr/bin/env node server/index.mjs | |
| Restart=always | |
| RestartSec=2 | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| sudo systemctl daemon-reload | |
| sudo systemctl enable gspotter-backend gspotter-frontend | |
| sudo systemctl restart gspotter-backend gspotter-frontend | |
| - name: Verify deployment | |
| run: | | |
| set -euo pipefail | |
| tries=0 | |
| until curl -fsS -o /dev/null "http://localhost:$FRONTEND_PORT/project2/"; do | |
| tries=$((tries + 1)) | |
| [[ $tries -lt 15 ]] || { | |
| echo "::error::Frontend did not come up" | |
| exit 1 | |
| } | |
| sleep 2 | |
| done | |
| curl -fsS -X POST "http://localhost:$BACKEND_PORT/" \ | |
| -H 'content-type: application/json' \ | |
| -d '{"query":"{ __typename }"}' | |
| echo "Deployment verified: frontend and backend are up" | |
| - name: Clean up job files | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| # Removes the checkout, node_modules, build output, npm cache, and TMPDIR. | |
| # Keeps the runner's own _work directory intact. | |
| if [ -d "$GITHUB_WORKSPACE" ]; then | |
| find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 1 \ | |
| -exec rm -rf -- {} + | |
| fi |