-
Notifications
You must be signed in to change notification settings - Fork 0
302 lines (289 loc) · 12.1 KB
/
Copy pathmain.yml
File metadata and controls
302 lines (289 loc) · 12.1 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
name: main
on: [push]
# Declared once for the whole workflow: every job inherits these, and a job- or
# step-level `env:` still overrides. All of it is fake credentials that only need to
# be non-empty and well-formed, so there is nothing job-specific to vary.
# `DJANGO_SETTINGS_MODULE` is the one value that genuinely differs (test vs
# production), so it is set per job / per step below and deliberately absent here.
env:
PYTHON_VERSION: "3.14"
UV_VERSION: "0.11.18"
SECRET_KEY: "ci-secret-key-not-for-production-use-only-0123456789"
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/vinta_schedule_test"
REDIS_URL: "redis://localhost:6379"
CORS_ALLOWED_ORIGINS: "https://example.com"
ALLOWED_HOSTS: ".example.com"
SMTP_HOST: 'example.com'
SMTP_USERNAME: "test"
SMTP_PASSWORD: "test"
SITE_DOMAIN: 'example.com'
API_DOMAIN: 'api.example.com'
DEFAULT_BCC_EMAILS: 'hugo@example.com'
DEFAULT_FROM_EMAIL: 'noreply@example.com'
AWS_STATIC_BUCKET_NAME: 'example-static'
AWS_ACCESS_KEY_ID: 'test'
AWS_SECRET_ACCESS_KEY: 'test'
AWS_S3_ACCESS_KEY_ID: 'test'
AWS_S3_SECRET_ACCESS_KEY: 'test'
AWS_MEDIA_BUCKET_NAME: 'example-media'
AWS_MEDIA_LOCATION: 'media'
AWS_MEDIA_S3_CUSTOM_DOMAIN: 'example.com'
AWS_MEDIA_S3_ENDPOINT_URL: 'https://example.com'
AWS_S3_REGION_NAME: 'us-east-1'
AWS_CLOUDFRONT_KEY_ID: 'test'
AWS_CLOUDFRONT_KEY: 'test'
SALT_KEY: 'test_salt_key_123'
TWILIO_ACCOUNT_SID: 'FAKE_ACCOUNT_SID'
TWILIO_API_KEY_SID: 'FAKE_API_KEY_SID'
TWILIO_API_KEY_SECRET: 'FAKE_API_KEY_SECRET'
TWILIO_NUMBER: '+12345678901'
TWILIO_DEFAULT_BROADCAST_NUMBERS: '+12345678901,+10987654321'
ACCOUNT_PHONE_VERIFICATION_ENABLED: 'false'
MERCADOPAGO_ACCESS_TOKEN: 'FAKE_MERCADOPAGO_ACCESS_TOKEN_FOR_CI'
MERCADOPAGO_WEBHOOK_SECRET: 'FAKE_WEBHOOK_SECRET_FOR_CI'
MERCADOPAGO_PUBLIC_KEY: 'FAKE_MERCADOPAGO_PUBLIC_KEY_FOR_CI'
STRIPE_SECRET_KEY: 'FAKE_STRIPE_SECRET_KEY_FOR_CI'
STRIPE_WEBHOOK_SECRET: 'FAKE_STRIPE_WEBHOOK_SECRET_FOR_CI'
STRIPE_PUBLISHABLE_KEY: 'FAKE_STRIPE_PUBLISHABLE_KEY_FOR_CI'
DEFAULT_PAYMENT_PROVIDER: 'stripe'
jobs:
checks:
name: Lint and Django checks
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: vinta_schedule_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
- run: uv sync --frozen
- name: Linting
run: uv run ruff check ./
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.test"
# A zero-error gate, not a baseline comparison: the backlog this repo carried
# (281 errors as of the commit that added this step) is gone, so any new error is
# this PR's. mypy needs no database -- the django-stubs plugin only imports the
# settings module named by `[tool.django-stubs]` -- but it does need the env vars
# that module reads, which the workflow-level `env:` block already provides.
- name: Type checking
run: uv run mypy .
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.test"
- run: uv run pre-commit run --all-files
env:
SKIP: ruff,eslint,missing-migrations,backend-schema,backend-auth-schema,check-event-guarded-surfaces,check-legacy-membership-resolution
- run: uv run python manage.py export_allauth_schema --check
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.production"
# `spectacular` has no `--check`, so the gate is regenerate-and-diff.
# `schema.yml` is committed and read directly by
# `organizations/tests/test_membership_api_surface.py`, which asks it whether any
# published serializer still declares `role` -- a question a stale file answers
# wrongly and silently. The pre-commit hook regenerates but does not fail, and it
# is in SKIP above, so without this step nothing in CI noticed.
- name: OpenAPI schema is up to date
run: |
uv run python manage.py spectacular --file schema.yml
git diff --exit-code -- schema.yml \
|| { echo '::error::schema.yml is stale. Run: python manage.py spectacular --file schema.yml'; exit 1; }
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.test"
- run: uv run python manage.py makemigrations --check --dry-run
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.production"
- run: uv run python manage.py check --deploy
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.production"
- run: uv run python manage.py check_event_guarded_surfaces
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.production"
- run: uv run python manage.py check_legacy_membership_resolution
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.test"
test:
name: Tests (shard ${{ matrix.shard }})
runs-on: ubuntu-latest
strategy:
# Each shard is an independent slice of the suite; one failing slice should
# not hide the failures in the others.
fail-fast: false
matrix:
# Keep `shard` the only dimension: `--splits` below reads
# `strategy.job-total`, which counts *all* matrix combinations.
shard: [1, 2, 3, 4]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: vinta_schedule_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
# Every step here runs under the test settings, unlike the `checks` job where the
# module varies per step -- so this one override lives at the job level.
env:
DJANGO_SETTINGS_MODULE: "vinta_schedule_api.settings.test"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
- run: uv sync --frozen
# `pytest-split` assigns each test to exactly one shard, so the four shards
# together run the whole suite -- adding an app or a test file needs no change
# here. With no `.test_durations` file committed it splits by test count, which
# measured within ~13% of a duration-balanced split (worst shard 99s vs 88s of
# a 350s suite) -- not worth the 900KB generated file that duration balancing
# would need. If the shards ever drift far apart, re-measure with
# `uv run pytest --store-durations --clean-durations --no-cov` (single process;
# the flag does not work under `-n auto`) and commit `.test_durations`.
# `-n auto` still parallelizes *within* a shard across the runner's cores.
- name: Run tests (shard ${{ matrix.shard }} of ${{ strategy.job-total }})
run: |
uv run pytest -vs --no-header -n auto \
--splits ${{ strategy.job-total }} --group ${{ matrix.shard }} \
--cov=. --cov-report=xml:junit/test-results.xml \
--junitxml=junit.xml -o junit_family=legacy
# Each shard covers only the code its own slice exercised; Codecov merges the
# four partial reports for the commit. Artifact names must be unique per shard.
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
name: code-coverage-report-shard-${{ matrix.shard }}
path: junit/test-results.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
env_vars: OS,PYTHON
fail_ci_if_error: true
files: ./junit/test-results.xml
flags: unittests
name: codecov-shard-${{ matrix.shard }}
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
deploy-staging:
name: Deploy staging (ECS)
needs: [checks, test]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
# `id-token` is what lets this job exchange a GitHub OIDC token for the AWS
# deploy role -- no access keys are stored anywhere in the repository. The role
# itself only trusts this repo on this ref (see modules/app-platform/iam.tf).
permissions:
id-token: write
contents: read
concurrency:
# Two deploys rolling the same services at once leaves ECS arbitrating
# between revisions. Queue them instead, and never cancel one mid-migration.
group: deploy-staging
cancel-in-progress: false
env:
AWS_REGION: us-east-1
# Written by the app-platform Terraform stack; holds the cluster, service
# names, task-definition families, subnets and security groups.
DEPLOY_PARAMETER: /vinta-schedule/staging/deploy
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN_STAGING }}
aws-region: ${{ env.AWS_REGION }}
# The repository URL comes from the same SSM parameter the deploy script
# reads, so the image location has exactly one source of truth: Terraform.
- name: Resolve deploy target
id: target
run: |
repo=$(aws ssm get-parameter --name "$DEPLOY_PARAMETER" \
--query 'Parameter.Value' --output text | jq -r '.ecr_repository_url')
echo "image=${repo}:${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "latest=${repo}:latest" >> "$GITHUB_OUTPUT"
- name: Log in to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
push: true
# Two tags for one image: the SHA is what the deploy pins and what a
# rollback names, `latest` is what a hand-run `ecs run-task` picks up.
tags: |
${{ steps.target.outputs.image }}
${{ steps.target.outputs.latest }}
# GitHub's own layer cache. The expensive layer is `uv sync`, which only
# changes when uv.lock does, so most deploys reuse it.
cache-from: type=gha
cache-to: type=gha,mode=max
# Without this, buildx pushes an OCI image index with an attestation
# manifest attached -- which ECS Fargate cannot pull.
provenance: false
- name: Migrate and roll out
run: |
./scripts/deploy/ecs_deploy.sh \
"$DEPLOY_PARAMETER" \
"${{ steps.target.outputs.image }}" \
"${{ github.sha }}"