From 553f5ab686515cc4c2b8a42af1cfcdbe010efc24 Mon Sep 17 00:00:00 2001 From: Erik Hjelm Fjeldheim Date: Fri, 25 Sep 2026 13:08:36 -0600 Subject: [PATCH 1/3] chore: #27 CI workflow on the self-hosted runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two jobs, both on [self-hosted, linux, x64] since NTNU GitHub Enterprise has no hosted runners. quality (every PR and every push to main): pnpm install --frozen-lockfile, then format:check, lint, typecheck, test, build, and a client bundle budget gate at 512 kB per JS chunk. deploy (push to main only): builds with the production settings and publishes client/dist to /var/www/html/project2, then curls the site to confirm it served. Restarts crate-api only if the unit exists, so the deploy works today at M1 and still works when the backend lands in M2. The bundle budget is a bærekraft measure: it is deliberately loose now and tightens as the app grows, so a dependency cannot quietly double our payload without CI noticing. Co-Authored-By: opencode --- .github/workflows/ci.yml | 121 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 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..f96ec4d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: CI/CD + +on: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Format, lint, typecheck, test, build + 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' + 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: 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 From 6ab73b6df681f193fa938768f31dca4e3bbc1137 Mon Sep 17 00:00:00 2001 From: Erik Hjelm Fjeldheim Date: Mon, 28 Sep 2026 10:51:25 -0600 Subject: [PATCH 2/3] chore: #27 pin deploy to the project2 runner label Found in review of #34: both jobs used [self-hosted, linux, x64] even though the runner is labelled project2. That works today because the Project 2 runner is repo-scoped and is the only one that can pick up these jobs, but it is not what the labels are for. The two jobs now differ deliberately: - quality is unpinned beyond the platform. It is a pure function of the checked-out tree, so it can run on any self-hosted runner we add later without editing this file. - deploy is pinned to [self-hosted, linux, x64, project2]. It 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 job must not land there. Reported by rachelks. Co-Authored-By: opencode --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f96ec4d..dad1d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,9 @@ concurrency: 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 @@ -74,7 +77,11 @@ jobs: name: Deploy to the group VM needs: quality if: github.ref == 'refs/heads/main' && github.event_name == 'push' - runs-on: [self-hosted, linux, x64] + # 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 From 15e3c311dddc360ba61daac49ad0bcf103df7a24 Mon Sep 17 00:00:00 2001 From: Erik Hjelm Fjeldheim Date: Mon, 28 Sep 2026 10:55:25 -0600 Subject: [PATCH 3/3] chore: #27 run CI on PRs against any base branch Found by actually verifying rather than assuming: PR #34 targets feat/1-repo-tooling because it is stacked on #32, and the workflow's `pull_request: branches: [main]` filter silently skipped CI for it. The push of 6ab73b6 produced no run at all. Stacked PRs are how we are working (see docs/WORKFLOW.md), so a branch filter on pull_request is wrong. pull_request now has no filter and runs for every PR whatever it targets; push stays limited to main so we only deploy from the mainline. Co-Authored-By: opencode --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dad1d86..e7cb803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,11 @@ 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: - branches: [main] push: branches: [main]