Skip to content

Merge pull request #39 from IT2810-H26/fix/27-ci-deploy #19

Merge pull request #39 from IT2810-H26/fix/27-ci-deploy

Merge pull request #39 from IT2810-H26/fix/27-ci-deploy #19

Workflow file for this run

name: CI/CD
on:
# No branch filter here on purpose. We stack PRs (a branch PR based on
# another branch), and a `branches: [main]` filter silently skips CI for
# every stacked PR. That is how this was found: #34 targets
# feat/1-repo-tooling and got no run at all.
pull_request:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: Format, lint, typecheck, test, build
# Deliberately unpinned beyond the platform: quality is a pure function of
# the checked-out tree, so it can run on any self-hosted runner we add
# later without changing this file. Only `deploy` is machine-specific.
runs-on: [self-hosted, linux, x64]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Test
run: pnpm test
- name: Build
run: pnpm build
# Bundle budget, one of our bærekraft measures. The budget is deliberately
# loose today and tightens as the app grows; the point is that CI fails
# before a dependency quietly doubles our payload.
- name: Enforce client bundle budget
run: |
set -euo pipefail
MAX_BYTES=512000
shopt -s nullglob
found=0
for f in client/dist/assets/*.js; do
found=1
size=$(stat -c%s "$f")
echo "$f = ${size} bytes (budget ${MAX_BYTES})"
if [ "$size" -gt "$MAX_BYTES" ]; then
echo "::error::Bundle budget exceeded: $f is ${size} bytes (max ${MAX_BYTES})."
exit 1
fi
done
if [ "$found" -eq 0 ]; then
echo "::error::No client JS bundle found in client/dist/assets/."
exit 1
fi
deploy:
name: Deploy to the group VM
needs: quality
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# Pinned to the `project2` label on purpose. This job writes to
# /var/www/html/project2 and restarts crate-api, both of which only exist
# on it2810-19. If a second runner is ever registered to this repo from a
# different machine, this must not land there.
runs-on: [self-hosted, linux, x64, project2]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build client
run: pnpm build
- name: Publish to Apache
run: |
set -euo pipefail
find /var/www/html/project2 -mindepth 1 -delete
cp -r client/dist/. /var/www/html/project2/
chmod -R a+rX /var/www/html/project2
curl -fsS -o /dev/null -w 'GET /project2/ : %{http_code}\n' http://localhost/project2/
# Restarting the GraphQL API is M2 work. Until `server/` exists in this
# repository there is nothing to deploy, and restarting the unit would
# just bring back whatever was last installed on the VM by hand.
#
# Two things here are deliberately not what the first version did:
#
# - The unit check is `[ -f ... ]`, not `systemctl list-unit-files | grep`.
# `grep -q` exits on the first match and closes the pipe, `systemctl`
# gets SIGPIPE and dies, and with `pipefail` the whole pipeline reports
# failure. So the `if` took the else branch and skipped a service that
# was definitely there. The log showed "Failed to print table: Broken
# pipe". Reported by rachelks.
#
# - The restart needs `sudo`, and the runner has no TTY. That needs a
# NOPASSWD rule scoped to exactly these commands. Without it the step
# dies with "sudo: A terminal is required to authenticate".
- name: Restart the GraphQL API if the server is deployed
run: |
set -euo pipefail
if [ ! -f /etc/systemd/system/crate-api.service ]; then
echo "crate-api.service is not installed. Skipping API restart."
exit 0
fi
if [ ! -d server ]; then
echo "No server/ in this repository yet (arrives in M2). Skipping API restart."
exit 0
fi
if [ ! -f server/dist/index.js ]; then
echo "::error::server/ exists but server/dist/index.js does not. Was 'pnpm build' run?"
exit 1
fi
sudo /usr/bin/systemctl restart crate-api
# `Type=simple` makes systemd report active as soon as the process is
# spawned, well before Node has bound the port. Measured on the VM,
# the API answers again after about 1850 ms, so a fixed `sleep 2`
# leaves a margin of roughly 120 ms. That was enough to return 503
# through the Apache proxy on at least one run. Poll instead.
printf 'Waiting for the API'
for _ in $(seq 1 30); do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 \
-X POST http://localhost/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ __typename }"}' 2>/dev/null || echo 000)
if [ "$code" = "200" ]; then
echo
echo "POST /graphql : 200"
exit 0
fi
printf '.'
sleep 1
done
echo
echo "::error::crate-api restarted but /graphql never returned 200 (last code ${code})."
exit 1