Repository navigation
238 lines (216 loc) · 9.12 KB
/
Copy pathpython-tests.yml
File metadata and controls
238 lines (216 loc) · 9.12 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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# =============================================================================
# python-tests — quality gates for the nexus_deploy package
# =============================================================================
# Runs on every push and PR. Three jobs:
# - lint: ruff format check + ruff lint + shellcheck (fast, ~30s)
# - typecheck: mypy --strict (~1-2 min, depends on cache hit)
# - test: pytest unit + integration with coverage gate (≥80%)
#
# Tests run only when the files they actually read change — to keep CI
# cheap during pure documentation PRs.
#
# That includes `services.yaml` and `stacks/**`, not just Python.
# `test_stack_conventions.py` reads both and is where support_images
# uniqueness, version pinning and compose/services.yaml agreement are
# enforced. Two mutations confirm it, rather than the path list being
# argued from the file names:
#
# services.yaml: postgres:14-alpine -> postgres:latest
# -> test_support_images_are_pinned[infisical] and
# test_compose_fallbacks_match_the_declared_version[infisical]
# stacks/infisical/docker-compose.yml: fallback 14 -> 15
# -> test_compose_fallbacks_match_the_declared_version[infisical]
#
# Before this, neither PR ran a single test: a stack-only change matched
# no path, so the assertions written for exactly that change fired only
# when a PR happened to touch Python too.
#
# Docs stay excluded — no test reads them.
# =============================================================================
name: "CI: Python Tests"
on:
pull_request:
paths:
- "src/**"
- "tests/**"
- "services.yaml"
- "stacks/**"
- "pyproject.toml"
- "uv.lock"
- ".python-version"
- ".github/workflows/python-tests.yml"
- "scripts/*.sh"
push:
branches: [main]
paths:
- "src/**"
- "tests/**"
- "services.yaml"
- "stacks/**"
- "pyproject.toml"
- "uv.lock"
- ".python-version"
- ".github/workflows/python-tests.yml"
- "scripts/*.sh"
# Cancel previous runs of this workflow on the same ref. The CI is fast
# but no point burning minutes on a force-push that obsoletes the prior
# attempt.
concurrency:
group: python-tests-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# ---------------------------------------------------------------------------
# Lint — ruff (Python) + shellcheck (bash helpers)
# ---------------------------------------------------------------------------
lint:
name: Lint (ruff + shellcheck)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.11.7"
enable-cache: true
- name: Sync dev deps
run: uv sync --frozen --group dev
- name: ruff format --check
run: uv run ruff format --check .
- name: ruff lint
run: uv run ruff check .
- name: shellcheck remaining bash
run: |
# Lints the bash helper scripts in scripts/ — operator
# one-shots (init-r2-state, cleanup-orphaned-resources,
# setup-control-plane-secrets, check-control-plane-env, and
# the smaller capture/check/parse helpers). The main deploy
# path is Python (python -m nexus_deploy run-pipeline) since
# Phase 4c (#535).
# `find -print0 | xargs -0` is portable to macOS bash 3.2 too.
# `--severity=error` only blocks on real bugs; the helpers
# carry a few style/info nits we tolerate.
find scripts -maxdepth 2 -name '*.sh' -type f -print0 \
| xargs -0 uv run shellcheck --severity=error --exclude=SC1091,SC2001
# ---------------------------------------------------------------------------
# Typecheck — mypy --strict
# ---------------------------------------------------------------------------
typecheck:
name: Typecheck (mypy --strict)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.11.7"
enable-cache: true
- name: Sync deps (incl. type stubs)
run: uv sync --frozen --group dev
- name: mypy strict
run: uv run mypy
# ---------------------------------------------------------------------------
# Test — pytest unit + integration, coverage gate ≥80%
# ---------------------------------------------------------------------------
test:
name: Test (pytest + coverage)
runs-on: ubuntu-latest
timeout-minutes: 15
# Job-scoped permissions — only the coverage-comment step needs write
# access to PRs, and we don't want it bleeding into other jobs.
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.11.7"
enable-cache: true
- name: Sync deps
run: uv sync --frozen --group dev
# tests/unit/test_secret_sync_collisions.py runs the rendered
# secret-sync script for real, and that script uses jq to turn the
# Infisical response into TSV. jq cannot be shimmed the way curl and
# docker are without reimplementing it.
#
# Installed rather than assumed present: five workflows in this repo
# use jq without installing it and one installs it explicitly, so its
# availability on the runner image is a habit rather than a
# guarantee. Those tests skip when jq is missing, which on a
# developer machine is the right outcome and in CI would mean a green
# run that never exercised what the suite claims to pin.
- name: Install jq (needed by the secret-sync script tests)
run: sudo apt-get update && sudo apt-get install -y jq
- name: pytest unit
# Runs the full test suite under pyproject.toml's testpaths (today
# only tests/unit/; an `integration` marker is registered for
# future testcontainers-based tests, but none exist yet, so this
# step is effectively unit-only at present).
# `--cov-fail-under=80` makes the gate explicit on the CLI;
# pyproject.toml's [tool.coverage.report] also has fail_under=80
# so local + CI agree even if someone runs `coverage report` directly.
# `--cov-report=xml` feeds the PR coverage-comment step, `=html`
# feeds the artifact upload, and `tee` keeps the step's stdout
# readable.
#
# `shell: bash` is REQUIRED here, not cosmetic. Without it Actions
# runs `bash -e {0}` — which has NO pipefail — so `pytest | tee`
# exits with tee's status and a failing suite reports success.
# Specifying `shell: bash` selects
# `bash --noprofile --norc -eo pipefail {0}` instead, so pytest's
# exit code propagates. This masked 3 genuinely failing snapshot
# tests while the job stayed green.
shell: bash
run: |
uv run pytest \
--cov=src/nexus_deploy --cov-fail-under=80 \
--cov-report=term-missing --cov-report=xml --cov-report=html \
| tee pytest-coverage.txt
- name: Coverage comment on PR
# Posts/updates a single markdown comment per PR with total
# coverage, badge, and per-file changed-lines breakdown. No
# external service, no token — just `pull-requests: write`
# scoped above. Skipped on push-to-main runs (no PR to comment on).
if: github.event_name == 'pull_request'
uses: MishaKav/pytest-coverage-comment@v1.1.54
with:
pytest-coverage-path: ./pytest-coverage.txt
pytest-xml-coverage-path: ./coverage.xml
title: "Coverage report — `nexus_deploy`"
badge-title: coverage
hide-badge: false
hide-report: false
create-new-comment: false
report-only-changed-files: false
- name: Upload coverage to Codecov
# External coverage trend graphs + auto-updating README badge.
# Public repos technically don't require a token, but Codecov has
# been progressively rolling out token-required uploads since
# 2023 to deal with token-free upload abuse — providing the
# CODECOV_TOKEN secret avoids the intermittent rate-limit /
# rejection. `fail_ci_if_error: false` makes the step
# non-blocking: if Codecov is down, the gate-relevant local
# `--cov-fail-under=80` step has already passed and CI stays
# green. The MishaKav comment step above is independent of
# Codecov (local-only PR comment); both run.
if: always()
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: unittests
name: nexus_deploy
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: htmlcov/
retention-days: 7
if-no-files-found: ignore