From 5117d40233e5ddc4e87af75dbc62edf8034aefa8 Mon Sep 17 00:00:00 2001 From: sofiesty Date: Wed, 14 Oct 2026 06:49:21 +0200 Subject: [PATCH] Deploy to VM from laptop: deploy scripts, Apollo client file, /project2 base path --- .DS_Store | Bin 6148 -> 0 bytes client/.env.production | 1 + client/package-lock.json | 35 ++++++++++++++++++ client/package.json | 1 + client/src/apollo/client.ts | 17 +++++++++ client/src/main.tsx | 29 ++++----------- client/vite.config.ts | 2 + deploy/apache-project2.conf | 20 ++++++++++ deploy/backend.env.example | 9 +++++ deploy/deploy.sh | 72 ++++++++++++++++++++++++++++++++++++ deploy/push-to-vm.sh | 43 +++++++++++++++++++++ deploy/wine-not-api.service | 19 ++++++++++ deploy/with-env.sh | 28 ++++++++++++++ 13 files changed, 255 insertions(+), 21 deletions(-) delete mode 100644 .DS_Store create mode 100644 client/.env.production create mode 100644 client/src/apollo/client.ts create mode 100644 deploy/apache-project2.conf create mode 100644 deploy/backend.env.example create mode 100755 deploy/deploy.sh create mode 100755 deploy/push-to-vm.sh create mode 100644 deploy/wine-not-api.service create mode 100755 deploy/with-env.sh diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index c7679af0ff28ad8cea95d9224df9c226e941f288..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5T0$TO%S06g&qT5rPfwN@e*r$@ZiOW9#m@66b;6#G^st5LeBayUVRc@ zM`w1oVyIrF$PCPWv$HeF?zhYS0Dx$Xf^C2b09dGm1sjVWgxX2xq+mQHM4`SBLwX#^ zz8^_{E}9*S$NqcBP9^>6j0mffvXCzIVqbJN*r>`gbF$$q1bc5`<+wXOAyt%K9x_m}}@;O`lr{lP&cbPZ-2)z*Q9dIBKQHBt%MbSa`7$DnI4(}*o7LZ>3?RAHVN zLZ_qOF>$WJOruT*VICjC{8^Y6ico*Y?>j0SgsYKDW`G&UGf>pk3Z4I*-{1fFB(5<7 z%)q~5K$O~EyM;qCXY1VH=&W^6Z%|1nF4Opxf`&SZF_w=22.22.0" + }, + "peerDependencies": { + "react": ">=19.2.7", + "react-dom": ">=19.2.7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/rolldown": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", diff --git a/client/package.json b/client/package.json index 55cb7d5..a82364f 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "graphql": "^16.14.2", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-router": "^8.4.0", "rxjs": "^7.8.2" }, "devDependencies": { diff --git a/client/src/apollo/client.ts b/client/src/apollo/client.ts new file mode 100644 index 0000000..e0a5bff --- /dev/null +++ b/client/src/apollo/client.ts @@ -0,0 +1,17 @@ +import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'; + +/** + * Apollo Client handles communication with our GraphQL backend and is also + * our state management: InMemoryCache stores query results, so the same + * query isn't sent to the server again (fewer requests, more sustainable). + * + * The backend URL is read from the environment variable VITE_GRAPHQL_URL, + * so the same code works locally and on the VM. Without it, we use the + * local backend on port 3001. + */ +export const client = new ApolloClient({ + link: new HttpLink({ + uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/', + }), + cache: new InMemoryCache(), +}); \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index 3db6379..e1bbd9e 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,30 +1,17 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'; +import { BrowserRouter } from 'react-router'; import { ApolloProvider } from '@apollo/client/react'; +import { client } from './apollo/client'; +import App from './App'; import './index.css'; -import App from './App.tsx'; - -/** - * Apollo Client handles communication with our GraphQL backend and is also - * our state management: InMemoryCache stores query results, so the same - * query isn't sent to the server again (fewer requests, more sustainable). - * - * The backend URL is read from the environment variable VITE_GRAPHQL_URL, - * so the same code works locally and on the VM. Without it, we use the - * local backend on port 3001. - */ -const client = new ApolloClient({ - link: new HttpLink({ - uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/', - }), - cache: new InMemoryCache(), -}); createRoot(document.getElementById('root')!).render( - - - + + + + + , ); diff --git a/client/vite.config.ts b/client/vite.config.ts index 9982072..2a1e3b1 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -3,5 +3,7 @@ import { defineConfig } from 'vite' // https://vite.dev/config/ export default defineConfig({ + // The app is served from http://it2810-xx.idi.ntnu.no/project2/ + base: '/project2/', plugins: [react()], }) diff --git a/deploy/apache-project2.conf b/deploy/apache-project2.conf new file mode 100644 index 0000000..d7632c8 --- /dev/null +++ b/deploy/apache-project2.conf @@ -0,0 +1,20 @@ +# Apache config for the Wine not? client, served at /project2. +# Installed to /etc/apache2/conf-available/project2.conf on the VM by deploy/deploy.sh on every deploy. + + + # Unknown paths (e.g. /project2/wine/42) get index.html, + # so React Router can handle them and a page refresh doesn't give 404. + FallbackResource /project2/index.html + + # index.html and version.txt are always re-checked, + # so a new deploy shows up immediately. + + Header set Cache-Control "no-cache" + + + +# Vite gives built files hashed names, so browsers can cache them for a year. +# Returning visitors don't download them again (less data transferred). + + Header set Cache-Control "public, max-age=31536000, immutable" + diff --git a/deploy/backend.env.example b/deploy/backend.env.example new file mode 100644 index 0000000..1f02da8 --- /dev/null +++ b/deploy/backend.env.example @@ -0,0 +1,9 @@ +# Template for /etc/wine-not/backend.env on the VM. The real file, with the +# password, only exists on the VM and can be read with sudo. +# Same variable names as backend/.env on a laptop. +PORT=3001 +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=winenot +DB_USER=winenot_app +DB_PASSWORD= diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..12538e1 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Builds the backend and the client on the VM and puts them live. Runs as root. +# +# Normally started by deploy/push-to-vm.sh on a laptop, which sends a new +# copy of main: +# bash deploy.sh --install +# +# To rebuild the code that's already on the VM (e.g. after a migration), +# on the VM: +# sudo bash /opt/wine-not/deploy/deploy.sh +# +# Both parts are built before anything goes live. If a build fails, the +# script stops and the version that's already live keeps running. +set -euo pipefail + +main() { + local app_dir=/opt/wine-not + local web_dir=/var/www/html/project2 + + # Only one deploy at a time; a second one waits for the first to finish. + exec 9>/run/wine-not-deploy.lock + flock 9 + + if [ "${1:-}" = "--install" ]; then + local src="$2" commit="$3" + echo "==> Copying commit $commit to $app_dir" + mkdir -p "$app_dir" + chmod 755 "$src" # the backend runs as user winenot and must read the code + # Replace the code, but keep installed packages, builds and the CSV data. + rsync -a --delete \ + --exclude node_modules --exclude dist --exclude /backend/data \ + "$src/" "$app_dir/" + echo "$commit" > "$app_dir/COMMIT" + fi + + echo "==> Backend: install and build" + cd "$app_dir/backend" + npm ci + npm run build + + echo "==> Client: install and build" + cd "$app_dir/client" + npm ci + npm run build + + echo "==> Server setup: backend service and Apache config" + install -m 644 "$app_dir/deploy/wine-not-api.service" /etc/systemd/system/wine-not-api.service + systemctl daemon-reload + systemctl enable --quiet wine-not-api + install -m 644 "$app_dir/deploy/apache-project2.conf" /etc/apache2/conf-available/project2.conf + a2enmod --quiet headers + a2enconf --quiet project2 + apache2ctl configtest + + echo "==> Going live" + systemctl restart wine-not-api + mkdir -p "$web_dir" + rsync -a --delete "$app_dir/client/dist/" "$web_dir/" + systemctl restart apache2 + + # Shows which commit is live at /project2/version.txt + local commit_live + commit_live="$(cat "$app_dir/COMMIT" 2>/dev/null || echo unknown)" + echo "$commit_live deployed $(date '+%Y-%m-%d %H:%M')" > "$web_dir/version.txt" + + sleep 2 + echo "==> Done. Commit $commit_live. Backend status: $(systemctl is-active wine-not-api)" +} + +# Everything is inside a function so bash reads the whole script before +# running it, even if the file is replaced while it runs. +main "$@" diff --git a/deploy/push-to-vm.sh b/deploy/push-to-vm.sh new file mode 100755 index 0000000..cfe210f --- /dev/null +++ b/deploy/push-to-vm.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Deploys the newest version of main to the VM. +# +# Run it on your LAPTOP, from the repo, on the NTNU network or VPN: +# bash deploy/push-to-vm.sh +# +# The VM has no access to the Git server, so your laptop packs up what's on +# main, copies it to the VM and runs deploy/deploy.sh there. +# You may be asked for your NTNU password up to three times: +# once to copy, once to log in and once for sudo. +set -euo pipefail + +VM_HOST="it2810-01.idi.ntnu.no" + +if [ "$#" -ne 1 ]; then + echo "Usage: bash deploy/push-to-vm.sh " >&2 + exit 1 +fi +vm_user="$1" + +cd "$(git rev-parse --show-toplevel)" + +echo "==> Getting the newest main" +git fetch --quiet origin main +commit="$(git rev-parse --short origin/main)" +echo " Commit $commit: $(git log -1 --format=%s origin/main)" + +# Only committed files on main are packed: no node_modules, .env or CSV data. +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +git archive --format=tar.gz -o "$tmp_dir/wine-not.tar.gz" origin/main + +echo "==> Copying to $VM_HOST" +remote_archive="/tmp/wine-not-$commit.tar.gz" +scp -q "$tmp_dir/wine-not.tar.gz" "$vm_user@$VM_HOST:$remote_archive" + +echo "==> Building and starting on the VM" +# Unpacks into a temporary folder and runs the deploy.sh from this commit, +# which copies the code to /opt/wine-not, builds it and puts it live. +ssh -t "$vm_user@$VM_HOST" \ + "sudo bash -c 'set -e; d=\$(mktemp -d); tar -xzf $remote_archive -C \"\$d\"; rm -f $remote_archive; bash \"\$d/deploy/deploy.sh\" --install \"\$d\" $commit; rm -rf \"\$d\"'" + +echo "==> Live at http://$VM_HOST/project2/" diff --git a/deploy/wine-not-api.service b/deploy/wine-not-api.service new file mode 100644 index 0000000..c469722 --- /dev/null +++ b/deploy/wine-not-api.service @@ -0,0 +1,19 @@ +# systemd service that runs the Wine not? GraphQL backend on port 3001. +# Installed to /etc/systemd/system/ on the VM by deploy/deploy.sh on every deploy. +[Unit] +Description=Wine not? GraphQL backend +After=network.target postgresql.service + +[Service] +User=winenot +Group=winenot +WorkingDirectory=/opt/wine-not/backend +# Database settings and port. This file exists only on the VM, never in Git. +EnvironmentFile=/etc/wine-not/backend.env +Environment=NODE_ENV=production +ExecStart=/usr/bin/node dist/index.js +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/with-env.sh b/deploy/with-env.sh new file mode 100755 index 0000000..d4d8dd8 --- /dev/null +++ b/deploy/with-env.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Runs one command with the backend's database settings loaded from +# /etc/wine-not/backend.env (the secret file that only exists on the VM). +# The DB_ settings are also given to psql (as PGHOST, PGUSER, ...), +# so psql connects to the same database as the backend. +# +# Usage on the VM, from /opt/wine-not/backend: +# sudo bash ../deploy/with-env.sh npm run import +# sudo bash ../deploy/with-env.sh psql -f db/migrations/.sql +set -euo pipefail + +if [ "$#" -eq 0 ]; then + echo "Usage: sudo bash ../deploy/with-env.sh [args...]" >&2 + exit 1 +fi + +set -a # export every variable defined in the file +# shellcheck source=/dev/null +. /etc/wine-not/backend.env +set +a + +export PGHOST="${DB_HOST:-localhost}" +export PGPORT="${DB_PORT:-5432}" +export PGDATABASE="${DB_NAME:-}" +export PGUSER="${DB_USER:-}" +export PGPASSWORD="${DB_PASSWORD:-}" + +exec "$@"