Compare commits

...

6 Commits

Author SHA1 Message Date
Admin
2864c4a6c0 chore: clean up release workflow and document Doppler usage
Some checks failed
Release / Test backend (push) Successful in 54s
Release / Check ui (push) Successful in 2m0s
Release / Docker (push) Failing after 2m20s
Release / Deploy to prod (push) Has been skipped
Release / Gitea Release (push) Has been skipped
2026-04-15 20:14:45 +05:00
Admin
6d0dac256d fix: simplify bake file to avoid locals/function blocks (buildx compat)
Some checks failed
Release / Test backend (push) Successful in 59s
Release / Check ui (push) Successful in 1m56s
Release / Docker (push) Failing after 2m21s
Release / Deploy to prod (push) Has been skipped
Release / Gitea Release (push) Has been skipped
The Gitea runner's docker buildx doesn't support HCL locals{} or function{}
blocks (added in buildx 0.12+). Replace with plain variables: VERSION and
MAJOR_MINOR are pre-computed in a CI step and passed as env vars to bake.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 19:43:54 +05:00
Admin
8922111471 fix: pin meilisearch to v1.40.0
Some checks failed
Release / Test backend (push) Successful in 56s
Release / Check ui (push) Successful in 2m10s
Release / Docker (push) Failing after 1m36s
Release / Deploy to prod (push) Has been skipped
Release / Gitea Release (push) Has been skipped
v1.42.1 (pulled by latest) is incompatible with existing data (db version
1.40.0). Pinning to v1.40.0 until a deliberate migration is planned.
Upgrade path: https://www.meilisearch.com/docs/learn/update_and_migration/updating

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 19:24:57 +05:00
Admin
74e7c8e8d1 chore: move tag logic into bake file, drop all metadata-action steps
Some checks failed
Release / Test backend (push) Successful in 59s
Release / Check ui (push) Successful in 1m55s
Release / Docker (push) Failing after 1m27s
Release / Deploy to prod (push) Has been skipped
Release / Gitea Release (push) Has been skipped
docker-bake.hcl now owns semver tag generation via HCL locals + a reusable
img_tags() function: GIT_TAG="v4.1.5" → :4.1.5, :4.1, :latest on all images.
The Docker job shrinks from 13 steps to 6 — no docker/metadata-action needed.

CI simply passes GIT_TAG, COMMIT, BUILD_TIME as env vars to bake-action.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 19:22:06 +05:00
Admin
2f74b2b229 fix: only pull app images during deploy, not infra images
Some checks failed
Release / Test backend (push) Successful in 1m8s
Release / Check ui (push) Successful in 1m59s
Release / Docker (push) Failing after 1m37s
Release / Deploy to prod (push) Has been skipped
Release / Gitea Release (push) Has been skipped
docker compose pull without arguments pulls ALL services including
getmeili/meilisearch:latest — a recent Meilisearch version bump broke
compatibility with existing data on the server, causing meilisearch to
crash immediately on restart.

CI deploy now only pulls the 5 custom app images (backend, runner, ui,
caddy, pocketbase). Infrastructure images (meilisearch, valkey, minio,
redis, crowdsec) are updated deliberately via `just pull-infra`.

Also add pocketbase to `just pull-images` now that it's a custom image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 19:19:31 +05:00
Admin
cb9598a786 chore: migrate Docker builds to docker buildx bake
Some checks failed
Release / Test backend (push) Successful in 54s
Release / Check ui (push) Successful in 2m10s
Release / Docker (push) Failing after 3m43s
Release / Deploy to prod (push) Has been skipped
Release / Gitea Release (push) Has been skipped
Replace 5 sequential build-push-action steps with a single docker/bake-action
call backed by docker-bake.hcl. BuildKit now builds all images in parallel:
backend/runner/pocketbase share one Go builder stage (compiled once), while
caddy and ui build concurrently alongside the Go targets.

Workflow YAML goes from ~130 lines of build steps to ~35. Adding a new image
now only requires a new target block in docker-bake.hcl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 19:09:27 +05:00
5 changed files with 141 additions and 210 deletions

View File

