Skip to content
Merged
Show file tree
Hide file tree
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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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
180 changes: 180 additions & 0 deletions .github/workflows/deploy-to-vm.yaml
Original file line number Diff line number Diff line change
@@ -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):
# <runner-user> 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://it2810-09.idi.ntnu.no/" ]] || {
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 <<EOF
[Unit]
Description=G-Spotter GraphQL backend (port $BACKEND_PORT)
After=network.target postgresql.service
Wants=postgresql.service
[Service]
User=$(id -un)
Group=$(id -gn)
WorkingDirectory=$DEPLOY_ROOT/apps/backend
ExecStart=/usr/bin/env pnpm start
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/gspotter-frontend.service > /dev/null <<EOF
[Unit]
Description=G-Spotter frontend (port $FRONTEND_PORT)
After=network.target gspotter-backend.service
Wants=gspotter-backend.service
[Service]
User=$(id -un)
Group=$(id -gn)
WorkingDirectory=$DEPLOY_ROOT/apps/frontend/.output
Environment=PORT=$FRONTEND_PORT
ExecStart=/usr/bin/env node server/index.mjs
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable gspotter-backend gspotter-frontend
sudo systemctl restart gspotter-backend gspotter-frontend
- name: Verify deployment
run: |
set -euo pipefail
tries=0
until curl -fsS -o /dev/null "http://localhost:$FRONTEND_PORT/project2/"; do
tries=$((tries + 1))
[[ $tries -lt 15 ]] || {
echo "::error::Frontend did not come up"
exit 1
}
sleep 2
done
curl -fsS -X POST "http://localhost:$BACKEND_PORT/" \
-H 'content-type: application/json' \
-d '{"query":"{ __typename }"}'
echo "Deployment verified: frontend and backend are up"
- name: Clean up job files
if: always()
run: |
set -euo pipefail
# Removes the checkout, node_modules, build output, npm cache, and TMPDIR.
# Keeps the runner's own _work directory intact.
if [ -d "$GITHUB_WORKSPACE" ]; then
find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 1 \
-exec rm -rf -- {} +
fi
2 changes: 1 addition & 1 deletion apps/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const server = new ApolloServer({
})

const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
listen: { port: 3001 },
context: async () => createContext(),
})

Expand Down
31 changes: 18 additions & 13 deletions apps/frontend/src/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
export default function Map() {
return (
<section id="map">
<MapContainer center={[63.4305, 10.3951]} zoom={15} scrollWheelZoom={false} className="h-100 w-full rounded-2xl">
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[63.4305, 10.3951]}>
<Popup>
<span>Guinness!!!</span>
</Popup>
</Marker>
</MapContainer>
<MapContainer
center={[63.4305, 10.3951]}
zoom={15}
scrollWheelZoom={false}
className="h-100 w-full rounded-2xl"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[63.4305, 10.3951]}>
<Popup>
<span>Guinness!!!</span>
</Popup>
</Marker>
</MapContainer>
</section>
)
}
)
}
7 changes: 6 additions & 1 deletion apps/frontend/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossOrigin=""/>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossOrigin=""
/>
<HeadContent />
</head>
<body className="font-sans antialiased [overflow-wrap:anywhere] selection:bg-[rgba(79,184,178,0.24)]">
Expand Down
1 change: 0 additions & 1 deletion apps/frontend/src/routes/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ function About() {
<main className="page-wrap px-4 py-12">
<section className="island-shell rounded-2xl p-6 sm:p-8">
<Map />

</section>
</main>
)
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const config = defineConfig({
tanstackStart(),
viteReact(),
],
base: '/project2',
})

export default config
5 changes: 5 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export default [
'prettier.config.js',
'.tanstack',
'**/src/generated/**',
'**/.output/**',
'**/dist/**',
'**/.nitro/**',
'**/.vinxi/**',
'**/.wrangler/**',
],
},
]
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"esbuild",
"lightningcss",
"@prisma/engines",
"prisma"
"prisma",
"@apollo/protobufjs",
"unrs-resolver"
]
}
}
Loading