name: Release on: push: tags: - "v*" # e.g. v1.0.0, v1.2.3 concurrency: group: ${{ gitea.workflow }}-${{ gitea.ref }} cancel-in-progress: true jobs: # ── backend: vet & test ─────────────────────────────────────────────────────── test-backend: name: Test backend runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: backend/go.mod cache-dependency-path: backend/go.sum - name: go vet working-directory: backend run: go vet ./... - name: Run tests working-directory: backend run: go test -short -race -count=1 -timeout=60s ./... # ── ui: type-check & build ──────────────────────────────────────────────────── check-ui: name: Check ui runs-on: ubuntu-latest defaults: run: working-directory: ui steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: ui/package-lock.json - name: Install dependencies run: npm ci - name: Type check run: npm run check - name: Build run: npm run build - name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: ui-build 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: build + push all images via docker bake ────────────────────────── # docker-bake.hcl owns all tag logic (semver, major.minor, latest) via HCL # functions — no docker/metadata-action needed. BuildKit builds all five # targets in parallel, sharing the Go builder stage across backend/runner/pocketbase. docker: name: Docker runs-on: ubuntu-latest needs: [test-backend, check-ui] steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_TOKEN }} - name: Download ui build artifacts uses: actions/download-artifact@v3 with: name: ui-build path: ui/build - name: Allow build/ into Docker context (override .dockerignore) run: | grep -v '^build$' ui/.dockerignore > ui/.dockerignore.tmp mv ui/.dockerignore.tmp ui/.dockerignore - name: Build and push all images uses: docker/bake-action@v6 with: files: docker-bake.hcl set: | *.output=type=image,push=true env: GIT_TAG: ${{ gitea.ref_name }} 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. # Copies the compose file from the tagged commit to the server, pulls the new # images, and restarts only the services whose image or config changed. # --remove-orphans cleans up containers no longer defined in the compose file # (e.g. the now-removed pb-init container). # # Required Gitea secrets: # PROD_HOST — prod server IP or hostname # PROD_USER — SSH login user (typically root) # PROD_SSH_KEY — private key whose public half is in authorized_keys # PROD_SSH_KNOWN_HOSTS — output of: ssh-keyscan -H deploy: name: Deploy to prod runs-on: ubuntu-latest needs: [docker] steps: - uses: actions/checkout@v4 - name: Install SSH key run: | mkdir -p ~/.ssh printf '%s\n' "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key printf '%s\n' "${{ secrets.PROD_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts - name: Copy docker-compose.yml to prod run: | scp -i ~/.ssh/deploy_key \ docker-compose.yml \ "${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:/opt/libnovel/docker-compose.yml" - name: Pull new images and restart changed services run: | ssh -i ~/.ssh/deploy_key \ "${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}" \ 'set -euo pipefail cd /opt/libnovel doppler run -- docker compose pull backend runner ui caddy pocketbase doppler run -- docker compose up -d --remove-orphans' # ── Gitea release ───────────────────────────────────────────────────────────── release: name: Gitea Release runs-on: ubuntu-latest needs: [docker] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Extract release notes from tag commit id: notes run: | set -euo pipefail # Subject line (first line of commit message) → release title SUBJECT=$(git log -1 --format="%s" "${{ gitea.sha }}") # Body (everything after the blank line) → release body BODY=$(git log -1 --format="%b" "${{ gitea.sha }}" | sed '/^Co-Authored-By:/d' | sed '/^[[:space:]]*$/{ N; /^\n$/d }' | sed 's/^[[:space:]]*$//' | awk 'NF || !p; {p = !NF}') echo "title=${SUBJECT}" >> "$GITHUB_OUTPUT" # Use a heredoc delimiter to safely handle multi-line body { echo "body<> "$GITHUB_OUTPUT" - name: Create release uses: https://gitea.com/actions/gitea-release-action@v1 with: token: ${{ secrets.GITEA_TOKEN }} title: ${{ steps.notes.outputs.title }} body: ${{ steps.notes.outputs.body }}