@@ -62,95 +62,7 @@ jobs:
path: ui/build
retention-days: 1
# ── ui: source map upload ─────────────────────────────────────────────────────
# Commented out — re-enable when GlitchTip source map uploads are needed again.
#
# upload-sourcemaps:
# name: Upload source maps
# runs-on: ubuntu-latest
# needs: [check-ui]
# steps:
# - name: Compute release version (strip leading v)
# id: ver
# run: |
# V="${{ gitea.ref_name }}"
# echo "version=${V#v}" >> "$GITHUB_OUTPUT"
#
# - name: Download build artifacts
# uses: actions/download-artifact@v3
# with:
# name: ui-build
# path: build
#
# - name: Install sentry-cli
# run: npm install -g @sentry/cli
#
# - name: Inject debug IDs into build artifacts
# run: sentry-cli sourcemaps inject ./build
# env:
# SENTRY_URL: https://errors.libnovel.cc/
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
# SENTRY_ORG: libnovel
# SENTRY_PROJECT: ui
#
# - name: Upload injected build (for docker-ui)
# uses: actions/upload-artifact@v3
# with:
# name: ui-build-injected
# path: build
# retention-days: 1
#
# - name: Create GlitchTip release
# run: sentry-cli releases new ${{ steps.ver.outputs.version }}
# env:
# SENTRY_URL: https://errors.libnovel.cc/
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
# SENTRY_ORG: libnovel
# SENTRY_PROJECT: ui
#
# - name: Upload source maps to GlitchTip
# run: sentry-cli sourcemaps upload ./build --release ${{ steps.ver.outputs.version }}
# env:
# SENTRY_URL: https://errors.libnovel.cc/
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
# SENTRY_ORG: libnovel
# SENTRY_PROJECT: ui
#
# - name: Finalize GlitchTip release
# run: sentry-cli releases finalize ${{ steps.ver.outputs.version }}
# env:
# SENTRY_URL: https://errors.libnovel.cc/
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
# SENTRY_ORG: libnovel
# SENTRY_PROJECT: ui
#
# - name: Prune old GlitchTip releases (keep latest 10)
# run: |
# set -euo pipefail
# KEEP=10
# OLD=$(curl -sf \
# -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
# "$SENTRY_URL/api/0/organizations/$SENTRY_ORG/releases/?project=$SENTRY_PROJECT&per_page=100" \
# | python3 -c "
# import sys, json
# releases = json.load(sys.stdin)
# for r in releases[$KEEP:]:
# print(r['version'])
# " KEEP=$KEEP)
# for ver in $OLD; do
# echo "Deleting old release: $ver"
# sentry-cli releases delete "$ver" || true
# done
# env:
# SENTRY_URL: https://errors.libnovel.cc
# SENTRY_AUTH_TOKEN: ${{ secrets.GLITCHTIP_AUTH_TOKEN }}
# SENTRY_ORG: libnovel
# SENTRY_PROJECT: ui
# ── docker: all images in one job (single login) ──────────────────────────────
# backend, runner, ui, and caddy are built sequentially in one job so Docker
# Hub only needs to be authenticated once. This also eliminates 3 redundant
# checkout + setup-buildx + scheduler round-trips compared to separate jobs.
# ── docker: build + push all images via docker bake ──────────────────────────
docker:
name: Docker
runs-on: ubuntu-latest
@@ -160,65 +72,20 @@ jobs:
- uses: docker/setup-buildx-action@v3
# Single login — credential is written to ~/.docker/config.json and
# reused by all subsequent build-push-action steps in this job.
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
# ── backend ──────────────────────────────────────────────────────────────
- name: Docker meta / backend
id: meta-backend
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USER }}/libnovel-backend
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Compute version tags
id: ver
run: |
V="${{ gitea.ref_name }}"
VER="${V#v}"
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "major_minor=$(echo "$VER" | cut -d. -f1-2)" >> "$GITHUB_OUTPUT"
- name: Build and push / backend
uses: docker/build-push-action@v6
with:
context: backend
target: backend
push: true
tags: ${{ steps.meta-backend.outputs.tags }}
labels: ${{ steps.meta-backend.outputs.labels }}
build-args: |
VERSION=${{ steps.meta-backend.outputs.version }}
COMMIT=${{ gitea.sha }}
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-backend:latest
cache-to: type=inline
# ── runner ───────────────────────────────────────────────────────────────
- name: Docker meta / runner
id: meta-runner
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USER }}/libnovel-runner
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push / runner
uses: docker/build-push-action@v6
with:
context: backend
target: runner
push: true
tags: ${{ steps.meta-runner.outputs.tags }}
labels: ${{ steps.meta-runner.outputs.labels }}
build-args: |
VERSION=${{ steps.meta-runner.outputs.version }}
COMMIT=${{ gitea.sha }}
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-runner:latest
cache-to: type=inline
# ── ui ───────────────────────────────────────────────────────────────────
- name: Download ui build artifacts
uses: actions/download-artifact@v3
with:
@@ -230,73 +97,17 @@ jobs:
grep -v '^build$' ui/.dockerignore > ui/.dockerignore.tmp
mv ui/.dockerignore.tmp ui/.dockerignore
- name: Docker meta / ui
id: meta-ui
uses: docker/metadata-action@v5
- name: Build and push all images
uses: docker/bake-action@v6
with:
images: ${{ secrets.DOCKER_USER }}/libnovel-ui
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push / ui
uses: docker/build-push-action@v6
with:
context: ui
push: true
tags: ${{ steps.meta-ui.outputs.tags }}
labels: ${{ steps.meta-ui.outputs.labels }}
build-args: |
BUILD_VERSION=${{ steps.meta-ui.outputs.version }}
BUILD_COMMIT=${{ gitea.sha }}
BUILD_TIME=${{ gitea.event.head_commit.timestamp }}
PREBUILT=1
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-ui:latest
cache-to: type=inline
# ── caddy ────────────────────────────────────────────────────────────────
- name: Docker meta / caddy
id: meta-caddy
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USER }}/libnovel-caddy
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push / caddy
uses: docker/build-push-action@v6
with:
context: caddy
push: true
tags: ${{ steps.meta-caddy.outputs.tags }}
labels: ${{ steps.meta-caddy.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-caddy:latest
cache-to: type=inline
# ── pocketbase ───────────────────────────────────────────────────────────
- name: Docker meta / pocketbase
id: meta-pocketbase
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USER }}/libnovel-pocketbase
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push / pocketbase
uses: docker/build-push-action@v6
with:
context: backend
target: pocketbase
push: true
tags: ${{ steps.meta-pocketbase.outputs.tags }}
labels: ${{ steps.meta-pocketbase.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_USER }}/libnovel-pocketbase:latest
cache-to: type=inline
files: docker-bake.hcl
set: |
*.output=type=image,push=true
env:
VERSION: ${{ steps.ver.outputs.version }}
MAJOR_MINOR: ${{ steps.ver.outputs.major_minor }}
COMMIT: ${{ gitea.sha }}
BUILD_TIME: ${{ gitea.event.head_commit.timestamp }}
# ── deploy: sync docker-compose.yml + restart prod ───────────────────────────
# Runs after all images are pushed to Docker Hub.
@@ -336,7 +147,7 @@ jobs:
"${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \
'set -euo pipefail
cd /opt/libnovel
doppler run -- docker compose pull
doppler run -- docker compose pull backend runner ui caddy pocketbase
doppler run -- docker compose up -d --remove-orphans'
# ── Gitea release ─────────────────────────────────────────────────────────────

