forked from hannibalevit/resumorph
-
Notifications
You must be signed in to change notification settings - Fork 0
221 lines (183 loc) · 6.63 KB
/
Copy pathserver-ci.yml
File metadata and controls
221 lines (183 loc) · 6.63 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
name: Server CI
# `pull_request` is what makes this run for contributions from forks: a fork's
# commits are pushed to the *fork's* repository, so no `push` event ever fires
# here, and the 5 jobs the `main` ruleset requires as status checks would sit
# pending forever. `push` is narrowed to `main` so in-repo branches don't get a
# duplicate run alongside their PR — post-merge pushes still need it for the
# `coverage-badge` job below.
#
# Runs from a fork get a read-only token and no secrets. Every job required by
# the ruleset works without them; anything that needs a secret must stay gated
# on `github.event_name == 'push'` (see `coverage-badge`).
on:
push:
branches: [main]
pull_request:
concurrency:
group: server-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
PYTHON_VERSION: "3.13"
jobs:
# Static analysis only — no system libs needed, so this is the cheapest possible
# gate. Everything else waits on this so we fail fast on style/type errors before
# spending time on apt installs or container spin-up.
lint:
name: Lint & type-check (native)
runs-on: ubuntu-latest
defaults:
run:
working-directory: server
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: "server/uv.lock"
- name: Set up Python
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: uv sync --frozen --dev
- name: Ruff check
run: uv run ruff check .
- name: Ruff format check
run: uv run ruff format --check .
- name: mypy
run: uv run mypy
- name: deptry
run: uv run deptry .
# Isolated test environment: the full pytest suite (with coverage) run inside a
# fresh python:3.13-slim container matching server/Dockerfile's base image,
# rather than the GitHub-hosted runner's own Ubuntu filesystem. Catches "works
# on the runner, breaks in prod" drift — e.g. a missing apt package or a
# libc/OpenSSL version the runner happens to have preinstalled but the real
# deployment target (Debian slim) doesn't.
test-docker:
name: Pytest (isolated Docker container)
needs: lint
runs-on: ubuntu-latest
container:
image: python:3.13-slim
defaults:
run:
working-directory: server
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install system dependencies
run: |
apt-get update
apt-get install -y --no-install-recommends curl ca-certificates
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install dependencies
run: uv sync --frozen --dev
- name: Run pytest
run: uv run pytest
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-coverage
path: server/htmlcov/
retention-days: 7
- name: Upload coverage.xml
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: backend-coverage-xml
path: server/coverage.xml
retention-days: 1
# Regenerates the README coverage badge from main's actual coverage numbers.
# Only runs on main (not PRs/branches) so the committed badge always reflects
# what's really on main, and only after test-docker has produced coverage.xml.
coverage-badge:
name: Update coverage badge
needs: test-docker
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: main
token: ${{ secrets.RELEASE_BOT_TOKEN }}
- name: Download coverage.xml
uses: actions/download-artifact@v4
with:
name: backend-coverage-xml
path: server
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
# auto-release.yml's version bump can land on main between our checkout
# and this commit — rebase onto whatever is latest so the push below
# doesn't get rejected as non-fast-forward. Must run before the badge is
# generated: rebase refuses to run with unstaged changes in the tree,
# and generating the badge dirties coverage.svg.
- name: Sync with latest main
run: |
git fetch origin main
git rebase origin/main
- name: Generate badge
run: |
uvx --from "genbadge[coverage]" genbadge coverage \
-i server/coverage.xml -o .github/badges/coverage.svg
- name: Commit badge if changed
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: update coverage badge [skip ci]"
file_pattern: ".github/badges/coverage.svg"
branch: main
# Builds and boots the actual image server/Dockerfile produces (what
# docker-compose ships to users) and smoke-tests it end to end. This is
# deliberately the most expensive lane, so it only starts once test-docker has
# proven the code itself works on the same Debian-slim base the image uses.
docker-image-smoke-test:
name: Build & smoke-test Docker image
needs: test-docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build server image
uses: docker/build-push-action@v6
with:
context: server
tags: resumorph-server:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run container
run: |
docker run -d --name resumorph-server \
-p 8000:8000 \
-e DATABASE_URL=sqlite:////tmp/resumorph.db \
-e ALLOWED_ORIGINS="*" \
resumorph-server:ci
- name: Wait for health check
run: |
for i in $(seq 1 30); do
if curl -sf http://localhost:8000/health; then
echo "Server is healthy"
exit 0
fi
sleep 2
done
echo "Server did not become healthy in time"
exit 1
- name: Container logs
if: always()
run: docker logs resumorph-server
- name: Stop container
if: always()
run: docker rm -f resumorph-server