Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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/
# crate-api only exists from M2 onward. Fail the deploy loudly if the
# unit is present but down; skip quietly if it has not been created yet.
- name: Restart the GraphQL API if present
run: |
set -euo pipefail
if systemctl list-unit-files | grep -q '^crate-api.service'; then
sudo systemctl restart crate-api
sleep 2
curl -fsS -o /dev/null -w 'POST /graphql : %{http_code}\n' \
-X POST http://localhost/graphql \
-H 'content-type: application/json' \
-d '{"query":"{ __typename }"}'
else
echo "crate-api.service not installed yet (arrives in M2). Skipping."
fi