mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-10-03 02:13:26 +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