From 6835a8bc3e7c3f8b2cc43b841bafe855f27c5df0 Mon Sep 17 00:00:00 2001 From: Erik Hjelm Fjeldheim Date: Mon, 28 Sep 2026 14:47:10 -0600 Subject: [PATCH] chore: #27 land the CI workflow on main The workflow is on main for the first time. PR #34 merged into feat/1-repo-tooling (its base at the time) after #32 had already merged that branch into main, so the workflow never reached main and the repo has been running with no CI at all. This brings it across. Content is unchanged from what #34 reviewed and what has been passing on the runner: quality job (format, lint, typecheck, test, build, bundle budget) on every PR and every push to main, deploy job on push to main only. Two fixes that came out of actually running it: - pull_request has no branch filter. A `branches: [main]` filter meant stacked PRs like #34, which targets feat/1-repo-tooling, silently got zero CI runs. - deploy is pinned to [self-hosted, linux, x64, project2]. It writes to /var/www/html/project2 and restarts crate-api, which only exist on it2810-19. quality stays unpinned so any future runner can pick it up. Co-Authored-By: opencode --- .github/workflows/ci.yml | 131 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e7cb803 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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