From 5c29a56ba8e2f1145107afd80373731678cd142c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Thu, 15 Oct 2026 21:05:41 +0200 Subject: [PATCH 1/6] Add CI and deploy-to-VM workflows * Add a CI workflow that installs dependencies, checks formatting, lints, and typechecks; the Prisma client is generated first from a dummy env file since packages/db/src/generated is not committed * Replace the old template that ran `npm ci` and `npm run test`, which do not exist in this pnpm monorepo, so CI could never pass * Add a deploy workflow that runs when CI succeeds on main: it builds the frontend with the VM GraphQL URL baked in, syncs the repo to /srv/gspotter, installs dependencies, applies the Prisma schema, and installs and restarts systemd services for the backend (port 3001) and the SSR frontend (port 3000), then verifies both with curl * One-time VM prerequisites (node, pnpm, sudoers, Apache reverse proxy, PostgreSQL) are documented in the workflow file header --- .github/workflows/ci.yaml | 39 ++++++ .github/workflows/deploy-to-vm.yaml | 180 ++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/deploy-to-vm.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..e73b206 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,39 @@ +name: CI/CD - Linter, formatter and tests + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + +jobs: + lint-format-and-test: + runs-on: [self-hosted] + timeout-minutes: 10 + + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Write env file + run: > + printf 'DATABASE_URL="postgresql://ci:ci@localhost:5432/ci"\n' > + packages/db/.env + + - name: Generate Prisma client + run: pnpm --filter @t09-project-2/db run db:generate + + - name: Check formatting + run: pnpm check + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck diff --git a/.github/workflows/deploy-to-vm.yaml b/.github/workflows/deploy-to-vm.yaml new file mode 100644 index 0000000..b27fb78 --- /dev/null +++ b/.github/workflows/deploy-to-vm.yaml @@ -0,0 +1,180 @@ +# Generated by GLM 5.3 NVFP4 + +# Deploys the app to the group VM via its self-hosted runner. +# +# One-time VM setup (run once as an admin): +# 1. Node.js >= 22 and pnpm installed system-wide (so systemd can find them): +# sudo npm install -g pnpm@10 +# 2. Sudoers entry so the runner can manage the services (sudo visudo): +# ALL=(root) NOPASSWD: /usr/bin/install, /usr/bin/tee, /usr/bin/systemctl +# 3. PostgreSQL on localhost (pnpm db:setup, see README). +# 4. Apache reverse proxy so the frontend is reachable at [baseurl]/project2 +# (the Nitro server already serves the app under /project2): +# sudo a2enmod proxy_http +# sudo tee /etc/apache2/conf-available/gspotter.conf > /dev/null <<'EOF' +# ProxyPreserveHost On +# ProxyPass /project2 http://localhost:3000/project2 +# ProxyPassReverse /project2 http://localhost:3000/project2 +# EOF +# sudo a2enconf gspotter && sudo systemctl reload apache2 +# Any old static build left in /var/www/html/project2 can be removed. +# +# Result: GraphQL backend on port 3001, SSR frontend on port 3000 (proxied to +# [baseurl]/project2 by Apache), both running as systemd services that restart +# on every deploy and on boot. + +name: CI/CD - Deploy to VM + +on: + workflow_run: + workflows: ['CI/CD - Linter, formatter and tests'] + types: [completed] + branches: + - main + +permissions: + contents: read + +concurrency: + group: production-deploy + cancel-in-progress: false + +env: + # Persistent location of the deployed app on the VM + DEPLOY_ROOT: /srv/gspotter + # Public URL of the VM, no trailing slash (baked into the frontend build) + PUBLIC_BASE_URL: http://CHANGEME + # Connection string for the VM's PostgreSQL (see pnpm db:setup) + DATABASE_URL: postgresql://gspot:gspotter@localhost:5432/gspot + BACKEND_PORT: 3001 + FRONTEND_PORT: 3000 + +jobs: + deploy: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: [self-hosted] + timeout-minutes: 20 + + steps: + - name: Check out source + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Check prerequisites + run: | + set -euo pipefail + for cmd in node pnpm rsync; do + command -v "$cmd" > /dev/null || { + echo "::error::$cmd is not installed on the runner" + exit 1 + } + done + [[ "$PUBLIC_BASE_URL" != "http://CHANGEME" ]] || { + echo "::error::Set PUBLIC_BASE_URL to the VM's public URL" + exit 1 + } + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Write env files + run: | + printf 'DATABASE_URL="%s"\n' "$DATABASE_URL" > apps/backend/.env + cp apps/backend/.env packages/db/.env + + - name: Generate Prisma client + run: pnpm --filter @t09-project-2/db run db:generate + + - name: Build frontend + run: pnpm --filter @t09-project-2/frontend run build + env: + VITE_GRAPHQL_URL: ${{ env.PUBLIC_BASE_URL }}:${{ env.BACKEND_PORT }}/ + + - name: Create deploy root + run: sudo install -d -o "$(id -un)" -g "$(id -gn)" "$DEPLOY_ROOT" + + - name: Sync app to deploy root + run: | + rsync -a --delete \ + --exclude .git \ + --exclude node_modules \ + ./ "$DEPLOY_ROOT/" + + - name: Install dependencies in deploy root + run: pnpm -C "$DEPLOY_ROOT" install --frozen-lockfile + + - name: Apply database schema + run: pnpm -C "$DEPLOY_ROOT" --filter @t09-project-2/db run db:push + + - name: Install and restart services + run: | + sudo tee /etc/systemd/system/gspotter-backend.service > /dev/null < /dev/null < Date: Thu, 15 Oct 2026 21:05:44 +0200 Subject: [PATCH 2/6] Serve the backend on port 3001 and the frontend under /project2 * Point the Apollo server at port 3001 so the API is reachable at [baseurl]:3001 * Set the Vite base path to /project2 so assets and routes resolve behind the Apache reverse proxy --- apps/backend/src/index.ts | 2 +- apps/frontend/vite.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 05eafcc..1fa77c7 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -10,7 +10,7 @@ const server = new ApolloServer({ }) const { url } = await startStandaloneServer(server, { - listen: { port: 4000 }, + listen: { port: 3001 }, context: async () => createContext(), }) diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 5a46a66..a9184f2 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -16,6 +16,7 @@ const config = defineConfig({ tanstackStart(), viteReact(), ], + base: '/project2', }) export default config From d946f1012660b92575bbff42178c0cec43b52909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Thu, 15 Oct 2026 21:05:44 +0200 Subject: [PATCH 3/6] Ignore build output directories in ESLint * Add .output, dist, .nitro, .vinxi and .wrangler to the ESLint ignores so `pnpm lint` passes after a build --- .github/workflows/deploy-to-vm.yaml | 2 +- eslint.config.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-to-vm.yaml b/.github/workflows/deploy-to-vm.yaml index b27fb78..91ef565 100644 --- a/.github/workflows/deploy-to-vm.yaml +++ b/.github/workflows/deploy-to-vm.yaml @@ -70,7 +70,7 @@ jobs: exit 1 } done - [[ "$PUBLIC_BASE_URL" != "http://CHANGEME" ]] || { + [[ "$PUBLIC_BASE_URL" != "http://it2810-09.idi.ntnu.no/" ]] || { echo "::error::Set PUBLIC_BASE_URL to the VM's public URL" exit 1 } diff --git a/eslint.config.js b/eslint.config.js index c1d1fa9..02cc8fc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,6 +20,11 @@ export default [ 'prettier.config.js', '.tanstack', '**/src/generated/**', + '**/.output/**', + '**/dist/**', + '**/.nitro/**', + '**/.vinxi/**', + '**/.wrangler/**', ], }, ] From dcc45a1c1f9982c0cfd65b9c7b469331887d6f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Thu, 15 Oct 2026 21:23:49 +0200 Subject: [PATCH 4/6] Allow @apollo/protobufjs and unrs-resolver install scripts * Recent pnpm versions no longer read the "pnpm" field in package.json and fail the install with ERR_PNPM_IGNORED_BUILDS when a dependency's build scripts are unreviewed (strictDepBuilds) * Approve both packages in allowBuilds in pnpm-workspace.yaml, the new home of the setting, and in the legacy onlyBuiltDependencies list for older pnpm versions; both postinstall scripts are benign (a version-scheme check and the standard napi-postinstall binary preparation) --- package.json | 4 +++- pnpm-workspace.yaml | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6022ea9..b076ce9 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "esbuild", "lightningcss", "@prisma/engines", - "prisma" + "prisma", + "@apollo/protobufjs", + "unrs-resolver" ] } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 27c5199..2b0c704 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,3 +7,5 @@ allowBuilds: lightningcss: true '@prisma/engines': true prisma: true + '@apollo/protobufjs': true + unrs-resolver: true From 8f4b66ea8ae527ac9082c66d4bd63dd04aebcb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Thu, 15 Oct 2026 21:23:49 +0200 Subject: [PATCH 5/6] Sync lockfile with react-leaflet dependency added in #11 * apps/frontend gained react-leaflet and @types/leaflet without a matching lockfile update, which makes every `pnpm install --frozen-lockfile` fail --- pnpm-lock.yaml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11f63ef..df77ef9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: react-dom: specifier: ^19.3.0 version: 19.3.0(react@19.3.0) + react-leaflet: + specifier: 5.0.0-rc.2 + version: 5.0.0-rc.2(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) tailwindcss: specifier: ^4.3.3 version: 4.3.3 @@ -97,6 +100,9 @@ importers: '@tanstack/router-cli': specifier: ^1.167.39 version: 1.167.43 + '@types/leaflet': + specifier: ^1.9.22 + version: 1.9.22 '@types/node': specifier: ^22.20.4 version: 22.20.5 @@ -967,6 +973,13 @@ packages: '@types/react': optional: true + '@react-leaflet/core@3.0.0': + resolution: {integrity: sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + '@rolldown/binding-android-arm-eabi@1.2.14': resolution: {integrity: sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1536,6 +1549,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/leaflet@1.9.22': + resolution: {integrity: sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA==} + '@types/lodash@4.17.26': resolution: {integrity: sha512-48NWfiY6oNcKOh47Z4h+sIvwcQR91h6KPhaYKgeMIrkT2jV82Gy90SErj60vW8jQbeMSOSbhIv3bZ7JoqPPVhA==} @@ -2549,6 +2565,9 @@ packages: launch-editor@2.14.2: resolution: {integrity: sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==} + leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3003,6 +3022,13 @@ packages: peerDependencies: react: ^19.3.0 + react-leaflet@5.0.0-rc.2: + resolution: {integrity: sha512-1xQGYG9mEIW+nfkQhqgHImwUuB1UDlnzYFSzv6PrBFDBeYrFmv0BbpwpNAFdJg/UQ2yz5UZSL7ZwlUxjwb8MZw==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + react@19.3.0: resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} @@ -4318,6 +4344,12 @@ snapshots: optionalDependencies: '@types/react': 19.3.0 + '@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + leaflet: 1.9.4 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + '@rolldown/binding-android-arm-eabi@1.2.14': optional: true @@ -4957,6 +4989,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/leaflet@1.9.22': + dependencies: + '@types/geojson': 7946.0.16 + '@types/lodash@4.17.26': {} '@types/long@4.0.2': {} @@ -5953,6 +5989,8 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.12.0 + leaflet@1.9.4: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -6383,6 +6421,13 @@ snapshots: react: 19.3.0 scheduler: 0.28.0 + react-leaflet@5.0.0-rc.2(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + leaflet: 1.9.4 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + react@19.3.0: {} readdirp@5.1.1: {} From e8af63a4b2694af747f9427cdc5cbd23e9721d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Thu, 15 Oct 2026 21:23:49 +0200 Subject: [PATCH 6/6] Format map component files to satisfy the prettier check * #11 landed while CI was broken, so Map.tsx, __root.tsx and about.tsx were merged without passing `prettier --check` --- apps/frontend/src/components/Map.tsx | 31 ++++++++++++++++------------ apps/frontend/src/routes/__root.tsx | 7 ++++++- apps/frontend/src/routes/about.tsx | 1 - 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/apps/frontend/src/components/Map.tsx b/apps/frontend/src/components/Map.tsx index 45ecdfb..8137d86 100644 --- a/apps/frontend/src/components/Map.tsx +++ b/apps/frontend/src/components/Map.tsx @@ -3,17 +3,22 @@ import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' export default function Map() { return (
- - - - - Guinness!!! - - - + + + + + Guinness!!! + + +
- ) -} \ No newline at end of file + ) +} diff --git a/apps/frontend/src/routes/__root.tsx b/apps/frontend/src/routes/__root.tsx index 709e4ad..4119982 100644 --- a/apps/frontend/src/routes/__root.tsx +++ b/apps/frontend/src/routes/__root.tsx @@ -49,7 +49,12 @@ function RootDocument({ children }: { children: React.ReactNode }) {