Merge pull request #62 from Sea-Haven-Industries/feature/ci-python-app-reusable
Some checks are pending
ci / ci / ci (push) Waiting to run

Add ci-python-app reusable workflow
This commit is contained in:
Adam Moussa 2026-06-17 17:28:00 -04:00 • committed by GitHub
commit 347558820d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

150
.github/workflows/ci-python-app.yaml vendored Normal file
View file

@ -0,0 +1,150 @@
name: CI — Python (app)
# Reusable CI for plain Python apps / locally-run tooling that do NOT deploy via
# SAM or CDK (use ci-python-sam.yaml / ci-typescript-cdk.yaml for those). Beyond
# lint + format it adds two things such repos commonly need:
# * a collect-only import check for a root suite whose live run needs secrets
# (verifies every test module imports cleanly without running them), and
# * an isolated full pytest run for a self-contained subproject dir whose tests
# package collides with the root tests/ package (e.g. a `tests/` under a
# subdir) and so must run in its own working directory.
#
# Naming is load-bearing (see this repo's ci.yaml): the org ruleset matches the
# required `ci / ci` check against the JOB check-run name. A caller job keyed `ci`
# invoking this workflow reports each job here as `ci / <job>`, so the aggregator
# job below is keyed `ci` to emit exactly `ci / ci`. The aggregator gates on every
# other job, so the single required check fails if any sub-job fails.
on:
workflow_call:
inputs:
python-version:
description: "Python version to use"
type: string
default: "3.12"
source-dirs:
description: "Space-separated directories for ruff (default: repo root)"
type: string
default: "."
requirements:
description: "Requirements file used for the pip cache key + install"
type: string
default: "requirements.txt"
collect-only:
description: "Run 'pytest --collect-only' at the repo root (imports resolve without secrets)"
type: boolean
default: true
subproject-dir:
description: "Optional self-contained subproject dir whose pytest suite runs in full"
type: string
default: ""
run-conventions-check:
description: "Run the lightweight conventions audit (README + .gitignore covers .env)"
type: boolean
default: true
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ inputs.python-version }}
- name: Install ruff
run: pip install ruff
- name: Ruff check
run: ruff check ${{ inputs.source-dirs }}
- name: Ruff format check
run: ruff format --check ${{ inputs.source-dirs }}
- name: Conventions check
if: ${{ inputs.run-conventions-check }}
run: |
errors=0
fail() { echo "::error::$1"; errors=$((errors + 1)); }
# README must exist
if [[ ! -f README.md ]]; then
fail "Missing README.md"
fi
# .gitignore must cover .env
if [[ -f .gitignore ]]; then
if ! grep -qE '^\.env$|^\.env\b' .gitignore; then
fail ".gitignore does not include .env"
fi
else
fail "Missing .gitignore"
fi
if [[ $errors -gt 0 ]]; then
echo "Conventions check failed with $errors error(s)."
exit 1
fi
echo "Conventions check passed."
test-collect:
if: ${{ inputs.collect-only }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ inputs.python-version }}
cache: pip
cache-dependency-path: ${{ inputs.requirements }}
- name: Install dependencies
run: |
pip install -r "${{ inputs.requirements }}"
pip install pytest python-dotenv
- name: Pytest collect-only
run: pytest --collect-only -q
subproject-tests:
if: ${{ inputs.subproject-dir != '' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ inputs.python-version }}
cache: pip
cache-dependency-path: ${{ inputs.requirements }}
- name: Install dependencies
run: |
pip install -r "${{ inputs.requirements }}"
pip install pytest
- name: Run subproject suite
working-directory: ${{ inputs.subproject-dir }}
run: python -m pytest -q
ci:
# Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`.
needs: [lint, test-collect, subproject-tests]
if: always()
runs-on: ubuntu-latest
steps:
- name: Require all jobs to have succeeded
run: |
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
echo "A required CI job failed or was cancelled."
exit 1
fi
echo "All CI jobs passed."