# CI runs first; build only proceeds if lint + typecheck pass. # # Push to dev: typecheck + lint + test + build :dev + : # Push to main: typecheck + lint + test + build :latest + : # Tag v* (release): typecheck + lint + test + build :latest + : + : # # main is the production line, so a merge to main rebuilds and moves :latest to its # tip (family rule 46) — no version release required. The : image is the # immutable rollback unit for every build. # # Required secret (repo -> Settings -> Secrets -> Actions): # REGISTRY_TOKEN -- Forgejo PAT with write:packages scope # The registry username is derived from github.repository_owner (public — it's in # the image path), so no REGISTRY_USER secret is needed. name: CI & Build on: push: branches: [dev, main] tags: ["v*"] paths: - "src/**" - "frontend/**" - "tests/**" - "pyproject.toml" - "alembic/**" - "alembic.ini" - "Dockerfile" - ".forgejo/workflows/ci.yml" # Cancel older runs on the same branch when a newer push lands. Tag runs get their # own group implicitly and are never cancelled. concurrency: group: ci-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} permissions: contents: read env: REGISTRY: git.fabledsword.com IMAGE: git.fabledsword.com/bvandeusen/thoughtsync jobs: typecheck: name: TypeScript typecheck if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 steps: - uses: actions/checkout@v6 - name: Install dependencies run: npm ci working-directory: frontend - name: Type check run: npx vue-tsc --noEmit working-directory: frontend lint: name: Python lint if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 steps: - uses: actions/checkout@v6 # ruff is pre-installed in the ci-runner base image. - name: Lint run: ruff check src/ test: name: Python tests if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 steps: - uses: actions/checkout@v6 - name: Create virtual environment run: uv venv /opt/venv - name: Install package with dev deps run: uv pip install --python /opt/venv/bin/python -e ".[dev]" - name: Run tests run: /opt/venv/bin/python -m pytest tests/ -q build: name: Build & push image # Build gates on lint + typecheck. The `test` job runs in parallel for # visibility but does not block dev image builds (DB-backed integration # testing happens against the dev image manually, not on every push). needs: [typecheck, lint] if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 permissions: contents: read packages: write steps: - uses: actions/checkout@v6 - name: Generate image tags and version id: tags # run: steps execute under busybox sh (family rule 81), so use POSIX `case`, # NOT bash `[[ ]]`. run: | TAGS="${{ env.IMAGE }}:${{ github.sha }}" BUILD_VERSION="dev" case "${{ github.ref }}" in refs/heads/dev) TAGS="$TAGS,${{ env.IMAGE }}:dev" ;; refs/heads/main) # Production line: :latest tracks main's tip (rule 46). No :main tag; # the : above is the rollback unit. Version label = short sha. TAGS="$TAGS,${{ env.IMAGE }}:latest" BUILD_VERSION="$(echo ${{ github.sha }} | cut -c1-7)" ;; refs/tags/*) TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}" BUILD_VERSION="${{ github.ref_name }}" ;; esac echo "value=$TAGS" >> $GITHUB_OUTPUT echo "build_version=$BUILD_VERSION" >> $GITHUB_OUTPUT - name: Free disk space run: | docker system prune -af || true docker builder prune --keep-storage 5g -f || true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to Forgejo registry uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner }} password: ${{ secrets.REGISTRY_TOKEN }} - name: Build and push uses: docker/build-push-action@v7 with: context: . push: true provenance: false tags: ${{ steps.tags.outputs.value }} build-args: BUILD_VERSION=${{ steps.tags.outputs.build_version }} cache-from: type=registry,ref=${{ env.IMAGE }}:cache cache-to: type=registry,ref=${{ env.IMAGE }}:cache,mode=max,ignore-error=true