Skip to content

CI/CD - Deploy to VM #1

CI/CD - Deploy to VM

CI/CD - Deploy to VM #1

Workflow file for this run

# 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://CHANGEME
# 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
[[ "$PUBLIC_BASE_URL" != "http://it2810-09.idi.ntnu.no/" ]] || {
echo "::error::Set PUBLIC_BASE_URL to the VM's public URL"
exit 1
}
- 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