Repository navigation
191 lines (177 loc) · 6.78 KB
/
Copy pathpython-backend.yml
File metadata and controls
191 lines (177 loc) · 6.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# =============================================================================
# Reusable: verify a Python backend.
# =============================================================================
# Lint, type-check, test and audit dependencies. Nothing here touches a secret,
# a registry or a server, so it is safe to run on pull requests from anywhere.
#
# uses: CtrlAltDevelop/ci-workflows/.github/workflows/python-backend.yml@v1
#
on:
workflow_call:
inputs:
working-directory:
description: "Directory holding manage.py / pyproject.toml"
type: string
default: "."
python-version:
type: string
default: "3.14"
requirements:
description: "Requirements file, relative to working-directory"
type: string
default: "requirements.txt"
postgres:
description: "Start a Postgres service container"
type: boolean
default: true
postgres-image:
type: string
default: "pgvector/pgvector:pg16"
redis:
type: boolean
default: true
mypy:
description: "Fail the run on type errors (off until a project is clean)"
type: boolean
default: false
lint-blocking:
description: |
Whether a lint finding fails the run. Turn it off on a project whose
lint backlog predates the pipeline: a gate nobody can pass is not a
gate, and leaving it red teaches everyone to ignore red. The findings
are still printed, and the job summary carries the count.
type: boolean
default: true
test-requirements:
description: |
Space-separated packages the suite needs on top of `requirements`.
A runtime requirements file rarely carries pytest, and pointing this
at a project's dev file would drag in its formatters and shells too.
type: string
default: "pytest pytest-django"
test-command:
type: string
default: "pytest -q --tb=short"
extra-env:
description: "Newline-separated KEY=VALUE pairs for the test step"
type: string
default: ""
audit-ignore:
description: |
Space-separated advisory IDs pip-audit should not fail on — for a
finding in a transitive dependency with no published fix, where the
only alternatives are to drop the parent package or to stop auditing
entirely. Each ID is a decision to ship a known vulnerability, so say
which package and why in a comment beside it, and take it out again
when a fix lands.
type: string
default: ""
# Read the code, nothing more. Every job below inherits this.
permissions: {}
jobs:
verify:
name: Verify
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
# An `image:` that evaluates to an empty string skips the service. If a
# future runner rejects that, split this into two jobs instead of adding a
# matrix — the inputs stay the same either way.
services:
postgres:
image: ${{ inputs.postgres && inputs.postgres-image || '' }}
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready --health-interval 10s
--health-timeout 5s --health-retries 5
redis:
image: ${{ inputs.redis && 'redis:7-alpine' || '' }}
ports: ["6379:6379"]
options: >-
--health-cmd "redis-cli ping" --health-interval 10s
--health-timeout 5s --health-retries 5
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: ${{ inputs.python-version }}
cache: pip
- name: Install
run: |
python -m pip install --upgrade pip
pip install -r "${{ inputs.requirements }}"
pip install ruff mypy pip-audit ${{ inputs.test-requirements }}
# The exit code is caught here rather than left to continue-on-error,
# which lets the step fail and then forgives it — GitHub still files the
# failure as an annotation, so a green run wears two red marks that mean
# nothing. The findings go to the summary instead.
- name: Lint
id: lint
env:
BLOCKING: ${{ inputs.lint-blocking }}
run: |
if ruff check . --output-format concise; then
echo "clean=true" >> "$GITHUB_OUTPUT"
else
echo "clean=false" >> "$GITHUB_OUTPUT"
[ "$BLOCKING" != "true" ] || exit 1
fi
- name: Note the lint backlog
if: steps.lint.outputs.clean == 'false'
run: |
{
echo "### Lint is not blocking on this project"
echo ""
echo '```'
ruff check . --statistics | head -20
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Format check
run: |
ruff format --check . ||
echo "::notice::Formatting differs from ruff format. Not blocking."
- name: Type check
if: inputs.mypy
run: mypy .
# Known-vulnerable dependencies fail the build here rather than being
# found later by whoever is scanning the running server.
- name: Audit dependencies
env:
IGNORE: ${{ inputs.audit-ignore }}
run: |
args=""
for id in $IGNORE; do
args="$args --ignore-vuln $id"
echo "::warning::Ignoring $id — see the caller for why"
done
pip-audit -r "${{ inputs.requirements }}" --strict $args
- name: Collect extra environment
if: inputs.extra-env != ''
run: printf '%s\n' "${{ inputs.extra-env }}" >> "$GITHUB_ENV"
- name: Test
run: ${{ inputs.test-command }}
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
POSTGRES_HOST: localhost
DATABASE_URL: postgres://test_user:test_pass@localhost:5432/test_db
REDIS_URL: redis://localhost:6379/0
# Deliberately a throwaway. A real key must never reach a PR runner.
DJANGO_SECRET_KEY: ci-only-not-a-real-key
SECRET_KEY: ci-only-not-a-real-key
# DJANGO_DEBUG is deliberately not set here. Whether a suite runs with
# DEBUG on is the project's decision, made in its test settings, and a
# value forced from the harness overrides the `setdefault` such a
# module uses — which is how 242 tests came to be answered with a
# 301 to https, from SECURE_SSL_REDIRECT switching itself on.