Skip to content

chore(actions)(deps): bump codecov/codecov-action from 5 to 6 in the github-actions group across 1 directory #149

chore(actions)(deps): bump codecov/codecov-action from 5 to 6 in the github-actions group across 1 directory

chore(actions)(deps): bump codecov/codecov-action from 5 to 6 in the github-actions group across 1 directory #149

Workflow file for this run

# CI/CD Pipeline — bankstatementprocessor monorepo
#
# Jobs (all lint jobs run in parallel):
# changes — detect which paths changed (skips heavy jobs on workflow-only PRs)
# lint-core — black, isort, ruff, mypy on packages/parser-core
# lint-free — black, isort, ruff on packages/parser-free
# security — bandit + pip-audit + xenon complexity gate
# test-core — pytest with 91% coverage gate (Python matrix), needs lint-core
# test-free — pytest on packages/parser-free, needs lint-free
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- '.gitignore'
- '*.txt'
- '**.png'
- '**.jpg'
- '**.jpeg'
- '**.gif'
- '**.svg'
pull_request:
branches: [ main, develop ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- '.gitignore'
- '**.png'
- '**.jpg'
- '**.jpeg'
- '**.gif'
- '**.svg'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
security-events: write
actions: read
# ---------------------------------------------------------------------------
# Jobs
# ---------------------------------------------------------------------------
jobs:
# Detect which paths changed so downstream jobs can skip when irrelevant
changes:
name: Detect changed paths
runs-on: ubuntu-latest
permissions:
pull-requests: read
contents: read
outputs:
core: ${{ steps.filter.outputs.core }}
free: ${{ steps.filter.outputs.free }}
any-src: ${{ steps.filter.outputs.any-src }}
docker: ${{ steps.filter.outputs.docker }}
workflows: ${{ steps.filter.outputs.workflows }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
core:
- 'packages/parser-core/**'
free:
- 'packages/parser-free/**'
any-src:
- 'packages/**'
docker:
- 'Dockerfile'
- 'entrypoint.sh'
workflows:
- '.github/workflows/**'
- '.github/labeler*.yml'
lint-core:
name: Lint — parser-core
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: needs.changes.outputs.core == 'true'
defaults:
run:
working-directory: packages/parser-core
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: pip
cache-dependency-path: packages/parser-core/pyproject.toml
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends poppler-utils
- name: Install dev + test dependencies
run: pip install --upgrade pip && pip install -e ".[dev,test]"
- name: Black
run: black --check --diff src tests
- name: isort
run: isort --check-only --diff src tests
- name: Ruff
run: ruff check src tests
- name: MyPy
run: mypy src --ignore-missing-imports
- name: Validate type stubs
run: |
mypy --check-untyped-defs stubs/pdfplumber/
stubtest pdfplumber --allowlist stubtest-allowlist.txt || echo "stubtest differences found — review allowlist"
- name: Validate version consistency
run: |
CODE_VER=$(grep '__version__ = ' src/bankstatements_core/__version__.py | cut -d'"' -f2)
TOML_VER=$(grep '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)
echo "Code: ${CODE_VER} TOML: ${TOML_VER}"
[ "${CODE_VER}" = "${TOML_VER}" ] || { echo "Version mismatch"; exit 1; }
lint-free:
name: Lint — parser-free
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: needs.changes.outputs.free == 'true'
defaults:
run:
working-directory: packages/parser-free
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: pip
cache-dependency-path: packages/parser-free/pyproject.toml
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends poppler-utils
- name: Install parser-core (editable) + parser-free + lint tools
run: |
pip install --upgrade pip
pip install -e ../parser-core
pip install -e ".[test]"
pip install black isort ruff
- name: Black
run: black --check --diff src tests
- name: isort
run: isort --check-only --diff src tests
- name: Ruff
run: ruff check src tests
security:
name: Security — bandit + pip-audit
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: needs.changes.outputs.any-src == 'true'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends poppler-utils
- name: Install security tools + packages
run: |
pip install --upgrade pip bandit[toml] pip-audit
pip install -e packages/parser-core
pip install -e packages/parser-free
- name: Bandit — parser-core
run: bandit -r packages/parser-core/src -f json -o bandit-core.json || true
- name: Bandit — parser-free
run: bandit -r packages/parser-free/src -f json -o bandit-free.json || true
- name: pip-audit — dependency vulnerability scan
run: pip-audit -r requirements/base.txt --progress-spinner off -f json -o pip-audit-report.json
- name: Complexity gate (Xenon)
run: |
# table_detector.py: _detect_text_based_table grade E (score 32) — PDF heuristic hotspot,
# pdfplumber word-coordinate coupling blocks safe decomposition without characterisation tests.
# Temporary exclusion — remove once refactored. Tracked: https://github.com/longieirl/bankstatementprocessor/issues/98
# template_detector.py: get_detection_explanation grade D (score 22) — diagnostic method;
# exhaustive path coverage of all scoring outcomes is inherent to its purpose. Stable exemption.
# commands/init.py: init_directories grade D (score 26) — CLI entrypoint orchestration;
# complexity is setup sequencing (mkdir + sample file creation), not extractable decision logic. Stable exemption.
xenon --max-average A --max-modules B --max-absolute C \
--exclude "packages/parser-core/src/bankstatements_core/analysis/table_detector.py,packages/parser-core/src/bankstatements_core/templates/template_detector.py,packages/parser-core/src/bankstatements_core/commands/init.py" \
packages/parser-core/src
- name: Upload security reports
uses: actions/upload-artifact@v7
if: always()
with:
name: security-reports
path: |
bandit-core.json
bandit-free.json
pip-audit-report.json
# ---------------------------------------------------------------------------
# Test jobs (serial after their respective lint job)
# ---------------------------------------------------------------------------
test-core:
name: Test parser-core (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 30
needs: lint-core
defaults:
run:
working-directory: packages/parser-core
strategy:
fail-fast: false
matrix:
# PRs: fast feedback on 3.12 only; pushes to main: full matrix
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.12"]') || fromJSON('["3.11", "3.12", "3.13"]') }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: packages/parser-core/pyproject.toml
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends poppler-utils
- name: Install package + test deps
run: pip install --upgrade pip && pip install -e ".[dev,test]"
- name: Run tests with coverage
run: |
python -m pytest tests/ -v \
--cov=bankstatements_core \
--cov-report=term-missing \
--cov-report=xml \
--cov-report=html \
--cov-fail-under=91 \
-n auto \
--tb=short
- name: Upload test results
uses: actions/upload-artifact@v7
if: always()
with:
name: test-results-core-${{ matrix.python-version }}
path: |
packages/parser-core/coverage.xml
packages/parser-core/htmlcov/
- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v6
with:
files: packages/parser-core/coverage.xml
flags: parser-core
fail_ci_if_error: false
test-free:
name: Test parser-free
runs-on: ubuntu-latest
timeout-minutes: 15
needs: lint-free
defaults:
run:
working-directory: packages/parser-free
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: pip
cache-dependency-path: packages/parser-free/pyproject.toml
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends poppler-utils
- name: Install parser-core (editable) + parser-free
run: |
pip install --upgrade pip
pip install -e ../parser-core
pip install -e ".[test]"
- name: Run tests
run: python -m pytest tests/ -v --tb=short
# ---------------------------------------------------------------------------
# Docker build validation — ensures Dockerfile builds successfully on PRs.
# Runs on pull_request only (no push to avoid duplicate builds with release.yml).
# ---------------------------------------------------------------------------
build-docker:
name: Build Docker image (PR validation)
runs-on: ubuntu-latest
timeout-minutes: 15
needs: changes
if: needs.changes.outputs.docker == 'true' && github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- name: Build image (no push)
uses: docker/build-push-action@v7
with:
context: .
target: production
push: false
load: true
tags: bankstatementsprocessor:pr-${{ github.event.pull_request.number }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Smoke-test image
run: |
docker run --rm \
bankstatementsprocessor:pr-${{ github.event.pull_request.number }} \
python -c "import bankstatements_free; import bankstatements_core; print('imports OK')"
# ---------------------------------------------------------------------------
# Workflow lint — validates GitHub Actions YAML syntax on PRs.
# ---------------------------------------------------------------------------
workflow-lint:
name: Lint — workflow files
runs-on: ubuntu-latest
timeout-minutes: 5
needs: changes
if: needs.changes.outputs.workflows == 'true'
steps:
- uses: actions/checkout@v6
- name: Run actionlint
uses: rhysd/actionlint@v1.7.11
# ---------------------------------------------------------------------------
# Downstream dispatch — notify private repo when parser-core changes on main
# Fires only on push to main (not PRs), only when parser-core files changed,
# and only after test-core passes.
#
# To skip the downstream trigger (e.g. formatting fixes, doc updates):
# add [skip downstream] anywhere in the commit message.
#
# Security:
# - Gated on push to main + test-core success — PRs never trigger this
# - DOWNSTREAM_DISPATCH_TOKEN must be a fine-grained PAT scoped to
# longieirl/bankstatements only, with Contents: write permission
# - The private repo validates the run_url payload origin before running
# ---------------------------------------------------------------------------
dispatch-downstream:
name: Dispatch downstream CI (bankstatements)
runs-on: ubuntu-latest
needs: test-core
if: |
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
!contains(github.event.head_commit.message, '[skip downstream]')
steps:
- uses: actions/checkout@v6
- name: Check if parser-core changed
id: changes
uses: dorny/paths-filter@v4
with:
filters: |
core:
- 'packages/parser-core/**'
- name: Trigger downstream CI in bankstatements
if: steps.changes.outputs.core == 'true'
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.DOWNSTREAM_DISPATCH_TOKEN }}
repository: longieirl/bankstatements
event-type: core-updated
client-payload: |
{
"sha": "${{ github.sha }}",
"ref": "${{ github.ref }}",
"run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
# ---------------------------------------------------------------------------
# Summary gate — required status check for branch protection
# Always runs so docs-only PRs (which skip the workflow) are handled by
# ci-docs.yml instead. When code jobs run, this aggregates their result.
# ---------------------------------------------------------------------------
ci-pass:
name: CI Pass
runs-on: ubuntu-latest
if: always()
needs: [changes, lint-core, lint-free, security, test-core, test-free, build-docker, workflow-lint]
steps:
- name: Check all jobs passed
run: |
results="${{ join(needs.*.result, ' ') }}"
for r in $results; do
if [ "$r" != "success" ] && [ "$r" != "skipped" ]; then
echo "Job failed with result: $r"
exit 1
fi
done
echo "All jobs passed or skipped."