From 43d350bd3255cf2ba64171d9e1da121a8430d23c Mon Sep 17 00:00:00 2001 From: urania Date: Fri, 26 Jun 2026 10:40:34 +0200 Subject: [PATCH] feat: ci/cd flow --- .env.example | 7 +++++- .gitea/workflows/release.yaml | 41 +++++++++++++++++++++++++++++++++++ .gitignore | 4 +++- Dockerfile | 8 +++++-- docker-compose.yaml | 20 +++++++++++++++++ release.sh | 30 +++++++++++++++++++++++++ src/hooks.server.ts | 19 ++++++++++++++-- 7 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 .gitea/workflows/release.yaml create mode 100644 docker-compose.yaml create mode 100755 release.sh diff --git a/.env.example b/.env.example index ecb40fd..15c7bed 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,7 @@ -# Drizzle +# Drizzle (local dev only; the container sets its own DATABASE_URL) DATABASE_URL=file:local.db + +# docker compose — generate secrets with: openssl rand -base64 32 +ORIGIN=http://localhost:3000 +CRYPTO_SECRET= +BETTER_AUTH_SECRET= diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..3fb0d5a --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,41 @@ +# Auto-create a Gitea release when a vX.Y.Z tag is pushed. +# Image build/push is handled by Komodo, so this only cuts the release. +name: release +on: + push: + tags: + - "v*" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # need full history to diff against the previous tag + + - name: Create release from tag + run: | + TAG="${{ github.ref_name }}" + PREV=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true) + RANGE="${PREV:+$PREV..}$TAG" + LOG=$(git log --pretty='format:%s|%h' "$RANGE") + + group() { echo "$LOG" | awk -F'|' -v re="$1" '$1 ~ re { sub(re, "", $1); printf "- %s (%s)\n", $1, $2 }'; } + BREAKING=$(group '^[a-z]+[^:]*!: ') + FEATS=$(group '^feat[^:]*: ') + FIXES=$(group '^fix[^:]*: ') + OTHER=$(echo "$LOG" | awk -F'|' '$1 !~ /^(feat|fix)[^:]*!?: / { printf "- %s (%s)\n", $1, $2 }') + + BODY="" + [ -n "$BREAKING" ] && BODY="$BODY### Breaking"$'\n'"$BREAKING"$'\n\n' + [ -n "$FEATS" ] && BODY="$BODY### Features"$'\n'"$FEATS"$'\n\n' + [ -n "$FIXES" ] && BODY="$BODY### Fixes"$'\n'"$FIXES"$'\n\n' + [ -n "$OTHER" ] && BODY="$BODY### Other"$'\n'"$OTHER"$'\n\n' + [ -n "$PREV" ] && BODY="$BODY**Full changelog:** ${{ github.server_url }}/${{ github.repository }}/compare/$PREV...$TAG" + jq -n --arg tag "$TAG" --arg body "$BODY" \ + '{tag_name:$tag, name:$tag, body:$body}' \ + | curl -fsSL -X POST "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Content-Type: application/json" \ + --data @- diff --git a/.gitignore b/.gitignore index 7b8c9c8..577d538 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ src/lib/paraglide project.inlang/cache/ # SQLite *.db -CONTEXT.md \ No newline at end of file +CONTEXT.md +build.sh +.claude \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6714ec7..d599277 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,10 +15,14 @@ FROM base AS release ENV NODE_ENV=production \ DATABASE_URL=/app/data/db.sqlite \ HOST=0.0.0.0 \ - PORT=3000 + PORT=3000 \ + ORIGIN=http://localhost:3000 \ + REPOSITORY_URL=https://tea.urania.dev/api/v1/repos/urania/nadir-agent/releases/latest # full node_modules: drizzle-kit (devDep) is needed for `drizzle-kit migrate` at startup +WORKDIR /app COPY --from=install /app/node_modules node_modules COPY --from=build /app/build build +COPY --from=build /app/config config COPY drizzle drizzle COPY drizzle.config.ts package.json ./ RUN mkdir -p /app/data @@ -27,4 +31,4 @@ RUN bun run db:migrate VOLUME /app/data EXPOSE 3000 # apply migrations (creates db.sqlite if absent), then start the server -CMD ["bun ./build/index.js"] +CMD ["sh", "-c", "bun run db:migrate && bun /app/build/index.js"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..dd3de17 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,20 @@ +# Example stack. nadir-agent runs on each managed host, not here — this is just the web UI. +# Secrets come from a .env file next to this one (see .env.example), never committed. +services: + nadir-frontend: + # pull from your private registry, or `docker compose build` locally + image: uraniadev/nadir:latest + container_name: nadir-webui + build: . + restart: unless-stopped + ports: + - "3000:3000" + environment: + ORIGIN: ${ORIGIN:-http://localhost:3000} + CRYPTO_SECRET: ${CRYPTO_SECRET:?set CRYPTO_SECRET in .env (openssl rand -base64 32)} + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?set BETTER_AUTH_SECRET in .env (openssl rand -base64 32)} + volumes: + - nadir-db:/app/data # db.sqlite folder + +volumes: + nadir-db: diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..8443d48 --- /dev/null +++ b/release.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Cut a release: bump package.json, tag, push, build+push image, create Gitea release. +# Usage: ./release.sh [patch|minor|major] (default: patch) +set -euo pipefail +cd "$(dirname "$0")" + +BUMP="${1:-patch}" +GITEA_API="${GITEA_API:-https://tea.urania.dev/api/v1}" +REPO="${REPO:-urania/nadir-webui}" +: "${GITEA_TOKEN:?set GITEA_TOKEN (Gitea > Settings > Applications > token with repo scope)}" + +[ -z "$(git status --porcelain)" ] || { echo "working tree dirty, commit first"; exit 1; } + +# bumps version in package.json AND creates commit + tag vX.Y.Z +bun pm version "$BUMP" +VERSION=$(bun -e 'console.log(require("./package.json").version)') +TAG="v$VERSION" + +git push --follow-tags + +# build + push the multi-arch image at this version +./build.sh + +# create the Gitea release from the tag +curl -fsSL -X POST "$GITEA_API/repos/$REPO/releases" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\"}" >/dev/null + +echo "released $TAG" diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 7b3dfa5..f09255b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,12 +1,27 @@ -import { type Handle, redirect } from '@sveltejs/kit'; +import { type Handle, redirect, type ServerInit } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import { building, dev } from '$app/environment'; import { getAuth } from '$lib/auth/server'; -import { getConfig } from '$lib/server/config'; import { getTextDirection } from '$lib/paraglide/runtime'; import { paraglideMiddleware } from '$lib/paraglide/server'; +import { getConfig } from '$lib/server/config'; import { svelteKitHandler } from 'better-auth/svelte-kit'; +export const init: ServerInit = () => { + const env = process.env; + const errors: string[] = []; + + for (const key of ['CRYPTO_SECRET', 'BETTER_AUTH_SECRET', 'REPOSITORY_URL']) + if (!env[key]?.trim()) errors.push(`${key} is missing`); + + for (const key of ['CRYPTO_SECRET', 'BETTER_AUTH_SECRET']) + if (env[key] && env[key]!.length < 32) + errors.push(`${key} is too short (need >= 32 chars; openssl rand -base64 32)`); + + if (errors.length) + throw new Error(`Invalid environment:\n - ${errors.join('\n - ')}`); +}; + const handleBetterAuth: Handle = async ({ event, resolve }) => { const cfg = getConfig(); const auth = getAuth();