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