-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
946 lines (911 loc) · 31.3 KB
/
Copy pathci-cd.yml
File metadata and controls
946 lines (911 loc) · 31.3 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
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
name: CI
on:
merge_group:
push:
branches:
- 'master'
- '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6
tags: [ 'v*' ]
pull_request:
branches:
- 'master'
- '[0-9].[0-9]+'
schedule:
- cron: '0 6 * * *' # Daily 6AM UTC build
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
COLOR: yes
FORCE_COLOR: 1 # Request colored output from CLI tools supporting it
MYPY_FORCE_COLOR: 1
PY_COLORS: 1
UPSTREAM_REPOSITORY_ID: >-
13258039
permissions: {}
jobs:
pre-setup:
name: Pre-Setup global build settings
runs-on: ubuntu-latest
outputs:
upstream-repository-id: ${{ env.UPSTREAM_REPOSITORY_ID }}
release-requested: >-
${{
(
github.event_name == 'push'
&& github.ref_type == 'tag'
)
&& true
|| false
}}
steps:
- name: Dummy
if: false
run: |
echo "Pre-setup step"
lint:
permissions:
contents: read # to fetch code (actions/checkout)
pull-requests: read # to read PR metadata (verify change fragments step)
name: Linter
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: >-
Verify that `requirements/runtime-deps.in`
is in sync with `pyproject.toml`
run: |
set -eEuo pipefail
make sync-direct-runtime-deps
git diff --exit-code -- requirements/runtime-deps.in
- name: Setup Python
uses: actions/setup-python@v7.0.0
with:
python-version: 3.11
- name: Cache PyPI
uses: actions/cache@v6.1.0
with:
key: pip-lint-${{ hashFiles('requirements/*.txt') }}
path: ~/.cache/pip
restore-keys: |
pip-lint-
- name: Install dependencies
run: |
python -m pip install -U pip wheel setuptools build twine -r requirements/lint.in -c requirements/lint.txt
- name: Install self
run: |
python -m pip install . -c requirements/runtime-deps.txt
env:
AIOHTTP_NO_EXTENSIONS: 1
- name: Run mypy
run: |
make mypy
- name: Run slotscheck
run: |
# Some extra requirements are needed to ensure all modules
# can be scanned by slotscheck.
pip install -r requirements/base.in -c requirements/base.txt
slotscheck -v -m aiohttp
- name: Verify CHANGES fragment references PR
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
# Fetch the body fresh: github.event.pull_request.body is frozen at
# the webhook payload and stale on re-runs after a body edit.
PR_BODY=$(
gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq .body
)
added=$(
gh api --paginate \
"repos/${{ github.repository }}/pulls/${PR_NUMBER}/files" \
--jq '.[] | select(.status=="added") | .filename' \
| grep '^CHANGES/' || true
)
failed=0
for f in $added; do
num=$(basename "$f" | cut -d. -f1)
[[ "$num" =~ ^[0-9]+$ ]] || continue
if [[ "$num" == "$PR_NUMBER" ]]; then continue; fi
if grep -qE "(^|[^0-9])#${num}([^0-9]|$)" <<<"$PR_BODY"; then continue; fi
echo "::error file=$f::Change fragment ($num) must reference PR #$PR_NUMBER or an issue this PR fixes"
failed=1
done
exit $failed
- name: Install spell checker
run: |
pip install -r requirements/doc-spelling.in -c requirements/doc-spelling.txt
- name: Run docs spelling
run: |
# towncrier --yes # uncomment me after publishing a release
make doc-spelling
- name: Build package
run: |
python -m build
env:
AIOHTTP_NO_EXTENSIONS: 1
- name: Run twine checker
run: |
twine check --strict dist/*
- name: Making sure that CONTRIBUTORS.txt remains sorted
run: |
LC_ALL=C sort --check --ignore-case CONTRIBUTORS.txt
gen_llhttp:
permissions:
contents: read # to fetch code (actions/checkout)
name: Generate llhttp sources
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Cache llhttp generated files
uses: actions/cache@v6.1.0
id: cache
with:
key: llhttp-${{ hashFiles('vendor/llhttp/package*.json', 'vendor/llhttp/src/**/*') }}
path: vendor/llhttp/build
- name: Setup NodeJS
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/setup-node@v7
with:
node-version: 18
- name: Generate llhttp sources
if: steps.cache.outputs.cache-hit != 'true'
run: |
make generate-llhttp
- name: Upload llhttp generated files
uses: actions/upload-artifact@v7
with:
name: llhttp
path: vendor/llhttp/build
if-no-files-found: error
test:
permissions:
contents: read # to fetch code (actions/checkout)
name: Test
needs: gen_llhttp
strategy:
matrix:
pyver: ['3.10', '3.11', '3.12', '3.13', '3.14']
no-extensions: ['', 'Y']
os: [ubuntu, macos, windows]
experimental: [false]
exclude:
- os: macos
no-extensions: 'Y'
- os: windows
no-extensions: 'Y'
include:
- pyver: pypy-3.11
no-extensions: 'Y'
os: ubuntu
experimental: false
- os: ubuntu
pyver: "3.14t"
no-extensions: ''
experimental: false
fail-fast: true
runs-on: ${{ matrix.os }}-latest
continue-on-error: ${{ matrix.experimental }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Setup Python ${{ matrix.pyver }}
id: python-install
# important: do not use system python
env:
UV_PYTHON_PREFERENCE: only-managed
uses: astral-sh/setup-uv@v9.0.0
with:
python-version: ${{ matrix.pyver }}
activate-environment: true
enable-cache: true
- name: Install dependencies
env:
DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }}
run: |
uv pip install -U pip wheel setuptools build twine -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
- name: Set PYTHON_GIL=0 for free-threading builds
if: ${{ endsWith(matrix.pyver, 't') }}
run: echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Restore llhttp generated files
if: ${{ matrix.no-extensions == '' }}
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize
if: ${{ matrix.no-extensions == '' }}
run: |
make cythonize
- name: Install self
env:
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
run: uv pip install -e .
- name: Run unittests
env:
COLOR: yes
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
PIP_USER: 1
run: >-
pytest --junitxml=junit.xml --numprocesses=auto --cov=aiohttp/ --cov=tests/ -m 'not dev_mode and not autobahn'
shell: bash
- name: Re-run the failing tests with maximum verbosity
if: failure()
env:
COLOR: yes
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
run: >- # `exit 1` makes sure that the job remains red with flaky runs
pytest --no-cov -vvvvv --lf && exit 1
shell: bash
- name: Run dev_mode tests
env:
COLOR: yes
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
PIP_USER: 1
PYTHONDEVMODE: 1
run: pytest -m dev_mode --cov=aiohttp/ --cov=tests/ --cov-append
shell: bash
- name: Turn coverage into xml
env:
COLOR: 'yes'
PIP_USER: 1
run: |
python -m coverage xml
- name: Upload coverage
uses: codecov/codecov-action@v7
with:
files: ./coverage.xml
flags: >-
CI-GHA,OS-${{
runner.os
}},VM-${{
matrix.os
}},Py-${{
steps.python-install.outputs.python-version
}}
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v7
with:
files: ./junit.xml
report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
test-mobile:
permissions:
contents: read # to fetch code (actions/checkout)
name: Test (${{ matrix.config.platform }}, ${{ matrix.pyver }}, ${{ matrix.config.os }})
runs-on: ${{ matrix.config.os }}
needs: gen_llhttp
strategy:
matrix:
pyver: ["cp313", "cp314"]
config:
- os: ubuntu-latest
platform: android
archs: x86_64
- os: macos-14
platform: ios
archs: arm64_iphonesimulator
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Setup Python ${{ matrix.pyver }}
id: python-install
# important: do not use system python
env:
UV_PYTHON_PREFERENCE: only-managed
uses: astral-sh/setup-uv@v9.0.0
with:
python-version: ${{ matrix.pyver }}
activate-environment: true
enable-cache: true
- name: Install build tooling and cython
run: |
uv pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize
run: |
make cythonize
- name: Free up disk space for Android emulator
if: ${{ matrix.config.platform == 'android' }}
uses: BRAINSia/free-disk-space@v2.1.3
with:
android: false
docker-images: false
mandb: false
large-packages: false
- name: Enable KVM group perms for Android emulator
if: ${{ matrix.config.platform == 'android' }}
# This is normally done by cibuildwheel automatically, when it detects Github Actions. But by unsetting GITHUB_ACTIONS
# in the test step, we also disable that automatic setup. So we need to do it manually here.
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Install cibuildwheel
run: uv pip install cibuildwheel==3.4.1
- name: Build wheels and test
# cibuildwheel normally uses grouping in its outputs for its build/test steps. But the loading time when
# expanding large groups in GitHub Actions is very high. So by unsetting GITHUB_ACTIONS, cibuildwheel does
# not know that it is running in a GitHub Action and thus does not use groups.
run: env -u GITHUB_ACTIONS cibuildwheel
env:
CIBW_BUILD: ${{ matrix.pyver }}-*
CIBW_PLATFORM: ${{ matrix.config.platform }}
CIBW_ARCHS: ${{ matrix.config.archs }}
CIBW_TEST_REQUIRES: -r requirements/test-mobile.txt
CIBW_TEST_SOURCES: setup.cfg README.rst tests
# Currently only Android supports colored output. See https://github.com/python/cpython/issues/150932 for iOS.
CIBW_TEST_COMMAND: python -m pytest ${{ matrix.config.platform == 'android' && '--color=yes' || '' }}
autobahn:
permissions:
contents: read # to fetch code (actions/checkout)
name: Autobahn testsuite
needs: gen_llhttp
strategy:
matrix:
pyver: ['3.14']
no-extensions: ['']
os: [ubuntu]
fail-fast: true
runs-on: ${{ matrix.os }}-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Setup Python ${{ matrix.pyver }}
id: python-install
# important: do not use system python
env:
UV_PYTHON_PREFERENCE: only-managed
uses: astral-sh/setup-uv@v9.0.0
with:
python-version: ${{ matrix.pyver }}
activate-environment: true
enable-cache: true
- name: Install dependencies
env:
DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }}
run: |
uv pip install -U pip wheel setuptools build twine -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
- name: Restore llhttp generated files
if: ${{ matrix.no-extensions == '' }}
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize
if: ${{ matrix.no-extensions == '' }}
run: |
make cythonize
- name: Install self
env:
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
run: uv pip install -e .
- name: Run unittests
env:
COLOR: yes
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
PIP_USER: 1
run: >-
PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}"
pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ --timeout=0 -m autobahn
shell: bash
- name: Turn coverage into xml
env:
COLOR: 'yes'
PIP_USER: 1
run: |
python -m coverage xml
- name: Upload coverage
uses: codecov/codecov-action@v7
with:
files: ./coverage.xml
flags: Autobahn
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v7
with:
files: ./junit.xml
report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
benchmark:
name: Benchmark
needs:
- gen_llhttp
- pre-setup # transitive, for accessing settings
if: >-
needs.pre-setup.outputs.upstream-repository-id == github.repository_id
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout project
uses: actions/checkout@v7
with:
submodules: true
- name: Setup Python 3.13.2
id: python-install
uses: actions/setup-python@v7.0.0
with:
python-version: 3.13.2
cache: pip
cache-dependency-path: requirements/*.txt
- name: Install dependencies
run: |
python -m pip install -U pip wheel setuptools build twine -r requirements/test.in -c requirements/test.txt
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize
run: |
make cythonize
- name: Install self
run: python -m pip install -e .
- name: Load kernel TLS module
if: runner.os == 'Linux'
run: sudo modprobe tls
- name: Show kernel and OpenSSL build information
if: runner.os == 'Linux'
run: |
lsb_release -a
uname -r
openssl version -a
- name: Run benchmarks
uses: CodSpeedHQ/action@v4.18.5
with:
mode: instrumentation
run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30 --timeout=0
cython-coverage:
permissions:
contents: read # to fetch code (actions/checkout)
name: Cython coverage
needs: gen_llhttp
strategy:
matrix:
os: [ubuntu, windows]
runs-on: ${{ matrix.os }}-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Setup Python
id: python-install
uses: actions/setup-python@v7.0.0
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -Im pip install -U pip wheel setuptools build twine -r requirements/test.in -c requirements/test.txt
- name: Uninstall blocbuster
run: python -m pip uninstall blockbuster -y
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize with linetrace
run: |
make cythonize CYTHON_EXTRA="-X linetrace=True"
- name: Install self
env:
AIOHTTP_CYTHON_TRACE: 1
run: python -m pip install -e .
- name: Run tests with Cython tracing
env:
COLOR: yes
PIP_USER: 1
run: >-
pytest tests/test_client_functional.py tests/test_http_parser.py tests/test_http_writer.py tests/test_web_functional.py tests/test_web_response.py tests/test_websocket_parser.py
--cov-config=.coveragerc-cython.toml --cov=aiohttp/ --cov=tests/ --numprocesses=auto
-m 'not dev_mode and not autobahn'
shell: bash
- name: Turn coverage into xml
run: |
python -m coverage xml -o cython-coverage.xml --rcfile=.coveragerc-cython.toml
- name: Upload coverage
uses: codecov/codecov-action@v7
with:
files: ./cython-coverage.xml
disable_search: true
flags: cython-coverage
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
check: # This job does nothing and is only used for the branch protection
if: always()
needs:
- lint
- test
- test-mobile
- autobahn
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
- name: Trigger codecov notification
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
run_command: send-notifications
pre-deploy:
name: Pre-Deploy
runs-on: ubuntu-latest
needs:
- check
- pre-setup # transitive, for accessing settings
if: fromJSON(needs.pre-setup.outputs.release-requested)
steps:
- name: Dummy
run: |
echo "Predeploy step"
build-tarball:
permissions:
contents: read # to fetch code (actions/checkout)
name: Tarball
runs-on: ubuntu-latest
needs: pre-deploy
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Setup Python
uses: actions/setup-python@v7.0.0
- name: Install build tooling and cython
run: >-
python -m
pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize
run: |
make cythonize
- name: Make sdist
run: |
python -m build --sdist
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dist-sdist
path: dist
build-wheels:
permissions:
contents: read # to fetch code (actions/checkout)
name: Build wheels on ${{ matrix.os }} ${{ matrix.qemu }} ${{ matrix.musl }} ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
needs: pre-deploy
strategy:
matrix:
os: ["ubuntu-latest", "windows-latest", "windows-11-arm", "macos-latest", "ubuntu-24.04-arm"]
qemu: ['']
musl: [""]
platform: [""]
include:
# Split ubuntu/musl jobs for the sake of speed-up
- os: ubuntu-latest
qemu: ppc64le
musl: ""
- os: ubuntu-latest
qemu: ppc64le
musl: musllinux
- os: ubuntu-latest
qemu: riscv64
musl: ""
- os: ubuntu-latest
qemu: riscv64
musl: musllinux
- os: ubuntu-latest
qemu: s390x
musl: ""
- os: ubuntu-latest
qemu: s390x
musl: musllinux
# armv7l builds on aarch64 hosts. We still register QEMU so
# binfmt picks up the 32-bit ARM userspace handler regardless of
# whether the host kernel has CONFIG_COMPAT enabled. Even with
# emulation, aarch64-on-aarch64 hosting beats x86_64 by a wide
# margin.
- os: ubuntu-24.04-arm
qemu: armv7l
musl: ""
- os: ubuntu-24.04-arm
qemu: armv7l
musl: musllinux
- os: ubuntu-latest
musl: musllinux
- os: ubuntu-24.04-arm
musl: musllinux
- os: ubuntu-latest
platform: android
- os: macos-14
platform: ios
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: true
- name: Set up QEMU
if: ${{ matrix.qemu }}
uses: docker/setup-qemu-action@v4
with:
platforms: all
# This should be temporary
# xref https://github.com/docker/setup-qemu-action/issues/188
# xref https://github.com/tonistiigi/binfmt/issues/215
image: tonistiigi/binfmt:qemu-v8.1.5
id: qemu
- name: Prepare emulation
run: |
if [[ -n "${{ matrix.qemu }}" ]]; then
# Build emulated architectures only if QEMU is set,
# use default "auto" otherwise
echo "CIBW_ARCHS_LINUX=${{ matrix.qemu }}" >> $GITHUB_ENV
# Override pyproject.toml's `build[uv]`: the pypa odd-arch
# manylinux/musllinux containers do not ship `uv` preinstalled.
echo "CIBW_BUILD_FRONTEND=build" >> $GITHUB_ENV
fi
shell: bash
- name: Setup Python
uses: actions/setup-python@v7.0.0
with:
python-version: 3.x
- name: Install build tooling and cython
run: >-
python -m
pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
name: llhttp
path: vendor/llhttp/build/
- name: Cythonize
run: |
make cythonize
- name: Build wheels
uses: pypa/cibuildwheel@v4.1.1
with:
# `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be
# available on the runner for Windows and macOS. Installing
# cibuildwheel with the `uv` extra bundles uv with it; the
# tested-arch manylinux/musllinux containers also ship uv
# preinstalled. The odd-arch containers do not, so the
# `Prepare emulation` step above sets `CIBW_BUILD_FRONTEND=build`
# for those QEMU matrix cells.
extras: uv
env:
CIBW_PLATFORM: ${{ matrix.platform || 'auto' }}
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
CIBW_ARCHS_IOS: arm64_iphoneos arm64_iphonesimulator x86_64_iphonesimulator
CIBW_ARCHS_ANDROID: arm64_v8a x86_64
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: >-
dist-${{ matrix.os }}-${{ matrix.musl }}-${{
matrix.platform
&& matrix.platform
|| matrix.qemu
&& matrix.qemu
|| 'native'
}}
path: ./wheelhouse/*.whl
deploy:
name: Deploy (${{ matrix.group }})
needs:
- build-tarball
- build-wheels
- pre-setup # transitive, for accessing settings
runs-on: ubuntu-latest
if: >-
needs.pre-setup.outputs.upstream-repository-id == github.repository_id
permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
id-token: write # IMPORTANT: mandatory for trusted publishing & sigstore
# TAG is shared by the two release-existence steps. GITHUB_TOKEN stays scoped
# to the steps that need it rather than job-wide, so third-party actions in
# this job never see it in their environment.
env:
TAG: ${{ github.ref_name }}
# The required-reviewer pypi environment gates this job, so a human must
# approve before anything is created or published. Release creation and
# publishing all live in this one gated matrix, so a release needs a single
# approval: the groups are pending together and a reviewer approves them in
# one review (see the strategy comment below).
environment:
name: pypi
url: https://pypi.org/p/aiohttp
strategy:
# The PyPI publish and the Sigstore signing each mint one short-lived OIDC
# identity per job and reuse it for every file, so signing the whole dist
# set in a single job can outlast the token and fail partway through
# (pypa/gh-action-pypi-publish#307). Splitting the work across groups, each
# its own job with a fresh identity signing only its share, keeps every
# signing loop well under the token lifetime.
#
# The groups run in parallel and all target the pypi environment, so they
# are pending for approval at the same time and a reviewer approves them in
# a single review rather than one prompt per group. The first group
# (job-index 0) creates the GitHub Release and the others wait for it; each
# group only ever touches its own disjoint share of dists, so the
# concurrent Release asset uploads never collide. fail-fast is off and
# every step is idempotent, so a single failed group can be re-run on its
# own.
#
# Each label "N of M" self-encodes its own position and total, which is the
# only source of truth for the split. Keep `group` the sole matrix axis: an
# include/exclude entry would renumber strategy.job-index / job-total, but
# the label-derived split below stays correct as long as job-index 0 is the
# first label.
fail-fast: false
matrix:
group:
- 1 of 2
- 2 of 2
steps:
- name: Checkout
# Only the release-creating group needs the repo (create-release reads
# CHANGES.rst and aiohttp/__init__.py); the others only touch dist/.
if: ${{ strategy.job-index == 0 }}
uses: actions/checkout@v7
with:
submodules: true
- name: Login
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
- name: Download distributions
uses: actions/download-artifact@v8
with:
path: dist
pattern: dist-*
merge-multiple: true
- name: Select this group's distributions
# Keep only this group's share of the dists so the job signs a bounded set.
# index and count come from the "N of M" label, the single source of truth
# for the split; to add a group, extend the matrix list above (e.g.
# "1 of 3" .. "3 of 3").
#
# The split is fully deterministic: the same built dists always sort the
# same way (LC_ALL=C, byte order, independent of runner locale) and land in
# the same group, so re-running a single failed group reprocesses exactly
# its own share and never touches another group's dists.
id: group
shell: bash
env:
GROUP: ${{ matrix.group }}
run: |
set -euo pipefail
index=$(( ${GROUP%% of *} - 1 ))
count=${GROUP##* of }
shopt -s nullglob
mapfile -t all < <(printf '%s\n' dist/*.whl dist/*.tar.gz | LC_ALL=C sort)
i=0
inputs=()
for f in "${all[@]}"; do
if [ "$(( i % count ))" -eq "${index}" ]; then
inputs+=("${f}")
else
rm -f -- "${f}"
fi
i=$(( i + 1 ))
done
echo "Group ${GROUP} keeps ${#inputs[@]} of ${#all[@]} dist(s):"
printf ' %s\n' "${inputs[@]}"
echo "sigstore-inputs=${inputs[*]}" >> "${GITHUB_OUTPUT}"
- name: Check whether the GitHub Release already exists
# The first group owns Release creation. Skipping Make Release when the
# release already exists lets the job be re-run after a partial failure
# without hitting HTTP 422. Query the API and branch on the HTTP status,
# not on prose: a 404 means "create it", any other failure (auth,
# rate-limit, network) re-raises so the job fails loudly.
if: ${{ strategy.job-index == 0 }}
id: gh-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
--silent 2>err; then
echo 'exists=true' >> "${GITHUB_OUTPUT}"
elif grep -q 'HTTP 404' err; then
echo 'exists=false' >> "${GITHUB_OUTPUT}"
else
cat err >&2
exit 1
fi
- name: Make Release
# The first group creates the Release and uploads its share of the
# packages; the other groups add their packages and signatures below.
if: ${{ strategy.job-index == 0 && steps.gh-release.outputs.exists != 'true' }}
uses: aio-libs/create-release@v1.6.6
with:
changes_file: CHANGES.rst
name: aiohttp
version_file: aiohttp/__init__.py
github_token: ${{ secrets.GITHUB_TOKEN }}
dist_dir: dist
fix_issue_regex: >-
:issue:`(\d+)`
fix_issue_repl: >-
#\1
- name: Wait for the GitHub Release
# The other groups do not create the Release; they wait for the first
# group to create it before they publish or upload anything, so a failure
# to create the Release blocks the irreversible PyPI upload too. Only a
# 404 counts as "not yet"; any other API failure re-raises immediately
# instead of silently retrying for the whole timeout.
if: ${{ strategy.job-index != 0 }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
for _ in $(seq 1 150); do
if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
--silent 2>err; then
exit 0
fi
if ! grep -q 'HTTP 404' err; then
cat err >&2
exit 1
fi
sleep 2
done
echo "GitHub Release ${TAG} did not appear in time" >&2
exit 1
- name: Publish 🐍📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
# Allow re-running after a partial PyPI upload without failing on
# dists that a prior attempt already published.
skip-existing: true
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.4.0
with:
inputs: ${{ steps.group.outputs.sigstore-inputs }}
- name: Upload artifact signatures to GitHub Release
# Confusingly, this action also supports updating releases, not
# just creating them. This is what we want here, since the first group
# created the release above.
#
# The groups run this concurrently against the same release, which is safe:
# each group's files are a disjoint share, so asset names never collide, and
# with no body/name inputs the action preserves the existing release
# metadata (it writes back what it reads) rather than clearing it, so the
# concurrent metadata updates are identical no-ops. The Wait step above
# guarantees the release (with its notes) already exists first.
uses: softprops/action-gh-release@v3.0.2
with:
# dist/ holds this group's packages plus their Sigstore signatures.
files: dist/**