Repository navigation
157 lines (148 loc) · 7.45 KB
/
Copy pathpython-ci.yml
File metadata and controls
157 lines (148 loc) · 7.45 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
name: Python CI
# Lint, format, and test gates for the backend. The disabled Netflix-era
# python.yml this replaces installed uv via unpinned `curl | sh` and had a
# per-file coverage gate that could never fail (it compared against a
# variable that was never assigned).
on:
pull_request:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
UV_VERSION: "0.5.9" # same pin as python-lock-sync
jobs:
python-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install ruff
# Version comes from .pre-commit-config.yaml so the CI gate can never
# drift from what the hooks enforce locally. The sed is scoped to the
# ruff-pre-commit block so adding repos above it can't change which
# rev is picked up.
run: |
RUFF_VERSION=$(sed -n '/astral-sh\/ruff-pre-commit/,/rev:/{s/^ *rev: v//p}' .pre-commit-config.yaml)
test -n "$RUFF_VERSION"
pipx install "ruff==${RUFF_VERSION}"
- run: ruff check .
- run: ruff format --check .
python-test:
runs-on: ubuntu-latest
services:
postgres:
# Same major as the deployment's pinned postgres image.
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: not-a-real-password-tests-only
ports:
- 5432:5432
# -h forces TCP: a bare pg_isready answers over the unix socket the
# entrypoint's bootstrap server listens on during initdb, so it reports
# healthy before the real server accepts connections.
options: >-
--health-cmd "pg_isready -h 127.0.0.1 -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- name: Install the production lock
run: |
python -m pip install "uv==${UV_VERSION}"
uv pip install --system --no-deps --require-hashes -r requirements-lock.txt
uv pip install --system --no-deps -e .
- name: Import the app from the lock alone
# Catches undeclared runtime dependencies (packages the code imports
# that only reach the environment via the test tools installed below).
# A missing sortedcontainers declaration once passed every other gate
# and would have broken the production image at startup.
env:
DISPATCH_ENCRYPTION_KEY: not-a-real-key-tests-only
DISPATCH_JWT_SECRET: not-a-real-key-tests-only
DATABASE_CREDENTIALS: postgres:not-a-real-password-tests-only
DATABASE_HOSTNAME: localhost
DISPATCH_UI_URL: https://example.com
STATIC_DIR: ""
run: python -c "import dispatch.main, dispatch.cli"
- name: Install test-only tools
# Constrained to the lock so they cannot move locked versions.
run: |
uv pip install --system --constraint requirements-lock.txt \
pytest==9.1.1 pytest-mock pytest-xdist pytest-cov factory-boy faker \
easydict devtools schemathesis coverage
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
cache-dependency-path: src/dispatch/static/dispatch/package-lock.json
- name: Install frontend node_modules for the mjml tests
working-directory: src/dispatch/static/dispatch
run: npm ci --ignore-scripts --no-audit --no-fund
- name: Run pytest with coverage
env:
# Optional. With these set, tests/plugins/dispatch_slack/test_slack_live.py
# posts to a real workspace; without them it skips, which is the case
# on forks and until the secrets are configured. See that file's
# docstring for the app manifest and scopes.
DISPATCH_SLACK_TEST_BOT_TOKEN: ${{ secrets.DISPATCH_SLACK_TEST_BOT_TOKEN }}
DISPATCH_SLACK_TEST_CHANNEL_ID: ${{ secrets.DISPATCH_SLACK_TEST_CHANNEL_ID }}
DISPATCH_SLACK_TEST_USER_ID: ${{ secrets.DISPATCH_SLACK_TEST_USER_ID }}
# Same arrangement for tests/plugins/dispatch_microsoft_teams/test_teams_live.py,
# which creates real meetings in the configured tenant when all four are set.
DISPATCH_MSTEAMS_TEST_AUTHORITY: ${{ secrets.DISPATCH_MSTEAMS_TEST_AUTHORITY }}
DISPATCH_MSTEAMS_TEST_CLIENT_ID: ${{ secrets.DISPATCH_MSTEAMS_TEST_CLIENT_ID }}
DISPATCH_MSTEAMS_TEST_SECRET: ${{ secrets.DISPATCH_MSTEAMS_TEST_SECRET }}
DISPATCH_MSTEAMS_TEST_USER_ID: ${{ secrets.DISPATCH_MSTEAMS_TEST_USER_ID }}
# Same arrangement for tests/plugins/dispatch_zoom/test_zoom_live.py,
# which is read-only: it authenticates and lists meetings, creating
# nothing in the configured Zoom account.
DISPATCH_ZOOM_TEST_ACCOUNT_ID: ${{ secrets.DISPATCH_ZOOM_TEST_ACCOUNT_ID }}
DISPATCH_ZOOM_TEST_CLIENT_ID: ${{ secrets.DISPATCH_ZOOM_TEST_CLIENT_ID }}
DISPATCH_ZOOM_TEST_CLIENT_SECRET: ${{ secrets.DISPATCH_ZOOM_TEST_CLIENT_SECRET }}
DISPATCH_ZOOM_TEST_API_USER_ID: ${{ secrets.DISPATCH_ZOOM_TEST_API_USER_ID }}
# --dist loadfile, not the default: module-scoped fixtures here restore
# whole databases, which per-test distribution would rebuild per worker.
# pytest-cov, not `coverage run`: coverage does not follow xdist's worker
# subprocesses and reports only the controller (22% here, measured).
run: |
pytest -q -n auto --dist loadfile --cov --cov-report=xml \
--junitxml=junit.xml -o junit_family=legacy
coverage report --format=markdown >> "$GITHUB_STEP_SUMMARY" || true
# Codecov rejects tokenless uploads here ("Token required - not valid
# tokenless upload"), so the token is not optional despite the repository
# being public. Failing the job is gated on having had one: a fork PR
# cannot read the secret, and blocking outside contributors on an upload
# they were never able to make is not the point of this gate. With a
# token, a rejected upload is a real failure -- it reported success while
# uploading nothing until this was turned on.
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage.xml
flags: backend
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: ${{ secrets.CODECOV_TOKEN != '' }}
# Test Analytics, not coverage: this uploads the JUnit report so Codecov
# can track flakiness and per-test duration. Worth having because the
# suite is order-sensitive and now runs in parallel, which is where
# flakiness would show up first. Same token, no new secret.
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1
with:
files: ./junit.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: ${{ secrets.CODECOV_TOKEN != '' }}