Skip to content

feat(nav): "Drucker" Link für Admin-Drucker-Verwaltung + getrennte Ac… #338

feat(nav): "Drucker" Link für Admin-Drucker-Verwaltung + getrennte Ac…

feat(nav): "Drucker" Link für Admin-Drucker-Verwaltung + getrennte Ac… #338

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
python:
name: Python — lint, type, test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Detect Python project
id: pyproject
run: |
if [ -f backend/pyproject.toml ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "path=backend" >> "$GITHUB_OUTPUT"
elif [ -f pyproject.toml ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "path=." >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Python (with cache)
if: steps.pyproject.outputs.exists == 'true'
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: ${{ steps.pyproject.outputs.path }}/pyproject.toml
- name: Set up Python (without cache, no pyproject.toml yet)
if: steps.pyproject.outputs.exists != 'true'
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install dependencies
if: steps.pyproject.outputs.exists == 'true'
working-directory: ${{ steps.pyproject.outputs.path }}
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Ruff lint
if: steps.pyproject.outputs.exists == 'true'
working-directory: ${{ steps.pyproject.outputs.path }}
run: ruff check .
- name: Ruff format check
if: steps.pyproject.outputs.exists == 'true'
working-directory: ${{ steps.pyproject.outputs.path }}
run: ruff format --check .
# Mypy is a HARD CI gate — strict mode is configured in pyproject.toml
# ([tool.mypy] strict = true). We do not use `|| true` here; type-check
# regressions must fail CI, not be merged silently. See CONTRIBUTING.md.
- name: Mypy (strict, blocking)
if: steps.pyproject.outputs.exists == 'true'
working-directory: ${{ steps.pyproject.outputs.path }}
run: mypy app/
# --cov-fail-under is a HARD CI gate. The 80% floor mirrors
# [tool.coverage.report].fail_under in pyproject.toml — declaring it
# again on the command line makes the failure surface in the test step
# itself (with the exact `TOTAL` line) instead of in a separate
# `coverage report` step that pytest-cov would silently swallow.
#
# -m "not slow" excludes wall-clock SSE timing tests (marked @pytest.mark.slow
# in tests/integration/test_sse_flush.py). These tests use real asyncio
# sleep timers and are too slow for every-commit CI. Run them separately
# via: pytest -m slow tests/integration/test_sse_flush.py
- name: Tests
if: steps.pyproject.outputs.exists == 'true'
working-directory: ${{ steps.pyproject.outputs.path }}
run: pytest -m "not slow" --cov=app --cov-report=xml --cov-fail-under=80 --junitxml=junit.xml -o junit_family=legacy -v
- name: Alembic — no pending migrations
if: steps.pyproject.outputs.exists == 'true'
working-directory: ${{ steps.pyproject.outputs.path }}
run: |
# alembic check compares SQLModel.metadata against the latest migration
# head, but requires a DB at head first. Upgrade-then-check on a clean
# data/ directory keeps CI deterministic.
mkdir -p data
alembic upgrade head
alembic check
- name: Upload coverage to Codecov
if: always() && hashFiles('backend/coverage.xml', 'coverage.xml') != ''
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: backend/coverage.xml,coverage.xml
fail_ci_if_error: false
flags: backend
- name: Upload test results to Codecov
if: ${{ !cancelled() && hashFiles('backend/junit.xml', 'junit.xml') != '' }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: backend/junit.xml,junit.xml
- name: No-op (no Python project yet — skipped)
if: steps.pyproject.outputs.exists != 'true'
run: echo "No pyproject.toml found — skipping Python checks for this branch"
go:
name: Go — vet, test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Check for Go module
id: go-check
run: |
if [ -f frontend/go.mod ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Go
if: steps.go-check.outputs.exists == 'true'
uses: actions/setup-go@v6
with:
go-version: stable
cache-dependency-path: frontend/go.sum
- name: go vet
if: steps.go-check.outputs.exists == 'true'
working-directory: frontend
run: go vet ./...
- name: Tests
if: steps.go-check.outputs.exists == 'true'
working-directory: frontend
run: go test -race -coverprofile=coverage.out ./...
oapi-codegen-drift:
name: Check oapi-codegen output is up to date
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Check for Go module
id: go-check
run: |
if [ -f frontend/go.mod ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Go
if: steps.go-check.outputs.exists == 'true'
uses: actions/setup-go@v6
with:
go-version: stable
cache-dependency-path: frontend/go.sum
- name: Regenerate client
if: steps.go-check.outputs.exists == 'true'
working-directory: frontend
run: make gen-client
- name: Fail if generated file differs
if: steps.go-check.outputs.exists == 'true'
run: git diff --exit-code frontend/internal/api/client.gen.go
- name: No-op (no Go module yet)
if: steps.go-check.outputs.exists != 'true'
run: echo "No frontend/go.mod found — skipping oapi-codegen drift check"
docker-build:
name: Docker — build smoke test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Check for Dockerfile
id: docker-check
run: |
if [ -f Dockerfile ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Build (no push)
if: steps.docker-check.outputs.exists == 'true'
uses: docker/build-push-action@v7
with:
context: .
push: false
tags: label-printer-hub:ci
cache-from: type=gha
cache-to: type=gha,mode=max
privacy-scan:
name: Privacy / secret scan
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Scan for private network artefacts
run: |
set +e
# IP-like patterns use PCRE negative lookbehind/lookahead so that
# they only match real IPv4 addresses, not SNMP OIDs (1.3.6.1.…)
# or version strings.
ip_patterns=(
'(?<![0-9.])172\.16\.[0-9]+\.[0-9]+(?![0-9.])'
'(?<![0-9.])192\.168\.[0-9]+\.[0-9]+(?![0-9.])'
'(?<![0-9.])10\.[0-9]+\.[0-9]+\.[0-9]+(?![0-9.])'
'(?<![0-9.])100\.(?:6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.[0-9]+\.[0-9]+(?![0-9.])'
)
# String patterns use ERE (no boundary tricks needed)
str_patterns=(
'hhdocker'
'hhplex'
'strausmann\.cloud'
'strausmann\.de'
'strausmann\.net'
'lager\.strausmann'
)
# Files exempted because they DOCUMENT the policy (the patterns are
# listed there as "don't do this" examples). All other markdown is
# scanned — README, examples, wiki-mirrored content, ADRs, etc.
exempt=(
':!docs/policies/privacy.md'
':!.gemini/styleguide.md'
':!.github/copilot-instructions.md'
':!.github/workflows/ci.yml'
)
fail=0
for p in "${ip_patterns[@]}"; do
hits=$(git grep -nIP "$p" -- "${exempt[@]}" || true)
if [ -n "$hits" ]; then
echo "::error title=Private IP detected::Pattern '$p' matched:"
echo "$hits"
fail=1
fi
done
for p in "${str_patterns[@]}"; do
hits=$(git grep -nIE "$p" -- "${exempt[@]}" || true)
if [ -n "$hits" ]; then
echo "::error title=Private artefact detected::Pattern '$p' matched:"
echo "$hits"
fail=1
fi
done
exit $fail