feat: Semantische Versionierung mit Git-Tags und App-Footer.
VERSION-Datei (0.1.0.0), Release-Flow in update-prod.sh und Build-time-Einbindung der Versionsnummer im Footer mit Copyright-Link. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+8
-4
@@ -2,12 +2,16 @@
|
|||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG APP_VERSION=0.1.0.0-dev
|
||||||
|
ENV VITE_APP_VERSION=$APP_VERSION
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY package*.json ./
|
COPY client/package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Copy code and build production bundle
|
# Copy client sources and repo version file
|
||||||
COPY . .
|
COPY client/ .
|
||||||
|
COPY VERSION ./VERSION
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# --- Production Stage ---
|
# --- Production Stage ---
|
||||||
@@ -15,7 +19,7 @@ FROM nginx:1.25-alpine
|
|||||||
WORKDIR /usr/share/nginx/html
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
# Copy custom Nginx configuration
|
# Copy custom Nginx configuration
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Copy built assets from builder
|
# Copy built assets from builder
|
||||||
COPY --from=builder /app/dist .
|
COPY --from=builder /app/dist .
|
||||||
|
|||||||
+45
-1
@@ -1949,7 +1949,7 @@ body:has(.theme-cupertino) {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
bottom: calc(36px + env(safe-area-inset-bottom, 0px));
|
||||||
z-index: 1200;
|
z-index: 1200;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
@@ -2107,4 +2107,48 @@ body:has(.theme-cupertino) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-version-footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 900;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px calc(6px + env(safe-area-inset-bottom, 0px));
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #64748b;
|
||||||
|
background: rgba(11, 12, 16, 0.72);
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version-footer a,
|
||||||
|
.app-version-footer button {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version-footer__version {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version-footer__sep {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version-footer__copyright {
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version-footer__copyright:hover {
|
||||||
|
color: #e2e8f0;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getActiveMasterKey, logoutUser } from './services/auth.js'
|
|||||||
import { startBackgroundSync, stopBackgroundSync, syncAllLogbooks, subscribeToSyncState } from './services/sync.js'
|
import { startBackgroundSync, stopBackgroundSync, syncAllLogbooks, subscribeToSyncState } from './services/sync.js'
|
||||||
import ReadOnlyViewer from './components/ReadOnlyViewer.tsx'
|
import ReadOnlyViewer from './components/ReadOnlyViewer.tsx'
|
||||||
import PwaInstallPrompt from './components/PwaInstallPrompt.tsx'
|
import PwaInstallPrompt from './components/PwaInstallPrompt.tsx'
|
||||||
|
import AppFooter from './components/AppFooter.tsx'
|
||||||
import { db } from './services/db.js'
|
import { db } from './services/db.js'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { Ship, LogOut, ChevronLeft, Users, Compass, FileText, Settings, Wifi, WifiOff } from 'lucide-react'
|
import { Ship, LogOut, ChevronLeft, Users, Compass, FileText, Settings, Wifi, WifiOff } from 'lucide-react'
|
||||||
@@ -319,6 +320,7 @@ export default function AppWrapper() {
|
|||||||
return (
|
return (
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<App />
|
<App />
|
||||||
|
<AppFooter />
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev'
|
||||||
|
|
||||||
|
export default function AppFooter() {
|
||||||
|
return (
|
||||||
|
<footer className="app-version-footer">
|
||||||
|
<span className="app-version-footer__version">v{APP_VERSION}</span>
|
||||||
|
<span className="app-version-footer__sep" aria-hidden="true">
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
<a className="app-version-footer__copyright" href="mailto:elpatron+kd@mailbox.org">
|
||||||
|
© 2026 Markus F.J. Busche
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare const __APP_VERSION__: string
|
||||||
@@ -1,9 +1,28 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { resolve, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
function readAppVersion(): string {
|
||||||
|
if (process.env.VITE_APP_VERSION) {
|
||||||
|
return process.env.VITE_APP_VERSION.trim()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return readFileSync(resolve(__dirname, '../VERSION'), 'utf8').trim()
|
||||||
|
} catch {
|
||||||
|
return '0.1.0.0-dev'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(readAppVersion())
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
+4
-2
@@ -33,8 +33,10 @@ services:
|
|||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./client
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: client/Dockerfile
|
||||||
|
args:
|
||||||
|
APP_VERSION: ${APP_VERSION:-0.1.0.0-dev}
|
||||||
container_name: daagbox-prod-frontend
|
container_name: daagbox-prod-frontend
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+124
-15
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# Remote deployment configuration
|
# Remote deployment configuration
|
||||||
# Override any of these via environment variables if needed, e.g.:
|
# Override any of these via environment variables if needed, e.g.:
|
||||||
# REMOTE_HOST=192.168.1.10 ./scripts/update-prod.sh
|
# REMOTE_HOST=192.168.1.10 ./scripts/update-prod.sh
|
||||||
@@ -13,18 +15,123 @@ COMPOSE_FILE="docker-compose.yml"
|
|||||||
BACKEND_CONTAINER="daagbox-prod-backend"
|
BACKEND_CONTAINER="daagbox-prod-backend"
|
||||||
MAX_WAIT=35
|
MAX_WAIT=35
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
VERSION_FILE="$REPO_ROOT/VERSION"
|
||||||
|
DEFAULT_VERSION="0.1.0.0"
|
||||||
|
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
# Translates to: Kapteins Daagbox Production Environment Update
|
|
||||||
echo " Kapteins Daagbox Prod Environment Update "
|
echo " Kapteins Daagbox Prod Environment Update "
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
echo "Target: ${REMOTE_TARGET}:${REMOTE_DIR}"
|
echo "Target: ${REMOTE_TARGET}:${REMOTE_DIR}"
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
read_current_version() {
|
||||||
|
if [ -f "$VERSION_FILE" ]; then
|
||||||
|
tr -d '[:space:]' < "$VERSION_FILE"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local latest_tag
|
||||||
|
latest_tag="$(git tag -l 'v*' --sort=-v:refname | head -n 1 || true)"
|
||||||
|
if [ -n "$latest_tag" ]; then
|
||||||
|
echo "${latest_tag#v}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$DEFAULT_VERSION"
|
||||||
|
}
|
||||||
|
|
||||||
|
bump_patch_version() {
|
||||||
|
local version="$1"
|
||||||
|
local major minor patch build
|
||||||
|
|
||||||
|
IFS='.' read -r major minor patch build <<< "$version"
|
||||||
|
major="${major:-0}"
|
||||||
|
minor="${minor:-1}"
|
||||||
|
patch="${patch:-0}"
|
||||||
|
build="${build:-0}"
|
||||||
|
|
||||||
|
build=$((10#$build + 1))
|
||||||
|
echo "${major}.${minor}.${patch}.${build}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_clean_git_tree() {
|
||||||
|
if git diff-index --quiet HEAD -- && [ -z "$(git status --porcelain)" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Uncommitted local changes detected:"
|
||||||
|
git status --short
|
||||||
|
echo ""
|
||||||
|
read -r -p "Commit all changes now before release? [y/N] " answer
|
||||||
|
|
||||||
|
if [[ ! "$answer" =~ ^[yY]$ ]]; then
|
||||||
|
echo "Aborting: working tree is not clean."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -p "Commit message: " commit_message
|
||||||
|
if [ -z "$commit_message" ]; then
|
||||||
|
echo "Aborting: commit message is required."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "$commit_message"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_release() {
|
||||||
|
local current_version release_version next_version tag_name
|
||||||
|
|
||||||
|
ensure_clean_git_tree
|
||||||
|
|
||||||
|
current_version="$(read_current_version)"
|
||||||
|
release_version="$current_version"
|
||||||
|
next_version="$(bump_patch_version "$current_version")"
|
||||||
|
tag_name="v${release_version}"
|
||||||
|
|
||||||
|
if git rev-parse "$tag_name" >/dev/null 2>&1; then
|
||||||
|
echo "Error: Git tag '$tag_name' already exists."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$next_version" > "$VERSION_FILE"
|
||||||
|
git add "$VERSION_FILE"
|
||||||
|
git commit -m "chore: release ${tag_name}"
|
||||||
|
git tag -a "$tag_name" -m "Release ${tag_name}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Prepared release ${tag_name}"
|
||||||
|
echo " Released: ${tag_name}"
|
||||||
|
echo " Next prep: v${next_version}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -r -p "Push commit and tag to origin? [Y/n] " push_answer
|
||||||
|
if [[ ! "$push_answer" =~ ^[nN]$ ]]; then
|
||||||
|
current_branch="$(git branch --show-current)"
|
||||||
|
git push origin "$current_branch"
|
||||||
|
git push origin "$tag_name"
|
||||||
|
echo "Pushed ${current_branch} and ${tag_name} to origin."
|
||||||
|
else
|
||||||
|
echo "Skipped push. Remote host must receive this commit/tag manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
export APP_VERSION="$release_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_release
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo "Deploying ${APP_VERSION} to ${REMOTE_TARGET}:${REMOTE_DIR}"
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
# Run the whole update procedure remotely over SSH.
|
# Run the whole update procedure remotely over SSH.
|
||||||
# The remote arguments are forwarded positionally so the heredoc can stay
|
|
||||||
# single-quoted (no local variable expansion / escaping surprises).
|
|
||||||
ssh -o ConnectTimeout=10 "$REMOTE_TARGET" 'bash -s' -- \
|
ssh -o ConnectTimeout=10 "$REMOTE_TARGET" 'bash -s' -- \
|
||||||
"$REMOTE_DIR" "$COMPOSE_FILE" "$BACKEND_CONTAINER" "$MAX_WAIT" "$REMOTE_HOST" <<'REMOTE_SCRIPT'
|
"$REMOTE_DIR" "$COMPOSE_FILE" "$BACKEND_CONTAINER" "$MAX_WAIT" "$REMOTE_HOST" "$APP_VERSION" <<'REMOTE_SCRIPT'
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
REMOTE_DIR="$1"
|
REMOTE_DIR="$1"
|
||||||
@@ -32,27 +139,32 @@ COMPOSE_FILE="$2"
|
|||||||
BACKEND_CONTAINER="$3"
|
BACKEND_CONTAINER="$3"
|
||||||
MAX_WAIT="$4"
|
MAX_WAIT="$4"
|
||||||
REMOTE_HOST="$5"
|
REMOTE_HOST="$5"
|
||||||
|
APP_VERSION="$6"
|
||||||
|
|
||||||
# Change to the deployment directory on the remote host
|
|
||||||
cd "$REMOTE_DIR" || { echo "Error: Remote directory '$REMOTE_DIR' not found."; exit 1; }
|
cd "$REMOTE_DIR" || { echo "Error: Remote directory '$REMOTE_DIR' not found."; exit 1; }
|
||||||
|
|
||||||
# 1. Pull latest code changes
|
|
||||||
echo "Pulling latest changes from Git..."
|
echo "Pulling latest changes from Git..."
|
||||||
git pull
|
git pull --tags
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Error: Git pull failed."
|
echo "Error: Git pull failed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Build docker images without cache
|
REMOTE_VERSION="$(tr -d '[:space:]' < VERSION)"
|
||||||
echo "Rebuilding Docker images without cache..."
|
if [ "$REMOTE_VERSION" != "$APP_VERSION" ]; then
|
||||||
|
echo "Note: Remote VERSION file already points to next release (v${REMOTE_VERSION})."
|
||||||
|
echo " Building deployed release v${APP_VERSION}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
export APP_VERSION="$APP_VERSION"
|
||||||
|
|
||||||
|
echo "Rebuilding Docker images without cache (APP_VERSION=${APP_VERSION})..."
|
||||||
docker compose -f "$COMPOSE_FILE" build --no-cache
|
docker compose -f "$COMPOSE_FILE" build --no-cache
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Error: Docker compose build failed."
|
echo "Error: Docker compose build failed."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Spin up the containers
|
|
||||||
echo "Starting updated container stack..."
|
echo "Starting updated container stack..."
|
||||||
docker compose -f "$COMPOSE_FILE" up -d
|
docker compose -f "$COMPOSE_FILE" up -d
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@@ -60,14 +172,12 @@ if [ $? -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Clean up old/stopped Docker assets (containers, networks, dangling images, cache)
|
|
||||||
echo "Cleaning up old/unused Docker resources..."
|
echo "Cleaning up old/unused Docker resources..."
|
||||||
docker system prune -f
|
docker system prune -f
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Warning: Docker system prune failed to run completely."
|
echo "Warning: Docker system prune failed to run completely."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. Wait for services to become healthy (Prisma migrations & DB check)
|
|
||||||
echo "Waiting for services to become healthy..."
|
echo "Waiting for services to become healthy..."
|
||||||
COUNTER=0
|
COUNTER=0
|
||||||
IS_READY=false
|
IS_READY=false
|
||||||
@@ -82,7 +192,6 @@ while [ $COUNTER -lt $MAX_WAIT ]; do
|
|||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
COUNTER=$((COUNTER + 1))
|
COUNTER=$((COUNTER + 1))
|
||||||
# Show simple progress dots
|
|
||||||
printf "."
|
printf "."
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
@@ -94,6 +203,7 @@ echo "=================================================="
|
|||||||
|
|
||||||
if [ "$IS_READY" = true ]; then
|
if [ "$IS_READY" = true ]; then
|
||||||
echo "SUCCESS: Production environment updated and healthy!"
|
echo "SUCCESS: Production environment updated and healthy!"
|
||||||
|
echo " -> Version: v${APP_VERSION}"
|
||||||
echo " -> App Frontend (Nginx): http://${REMOTE_HOST}"
|
echo " -> App Frontend (Nginx): http://${REMOTE_HOST}"
|
||||||
echo " -> Backend API Health: http://${REMOTE_HOST}/api/health"
|
echo " -> Backend API Health: http://${REMOTE_HOST}/api/health"
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
@@ -106,11 +216,10 @@ else
|
|||||||
fi
|
fi
|
||||||
REMOTE_SCRIPT
|
REMOTE_SCRIPT
|
||||||
|
|
||||||
# Capture and report the remote exit status locally
|
|
||||||
REMOTE_EXIT=$?
|
REMOTE_EXIT=$?
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
if [ $REMOTE_EXIT -eq 0 ]; then
|
if [ $REMOTE_EXIT -eq 0 ]; then
|
||||||
echo "Remote update completed successfully on ${REMOTE_TARGET}."
|
echo "Remote update completed successfully on ${REMOTE_TARGET} (v${APP_VERSION})."
|
||||||
elif [ $REMOTE_EXIT -eq 3 ]; then
|
elif [ $REMOTE_EXIT -eq 3 ]; then
|
||||||
echo "Remote update finished, but the backend was not healthy in time on ${REMOTE_TARGET}."
|
echo "Remote update finished, but the backend was not healthy in time on ${REMOTE_TARGET}."
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user