mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 05:53:12 +00:00
Add ci-python-app reusable workflow
Reusable CI for plain Python apps / locally-run tooling that don't deploy via SAM or CDK. Beyond ruff lint/format + the conventions audit, it adds a collect-only import check for a root suite whose live run needs secrets, and an isolated full pytest run for a self-contained subproject dir (whose tests/ package would collide with the root tests/ under one rootdir). Emits the org-required `ci / ci` via an aggregator job keyed `ci` that gates on every other job. actionlint-clean.
This commit is contained in:
parent
0442339fff
commit
c36b737af7
1 changed files with 150 additions and 0 deletions
150
.github/workflows/ci-python-app.yaml
vendored
Normal file
150
.github/workflows/ci-python-app.yaml
vendored
Normal 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."
|
||||
Loading…
Add table
Reference in a new issue