Repository navigation
476 lines (425 loc) · 20 KB
/
Copy pathtests.yml
File metadata and controls
476 lines (425 loc) · 20 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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
name: Tests
# Zmiany czysto dokumentacyjne (docs/, mkdocs.yml, .readthedocs.yaml)
# NIE uruchamiaja calej suity testow — buduje je wtedy tylko workflow
# `Docs` (ma komplementarny `paths:`). Dzieki temu commit z sama
# dokumentacja deployuje MkDocs na Pages, ale nie odpala ~10-min testow.
# UWAGA: `[skip ci]` w komunikacie commita ubilby ROWNIEZ `Docs` —
# dlatego selekcja jest po sciezkach, nie po komunikacie. Commit
# mieszany (dokumentacja + kod) i tak odpali testy, bo nie wszystkie
# zmienione pliki sa ignorowane.
on:
push:
branches:
- dev
- master
paths-ignore:
- docs/**
- mkdocs.yml
- .readthedocs.yaml
pull_request:
branches:
- dev
paths-ignore:
- docs/**
- mkdocs.yml
- .readthedocs.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# Default minimum permissions; per-job overrides below where extra
# scopes are needed (notably `packages:` for the GHCR push/pull flow).
permissions:
contents: read
jobs:
lint:
# Lint only Python files changed in this push/PR by running the
# pre-commit ruff hooks THEMSELVES (see .pre-commit-config.yaml)
# against the changed range. The ruff version + args live in one
# place, so local pre-commit and CI can never disagree. The repo's
# convention is changed-files-only; pre-commit's --from-ref/--to-ref
# reproduces that scope and historic drift is left alone.
name: Lint changed files
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
# No apt build deps / project sync here: pre-commit builds an
# isolated ruff env from a prebuilt wheel (no native compilation),
# so the heavy toolchain the test jobs need is unnecessary for
# linting. uv is only a fast launcher for pre-commit (via uvx).
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
# Patrz komentarz przy jobie `lock-check` — bez `version` setup-uv
# bierze "latest", co czyni CI niedeterministycznym.
version: "0.13.5"
enable-cache: true
- name: Determine diff base
id: base
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
BEFORE_SHA: ${{ github.event.before }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
git fetch --no-tags --depth=100 origin "$BASE_REF"
echo "ref=origin/$BASE_REF" >> "$GITHUB_OUTPUT"
elif [ -n "$BEFORE_SHA" ] && \
[ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
echo "ref=$BEFORE_SHA" >> "$GITHUB_OUTPUT"
else
# First push to a new branch: diff against dev
git fetch --no-tags --depth=100 origin dev
echo "ref=origin/dev" >> "$GITHUB_OUTPUT"
fi
- name: Assert ruff pins agree (pyproject vs .pre-commit-config)
# Krok POPRZEDZA lint, bo ustala CZYM lintujemy. Ruff ma dwa piny:
# `ruff==X` w extrze [dev] (pyproject.toml) i `rev: vX` przy
# astral-sh/ruff-pre-commit (.pre-commit-config.yaml). Krok niżej
# używa tego drugiego, `uv run ruff` u dewelopera — pierwszego.
# To dwa różne binaria, więc rozjazd pinów przywraca dokładnie ten
# tryb awarii, który krok niżej deklaruje jako niemożliwy ("one
# ruff, one config"): patrz komentarz przy rev w pre-commit config
# o układzie I001 przepuszczonym lokalnie i odrzuconym przez CI.
#
# Osobny krok, a nie hook w tamtym wywołaniu, bo krok niżej odpala
# hooki PO NAZWIE (`pre-commit run ruff`), więc lokalny hook
# `ruff-piny-zsynchronizowane` nigdy by się w CI nie uruchomił.
# Skrypt jest stdlib-only → goły `python3`, bez `uv sync`.
run: python3 bin/check-ruff-pin-sync.py
- name: Ruff + ruff-format (via pre-commit — single source of truth)
# Runs the SAME `ruff` and `ruff-format` hooks declared in
# .pre-commit-config.yaml (pinned rev) over the changed range, so
# CI and a local `pre-commit run` execute the byte-identical ruff
# binary + args (--fix --exit-non-zero-on-fix). "passes locally,
# fails CI" becomes impossible — there is one ruff, one config.
# Bumping the ruff `rev` in .pre-commit-config.yaml updates CI
# automatically; no edit here is ever needed for a ruff upgrade.
#
# pre-commit derives the file scope ITSELF from --from-ref/
# --to-ref; we deliberately do NOT hand it a file list. A
# hand-rolled `git diff | tr '\n' ' '` list carries a trailing
# space, and `pre-commit run --files "<path> "` matches no real
# file → "(no files to check) Skipped" → exit 0 → the gate
# silently passes broken code. --from-ref/--to-ref sidesteps that
# entirely. --show-diff-on-failure prints the exact reformat diff
# so the fix is obvious straight from the CI log.
#
# `uvx` runs pre-commit as an isolated tool, so no `uv sync` / apt
# build toolchain is needed merely to lint (pre-commit builds ruff
# from a prebuilt wheel — no native code). pre-commit is left
# UNPINNED on purpose: it is just the runner, and only the ruff
# `rev` in .pre-commit-config.yaml affects results — so there is
# no third version to babysit alongside the ruff rev + pin.
env:
DIFF_BASE: ${{ steps.base.outputs.ref }}
run: |
uvx pre-commit run ruff \
--from-ref "$DIFF_BASE" --to-ref HEAD --show-diff-on-failure
uvx pre-commit run ruff-format \
--from-ref "$DIFF_BASE" --to-ref HEAD --show-diff-on-failure
lockfile:
# Assert uv.lock is resolved against the CURRENT pyproject.toml. The
# test image installs deps with `uv sync --frozen`, which applies
# uv.lock AS-IS without checking it against pyproject.toml — so a dep
# edited without re-running `uv lock` would leave dev CI green while
# testing stale deps, and the release pipeline's own `uv lock` step
# would then silently re-resolve into deps dev never tested. This
# fast standalone gate makes lock consistency a tested invariant on
# dev, turning uv.lock into a trustworthy artifact. `uv lock --check`
# is read-only: it never writes the lockfile, it only fails on drift.
# Mirrors the `lint` job: same pinned uv setup, no project sync.
name: uv.lock in sync
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
# PIN OBOWIAZKOWY. Bez `version` setup-uv loguje "Could not
# determine uv version from uv.toml or pyproject.toml. Falling back
# to latest" i bierze najnowsze uv z releases.astral.sh. `uv lock
# --check` zaczyna wtedy zalezec od DATY przebiegu, nie od tresci
# repo: ten sam, bit-w-bit identyczny uv.lock przechodzil na uv
# 0.11.29 i padal na 0.11.31 ("lockfile needs to be updated"),
# wywracajac gate na branchach, ktore deps w ogole nie ruszaly.
# Wersje podnosimy SWIADOMIE, razem z przegenerowanym uv.lock.
version: "0.13.5"
enable-cache: true
- name: Assert uv.lock matches pyproject.toml
run: uv lock --check
baseline-check:
# Guard repo-state-only: czy baseline-sql/baseline.sql nadaza za migracjami
# (manage.py baseline_check). Wydzielone z prepare-image i puszczone
# ROWNOLEGLE — wczesniej te ~37s (apt + uv sync + check) blokowaly start
# builda obrazu, a wiec i wszystkich shardow, na KAZDYM runie. Drift jest
# rzadki: sporadyczny "zmarnowany" build przy realnym drifcie jest tanszy
# niz 37s na kazdym runie. Drift = ten job czerwony = caly run czerwony.
name: Check baseline freshness
if: |
!(github.ref == 'refs/heads/dev' && contains(github.event.head_commit.message, 'Merge tag'))
&& !(github.ref == 'refs/heads/master' && startsWith(github.event.head_commit.message, 'Release v'))
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install system build deps (for uv sync)
run: |
sudo apt-get update
xargs -a docker/bpp_base/build.txt sudo apt-get install -y
- name: Install uv
uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
# Patrz komentarz przy jobie `lock-check`. Tu dochodzi drugi powod:
# `uv sync --frozen` czyta uv.lock, wiec plywajaca wersja uv czyni
# srodowisko testowe niereprodukowalnym miedzy przebiegami.
version: "0.13.5"
enable-cache: false
- name: Install project deps
run: uv sync --frozen --no-install-project
- name: Check baseline freshness
env:
DJANGO_BPP_SKIP_DOTENV: "1"
run: uv run python src/manage.py baseline_check --max-delta 50
prepare-image:
# Build the test-runner image once and push it to GHCR with two
# tags: `:sha-<commit>` (consumed by downstream test jobs) and
# `:cache` (registry-mode buildx cache, auto-merged across builds).
# All test jobs `needs:` this step so the image is built once per
# workflow run instead of N times per matrix runner.
name: Build test-runner image
if: |
!(github.ref == 'refs/heads/dev' && contains(github.event.head_commit.message, 'Merge tag'))
&& !(github.ref == 'refs/heads/master' && startsWith(github.event.head_commit.message, 'Release v'))
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
# GHCR push needs `packages: write`. GITHUB_TOKEN inherits this
# scope only when the job declares it explicitly.
packages: write
outputs:
image: ${{ steps.tag.outputs.image }}
env:
PYTHON_VERSION: "3.12"
DEBIAN_VERSION: trixie
NODEJS_VERSION: "24"
# Lowercase repo owner, since GHCR rejects mixed-case namespaces.
# GitHub Actions ${{ github.repository_owner }} preserves case.
IMAGE_REPO: ghcr.io/iplweb/bpp-test-runner
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# baseline_check przeniesiony do osobnego joba `baseline-check`, ktory
# biegnie ROWNOLEGLE z tym buildem (wczesniej jego ~37s apt+uv sync+check
# blokowaly start builda obrazu — a wiec i wszystkich shardow). Baseline
# drift jest rzadki, wiec sporadyczny "zmarnowany" build przy realnym
# drifcie jest tanszy niz 37s na kazdym runie.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver: docker-container
- name: Log in to GHCR
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image tag
id: tag
env:
SHA: ${{ github.sha }}
run: |
echo "image=${IMAGE_REPO}:sha-${SHA}" >> "$GITHUB_OUTPUT"
- name: Build & push test-runner image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: docker/bpp_base/Dockerfile
target: test-runner
# Zstd zmniejsza transfer obrazu pullowanego rownolegle przez 12
# shardow. `outputs: type=registry` jest odpowiednikiem push=true.
outputs: type=registry,compression=zstd,compression-level=3,force-compression=true
# Two tags: immutable sha-pinned (consumed by tests jobs) and
# the registry-cache helper used to seed `cache-from` next time.
tags: |
${{ steps.tag.outputs.image }}
build-args: |
PYTHON_VERSION=${{ env.PYTHON_VERSION }}
DEBIAN_VERSION=${{ env.DEBIAN_VERSION }}
NODEJS_VERSION=${{ env.NODEJS_VERSION }}
# Zmienia się co commit -> busta warstwy COPY src/ + grunt w
# test-runnerze (patrz ARG GIT_SHA w Dockerfile), żeby cache-from
# nie przywrócił nieaktualnego drzewa źródeł.
GIT_SHA=${{ github.sha }}
# Registry-mode cache. mode=max stores all intermediate layers,
# so subsequent builds where uv.lock/Dockerfile/build.txt are
# unchanged finish in seconds. The first run after rolling out
# this workflow has no cache and pays the full ~3 min build —
# warm cache afterwards.
cache-from: type=registry,ref=${{ env.IMAGE_REPO }}:cache
cache-to: type=registry,ref=${{ env.IMAGE_REPO }}:cache,mode=max
tests:
# Single sharded job covering the full suite (playwright +
# non-playwright + serial) under `pytest -n auto`. Earlier the
# workflow split this into three specialised jobs (parallel /
# serial / playwright); shard-then-mix is faster end-to-end because
# the per-shard fixed cost (image pull, PG init + baseline load,
# Redis startup) is paid once instead of three times.
#
# Splitting is duration-based (pytest-split, `--splitting-algorithm
# least_duration`): each group gets tests whose ESTIMATED total
# runtime is roughly equal, read from the committed `.test_durations`
# file (mounted read-only via docker-compose.test.ci.yml). This
# replaced pytest-shard's hash-based split, which balanced test COUNT
# not TIME and so left whichever shard happened to collect the slow
# Playwright tests ~2x slower than the rest — and the gate waits on
# the slowest shard. If `.test_durations` is missing/stale,
# pytest-split degrades gracefully to an even split (tests absent
# from the file are weighted at the average), so CI never breaks on
# a stale file — it just balances less well until `make tests`
# regenerates it. The matrix stays 0-indexed (shard) to keep the job
# names — and the branch-protection required checks — unchanged;
# pytest-split groups are 1-indexed, so the run step maps group=shard+1.
#
# @pytest.mark.serial tests are deliberately mixed in here. They
# don't actually depend on running outside xdist — the marker is
# used by callers that want to opt out of parallelism, but the
# tests themselves pass under -n auto. If that ever changes, gate
# serial tests with @pytest.mark.xdist_group(name="serial") so
# `--dist=loadgroup` keeps them on a single worker.
#
# DB setup: docker-compose.test.yml mounts baseline.sql into
# /docker-entrypoint-initdb.d/ and sets DJANGO_BPP_TEST_TEMPLATE=bpp,
# so xdist worker DBs are created via `CREATE DATABASE … WITH
# TEMPLATE bpp` (instant in-server clone) instead of replaying
# `psql -f baseline.sql` per worker.
name: Tests (sharded)
needs: prepare-image
if: |
!(github.ref == 'refs/heads/dev' && contains(github.event.head_commit.message, 'Merge tag'))
&& !(github.ref == 'refs/heads/master' && startsWith(github.event.head_commit.message, 'Release v'))
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
contents: read
packages: read
strategy:
fail-fast: false
max-parallel: 12
matrix:
python-version: ["3.12"]
# 0-indexed; MUSI byc spojne z NUM_SHARDS nizej. Duration-split
# (pytest-split, .test_durations) balansuje dowolne N po czasie, wiec
# bump jest tani: 8 -> 12 tnie czas testow najwolniejszego sharda
# (~220s -> ~150s). Powyzej ~12 zysk topnieje, bo czas sharda spada
# ponizej czasu prepare-image (to staje sie podloga krytycznej sciezki).
shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
env:
TEST_RUNNER_IMAGE: ${{ needs.prepare-image.outputs.image }}
COMPOSE_FILES: -f docker-compose.test.yml -f docker-compose.test.ci.yml
NUM_SHARDS: "12"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Log in to GHCR
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare ci-output dir
run: mkdir -p ci-output
- name: Pull test-runner image
# Retry na transient network/registry error (timeout GHCR/Docker Hub
# to najczestszy szum CI — nie regresja kodu). 3 proby, 10s backoff;
# dopiero trwala porazka wywala krok.
run: |
n=0
until docker compose $COMPOSE_FILES pull test-runner; do
n=$((n + 1))
if [ "$n" -ge 3 ]; then
echo "docker pull test-runner nie powiodl sie po $n probach" >&2
exit 1
fi
echo "pull test-runner padl (proba $n), ponawiam za 10s..." >&2
sleep 10
done
- name: Pull service images (db, redis)
# Osobny, retry-owany pull obrazow z Docker Huba (iplweb/bpp_dbserver,
# redis) PRZED `up -d`. Wczesniej `up -d` pobieral je implicite bez
# retry — timeout Docker Huba na iplweb/bpp_dbserver wywalal caly shard
# (poz. 3.3 audytu). Teraz `up -d` widzi obrazy juz lokalnie.
run: |
n=0
until docker compose $COMPOSE_FILES pull db redis; do
n=$((n + 1))
if [ "$n" -ge 3 ]; then
echo "docker pull db/redis nie powiodl sie po $n probach" >&2
exit 1
fi
echo "pull db/redis padl (proba $n), ponawiam za 10s..." >&2
sleep 10
done
- name: Start services
run: docker compose $COMPOSE_FILES up -d db redis
- name: Wait for services
timeout-minutes: 2
run: |
until docker compose $COMPOSE_FILES exec db \
pg_isready -U bpp; do
echo "Waiting for PostgreSQL..."
sleep 2
done
until docker compose $COMPOSE_FILES exec redis \
redis-cli ping; do
echo "Waiting for Redis..."
sleep 2
done
- name: Tests (shard ${{ matrix.shard }}/${{ env.NUM_SHARDS }})
timeout-minutes: 25
env:
SHARD_ID: ${{ matrix.shard }}
run: |
# pytest-split groups are 1-indexed; the matrix shard is
# 0-indexed (kept that way so the job names / required checks
# don't change). Map group = shard + 1.
GROUP=$((SHARD_ID + 1))
docker compose $COMPOSE_FILES run --rm \
test-runner uv run pytest \
-n auto \
--splits "$NUM_SHARDS" --group "$GROUP" \
--splitting-algorithm least_duration \
--durations-path .test_durations \
--timeout 300
- name: Cleanup
if: always()
run: docker compose $COMPOSE_FILES down -v
js-tests:
# Jednostkowe testy czystej logiki JS (vitest): budowanie URL-i
# eksploratora powiazan, geometria ukladow grafu, normalizacja
# rozmiarow, formatowanie/lokalizacja DjangoQL. NIE wymaga obrazu
# testowego ani DB — czysty node, ~1 min. notifications.js zyje i
# jest testowany w pakiecie django-channels-broadcast (jego CI).
name: JS unit tests (vitest)
if: |
!(github.ref == 'refs/heads/dev' && contains(github.event.head_commit.message, 'Merge tag'))
&& !(github.ref == 'refs/heads/master' && startsWith(github.event.head_commit.message, 'Release v'))
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24"
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Run vitest suite
run: npx vitest run