View File

@@ -2,6 +2,17 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Environment / Secrets
All project environment variables are stored in **Doppler**. When you need to access any secret or env var (e.g. API tokens, database URLs, credentials), fetch them via:
```bash
doppler run -- <command> # inject all secrets into a command
doppler secrets get SECRET_NAME # inspect a specific secret
```
Never use `.env` files. Do not ask the user to provide secrets manually — they are available via Doppler.
## Commands
### Docker (via `just` — the primary way to run services)

109
docker-bake.hcl Normal file
View File

@@ -0,0 +1,109 @@
# docker-bake.hcl — defines all five production images.
#
# Uses only plain variables for broad buildx compatibility (no locals/functions).
# CI pre-computes VERSION and MAJOR_MINOR from the git tag and passes them as
# env vars. Locally, everything gets a :dev tag.
#
# Local build (no push):
# docker buildx bake
#
# CI passes: DOCKER_USER, VERSION, MAJOR_MINOR, COMMIT, BUILD_TIME
variable "DOCKER_USER" { default = "kalekber" }
variable "VERSION" { default = "dev" } # e.g. "4.1.6" (no leading v)
variable "MAJOR_MINOR" { default = "dev" } # e.g. "4.1"
variable "COMMIT" { default = "unknown" }
variable "BUILD_TIME" { default = "" }
# ── Shared defaults ───────────────────────────────────────────────────────────
target "_defaults" {
pull = true
# CI overrides to push=true via --set *.output=type=image,push=true
output = ["type=image,push=false"]
cache-to = ["type=inline"]
}
# ── Go targets (share the backend/ build context + builder stage) ─────────────
target "backend" {
inherits = ["_defaults"]
context = "backend"
target = "backend"
tags = [
"${DOCKER_USER}/libnovel-backend:${VERSION}",
"${DOCKER_USER}/libnovel-backend:${MAJOR_MINOR}",
"${DOCKER_USER}/libnovel-backend:latest",
]
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-backend:latest"]
args = {
VERSION = VERSION
COMMIT = COMMIT
}
}
target "runner" {
inherits = ["_defaults"]
context = "backend"
target = "runner"
tags = [
"${DOCKER_USER}/libnovel-runner:${VERSION}",
"${DOCKER_USER}/libnovel-runner:${MAJOR_MINOR}",
"${DOCKER_USER}/libnovel-runner:latest",
]
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-runner:latest"]
args = {
VERSION = VERSION
COMMIT = COMMIT
}
}
target "pocketbase" {
inherits = ["_defaults"]
context = "backend"
target = "pocketbase"
tags = [
"${DOCKER_USER}/libnovel-pocketbase:${VERSION}",
"${DOCKER_USER}/libnovel-pocketbase:${MAJOR_MINOR}",
"${DOCKER_USER}/libnovel-pocketbase:latest",
]
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-pocketbase:latest"]
}
# ── UI (SvelteKit — separate context) ────────────────────────────────────────
target "ui" {
inherits = ["_defaults"]
context = "ui"
tags = [
"${DOCKER_USER}/libnovel-ui:${VERSION}",
"${DOCKER_USER}/libnovel-ui:${MAJOR_MINOR}",
"${DOCKER_USER}/libnovel-ui:latest",
]
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-ui:latest"]
args = {
BUILD_VERSION = VERSION
BUILD_COMMIT = COMMIT
BUILD_TIME = BUILD_TIME
PREBUILT = "1"
}
}
# ── Caddy (custom plugins — separate context) ─────────────────────────────────
target "caddy" {
inherits = ["_defaults"]
context = "caddy"
tags = [
"${DOCKER_USER}/libnovel-caddy:${VERSION}",
"${DOCKER_USER}/libnovel-caddy:${MAJOR_MINOR}",
"${DOCKER_USER}/libnovel-caddy:latest",
]
cache-from = ["type=registry,ref=${DOCKER_USER}/libnovel-caddy:latest"]
}
# ── Default group: all five images ────────────────────────────────────────────
group "default" {
targets = ["backend", "runner", "pocketbase", "ui", "caddy"]
}

View File

@@ -96,7 +96,7 @@ services:
# ─── Meilisearch (full-text search) ──────────────────────────────────────────
meilisearch:
image: getmeili/meilisearch:latest
image: getmeili/meilisearch:v1.40.0
restart: unless-stopped
environment:
MEILI_MASTER_KEY: "${MEILI_MASTER_KEY}"

View File

@@ -63,7 +63,7 @@ build-push: build push
# Pull all images from Docker Hub (uses GIT_TAG from Doppler)
pull-images:
{{doppler}} docker compose pull backend runner ui caddy
{{doppler}} docker compose pull backend runner ui caddy pocketbase
# Pull all third-party base images (minio, pocketbase, etc.)
pull-infra: