Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ on:
branches: [main, master]

jobs:
# ===========================================================================
# Path detection (shared with Test Suite)
# ===========================================================================
changes:
name: Detect changes
uses: ./.github/workflows/detect-changes.yml

# ===========================================================================
# PR Title Lint (conventional commits)
# ===========================================================================
Expand Down Expand Up @@ -37,74 +44,117 @@ jobs:

# ===========================================================================
# Frontend Lint & Build
# Always runs: this job name is a required status check. When there are no
# frontend-relevant paths, report success without installing or linting.
# ===========================================================================
frontend-ci:
name: Frontend Lint & Build
needs: changes
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend

steps:
- name: Skip frontend CI (no relevant changes)
if: needs.changes.outputs.frontend != 'true'
run: |
echo "No frontend-relevant paths changed; skipping Frontend Lint & Build."
{
echo "### Frontend Lint & Build"
echo
echo "Skipped — no frontend-relevant changes."
} >> "$GITHUB_STEP_SUMMARY"

- uses: actions/checkout@v7
if: needs.changes.outputs.frontend == 'true'

- uses: actions/setup-node@v7
if: needs.changes.outputs.frontend == 'true'
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: 'frontend/package-lock.json'

- name: Install dependencies
if: needs.changes.outputs.frontend == 'true'
working-directory: frontend
run: npm ci

- name: Lint
if: needs.changes.outputs.frontend == 'true'
working-directory: frontend
run: npm run lint

- name: Format check
if: needs.changes.outputs.frontend == 'true'
working-directory: frontend
run: npm run format:check

- name: Typecheck
if: needs.changes.outputs.frontend == 'true'
working-directory: frontend
run: npm run typecheck

- name: Build
if: needs.changes.outputs.frontend == 'true'
working-directory: frontend
run: npm run build
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ===========================================================================
# Backend Lint
# Always runs: this job name is a required status check. When there are no
# backend-relevant paths, report success without installing or linting.
# ===========================================================================
backend-lint:
name: Backend Lint
needs: changes
runs-on: ubuntu-latest

steps:
- name: Skip backend lint (no relevant changes)
if: needs.changes.outputs.backend != 'true'
run: |
echo "No backend-relevant paths changed; skipping Backend Lint."
{
echo "### Backend Lint"
echo
echo "Skipped — no backend-relevant changes."
} >> "$GITHUB_STEP_SUMMARY"

- uses: actions/checkout@v7
if: needs.changes.outputs.backend == 'true'

- name: Set up Python 3.11
if: needs.changes.outputs.backend == 'true'
uses: actions/setup-python@v7
with:
python-version: "3.11"

# Pin to match uv.lock / ruff-pre-commit rev so local hooks and CI agree.
- name: Install linting tools
if: needs.changes.outputs.backend == 'true'
run: pip install "ruff==0.15.7"

- name: Run ruff linter
if: needs.changes.outputs.backend == 'true'
run: ruff check comicarr/ --output-format=github

- name: Run modern backend quality ratchet
if: needs.changes.outputs.backend == 'true'
run: python scripts/run_ruff.py modern

# Deleting a module-level global cannot make its reassignment fail, so
# the only gate against GLOBAL_MESSAGES coming back is a source scan.
- name: Check retired globals stay retired
if: needs.changes.outputs.backend == 'true'
run: python scripts/check_retired_globals.py

- name: Run ruff formatter check
if: needs.changes.outputs.backend == 'true'
run: ruff format --check comicarr/

# The generated TypeScript is a backend artifact — its input is the config
# registry, so drift is caused by a Python edit and belongs in this job.
# The generator loads registry.py by path and needs no project deps, so
# this runs on the ruff-only install above.
- name: Check generated settings types are fresh
if: needs.changes.outputs.backend == 'true'
run: python scripts/generate_config_types.py --check
93 changes: 93 additions & 0 deletions .github/workflows/detect-changes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Shared path detection for CI and Test Suite.
# Callers that gate required checks must still *run* those jobs and no-op when
# the matching output is false — skipped required checks block merges.
name: Detect path changes

on:
workflow_call:
outputs:
backend:
description: Backend-relevant paths changed (or forced full run)
value: ${{ jobs.detect.outputs.backend }}
frontend:
description: Frontend-relevant paths changed (or forced full run)
value: ${{ jobs.detect.outputs.frontend }}
e2e:
description: Paths that warrant E2E (or forced full run)
value: ${{ jobs.detect.outputs.e2e }}

jobs:
detect:
name: Detect changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
backend: ${{ steps.resolve.outputs.backend }}
frontend: ${{ steps.resolve.outputs.frontend }}
e2e: ${{ steps.resolve.outputs.e2e }}
steps:
- uses: actions/checkout@v7

# schedule / workflow_dispatch have no useful path diff — run everything.
- name: Force full run for schedule and manual dispatch
id: force
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
run: |
echo "backend=true" >> "$GITHUB_OUTPUT"
echo "frontend=true" >> "$GITHUB_OUTPUT"
echo "e2e=true" >> "$GITHUB_OUTPUT"

- name: Filter changed paths
id: filter
if: github.event_name == 'pull_request' || github.event_name == 'push'
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
backend:
- 'comicarr/**'
- 'tests/**'
- 'scripts/**'
- 'Comicarr.py'
- 'comictagger.py'
- 'pyproject.toml'
- 'uv.lock'
- 'alembic/**'
- 'alembic.ini'
- '.pre-commit-config.yaml'
- 'frontend/src/types/config.generated.ts'
- '.github/workflows/ci.yml'
- '.github/workflows/test.yml'
- '.github/workflows/detect-changes.yml'
frontend:
- 'frontend/**'
- '.github/workflows/ci.yml'
- '.github/workflows/test.yml'
- '.github/workflows/detect-changes.yml'
e2e:
- 'frontend/**'
- 'comicarr/**'
- 'Comicarr.py'
- 'pyproject.toml'
- 'uv.lock'
- 'alembic/**'
- 'alembic.ini'
- 'Dockerfile'
- 'docker/**'
- 'docker-compose.yml'
- '.github/workflows/test.yml'
- '.github/workflows/detect-changes.yml'

- name: Resolve outputs
id: resolve
run: |
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "backend=${{ steps.force.outputs.backend }}" >> "$GITHUB_OUTPUT"
echo "frontend=${{ steps.force.outputs.frontend }}" >> "$GITHUB_OUTPUT"
echo "e2e=${{ steps.force.outputs.e2e }}" >> "$GITHUB_OUTPUT"
else
echo "backend=${{ steps.filter.outputs.backend }}" >> "$GITHUB_OUTPUT"
echo "frontend=${{ steps.filter.outputs.frontend }}" >> "$GITHUB_OUTPUT"
echo "e2e=${{ steps.filter.outputs.e2e }}" >> "$GITHUB_OUTPUT"
fi
62 changes: 57 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ on:
workflow_dispatch:

jobs:
# ===========================================================================
# Path detection (shared with CI)
# ===========================================================================
changes:
name: Detect changes
uses: ./.github/workflows/detect-changes.yml

# ===========================================================================
# Backend Python Tests
# ===========================================================================
backend-tests:
name: Backend Tests (Python ${{ matrix.python-version }})
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -63,6 +72,8 @@ jobs:

migration-dialects:
name: Schema migrations (${{ matrix.dialect }})
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -137,6 +148,8 @@ jobs:
# ===========================================================================
frontend-tests:
name: Frontend Tests
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
defaults:
run:
Expand Down Expand Up @@ -177,11 +190,21 @@ jobs:

# ===========================================================================
# E2E Tests
# Runs when app surfaces change. Unit jobs that were skipped (other side of
# the monorepo) do not block; a failing unit job on a side that *did* change
# still blocks E2E.
# ===========================================================================
e2e-tests:
name: E2E Smoke Tests
runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests]
needs: [changes, backend-tests, frontend-tests]
if: >-
always()
&& !cancelled()
&& needs.changes.result == 'success'
&& needs.changes.outputs.e2e == 'true'
&& (needs.changes.outputs.backend != 'true' || needs.backend-tests.result == 'success')
&& (needs.changes.outputs.frontend != 'true' || needs.frontend-tests.result == 'success')

steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -251,8 +274,18 @@ jobs:
e2e-full-tests:
name: E2E Full Tests
runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests]
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
needs: [changes, backend-tests, frontend-tests]
if: >-
always()
&& !cancelled()
&& needs.changes.result == 'success'
&& (
github.event_name == 'schedule'
|| github.event_name == 'workflow_dispatch'
|| (github.event_name == 'push' && needs.changes.outputs.e2e == 'true')
)
&& (needs.changes.outputs.backend != 'true' || needs.backend-tests.result == 'success')
&& (needs.changes.outputs.frontend != 'true' || needs.frontend-tests.result == 'success')

steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -318,18 +351,37 @@ jobs:

# ===========================================================================
# Summary Job
# Skipped jobs (path-filtered) are treated as pass; only real failures fail CI.
# ===========================================================================
test-summary:
name: Test Summary
runs-on: ubuntu-latest
needs: [backend-tests, frontend-tests, e2e-tests, e2e-full-tests, migration-dialects]
needs: [changes, backend-tests, frontend-tests, e2e-tests, e2e-full-tests, migration-dialects]
if: always()

steps:
- name: Check test results
run: |
echo "changes=${{ needs.changes.result }}"
echo "backend-tests=${{ needs.backend-tests.result }}"
echo "frontend-tests=${{ needs.frontend-tests.result }}"
echo "e2e-tests=${{ needs.e2e-tests.result }}"
echo "e2e-full-tests=${{ needs.e2e-full-tests.result }}"
echo "migration-dialects=${{ needs.migration-dialects.result }}"

if [ "${{ needs.changes.result }}" == "failure" ] || [ "${{ needs.changes.result }}" == "cancelled" ]; then
echo "Path detection failed!"
exit 1
fi

if [ "${{ needs.backend-tests.result }}" == "failure" ] || [ "${{ needs.frontend-tests.result }}" == "failure" ] || [ "${{ needs.e2e-tests.result }}" == "failure" ] || [ "${{ needs.e2e-full-tests.result }}" == "failure" ] || [ "${{ needs.migration-dialects.result }}" == "failure" ]; then
echo "Some tests failed!"
exit 1
fi
echo "All tests passed!"

if [ "${{ needs.backend-tests.result }}" == "cancelled" ] || [ "${{ needs.frontend-tests.result }}" == "cancelled" ] || [ "${{ needs.e2e-tests.result }}" == "cancelled" ] || [ "${{ needs.e2e-full-tests.result }}" == "cancelled" ] || [ "${{ needs.migration-dialects.result }}" == "cancelled" ]; then
echo "Some tests were cancelled!"
exit 1
fi

echo "All required tests passed (skipped path-filtered jobs are OK)."