@@ -753,7 +753,7 @@ jobs:
753753 path : ./wheelhouse/*.whl
754754
755755 deploy :
756- name : Deploy (${{ matrix.group }})
756+ name : Deploy
757757 needs :
758758 - build-tarball
759759 - build-wheels
@@ -766,54 +766,12 @@ jobs:
766766 contents : write # IMPORTANT: mandatory for making GitHub Releases
767767 id-token : write # IMPORTANT: mandatory for trusted publishing & sigstore
768768
769- # TAG is shared by the two release-existence steps. GITHUB_TOKEN stays scoped
770- # to the steps that need it rather than job-wide, so third-party actions in
771- # this job never see it in their environment.
772- env :
773- TAG : ${{ github.ref_name }}
774-
775- # The required-reviewer pypi environment gates this job, so a human must
776- # approve before anything is created or published. Release creation and
777- # publishing all live in this one gated matrix, so a release needs a single
778- # approval: the groups are pending together and a reviewer approves them in
779- # one review (see the strategy comment below).
780769 environment :
781770 name : pypi
782771 url : https://pypi.org/p/aiohttp
783772
784- strategy :
785- # The PyPI publish and the Sigstore signing each mint one short-lived OIDC
786- # identity per job and reuse it for every file, so signing the whole dist
787- # set in a single job can outlast the token and fail partway through
788- # (pypa/gh-action-pypi-publish#307). Splitting the work across groups, each
789- # its own job with a fresh identity signing only its share, keeps every
790- # signing loop well under the token lifetime.
791- #
792- # The groups run in parallel and all target the pypi environment, so they
793- # are pending for approval at the same time and a reviewer approves them in
794- # a single review rather than one prompt per group. The first group
795- # (job-index 0) creates the GitHub Release and the others wait for it; each
796- # group only ever touches its own disjoint share of dists, so the
797- # concurrent Release asset uploads never collide. fail-fast is off and
798- # every step is idempotent, so a single failed group can be re-run on its
799- # own.
800- #
801- # Each label "N of M" self-encodes its own position and total, which is the
802- # only source of truth for the split. Keep `group` the sole matrix axis: an
803- # include/exclude entry would renumber strategy.job-index / job-total, but
804- # the label-derived split below stays correct as long as job-index 0 is the
805- # first label.
806- fail-fast : false
807- matrix :
808- group :
809- - 1 of 2
810- - 2 of 2
811-
812773 steps :
813774 - name : Checkout
814- # Only the release-creating group needs the repo (create-release reads
815- # CHANGES.rst and aiohttp/__init__.py); the others only touch dist/.
816- if : ${{ strategy.job-index == 0 }}
817775 uses : actions/checkout@v7
818776 with :
819777 submodules : true
@@ -826,64 +784,32 @@ jobs:
826784 path : dist
827785 pattern : dist-*
828786 merge-multiple : true
829- - name : Select this group's distributions
830- # Keep only this group's share of the dists so the job signs a bounded set.
831- # index and count come from the "N of M" label, the single source of truth
832- # for the split; to add a group, extend the matrix list above (e.g.
833- # "1 of 3" .. "3 of 3").
834- #
835- # The split is fully deterministic: the same built dists always sort the
836- # same way (LC_ALL=C, byte order, independent of runner locale) and land in
837- # the same group, so re-running a single failed group reprocesses exactly
838- # its own share and never touches another group's dists.
839- id : group
840- shell : bash
841- env :
842- GROUP : ${{ matrix.group }}
787+ - name : Collected dists
843788 run : |
844- set -euo pipefail
845- index=$(( ${GROUP%% of *} - 1 ))
846- count=${GROUP##* of }
847- shopt -s nullglob
848- mapfile -t all < <(printf '%s\n' dist/*.whl dist/*.tar.gz | LC_ALL=C sort)
849- i=0
850- inputs=()
851- for f in "${all[@]}"; do
852- if [ "$(( i % count ))" -eq "${index}" ]; then
853- inputs+=("${f}")
854- else
855- rm -f -- "${f}"
856- fi
857- i=$(( i + 1 ))
858- done
859- echo "Group ${GROUP} keeps ${#inputs[@]} of ${#all[@]} dist(s):"
860- printf ' %s\n' "${inputs[@]}"
861- echo "sigstore-inputs=${inputs[*]}" >> "${GITHUB_OUTPUT}"
789+ tree dist
862790 - name : Check whether the GitHub Release already exists
863- # The first group owns Release creation. Skipping Make Release when the
864- # release already exists lets the job be re-run after a partial failure
865- # without hitting HTTP 422. Query the API and branch on the HTTP status,
866- # not on prose: a 404 means "create it", any other failure (auth,
867- # rate-limit, network) re-raises so the job fails loudly.
868- if : ${{ strategy.job-index == 0 }}
791+ # Allows re-running the deploy job after a partial failure (e.g. PyPI
792+ # upload error) without the Make Release step failing with HTTP 422
793+ # because the tag/release was created on a prior attempt. Treat
794+ # only the literal `release not found` reply as "does not exist";
795+ # other failures (auth, rate-limit, network) re-raise so the job
796+ # fails loudly instead of falling through to Make Release.
869797 id : gh-release
870798 env :
871799 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
800+ TAG : ${{ github.ref_name }}
872801 run : |
873- set -euo pipefail
874- if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
875- --silent 2>err; then
802+ if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \
803+ >/dev/null 2>err; then
876804 echo 'exists=true' >> "${GITHUB_OUTPUT}"
877- elif grep -q 'HTTP 404 ' err; then
805+ elif grep -qx 'release not found ' err; then
878806 echo 'exists=false' >> "${GITHUB_OUTPUT}"
879807 else
880808 cat err >&2
881809 exit 1
882810 fi
883811 - name : Make Release
884- # The first group creates the Release and uploads its share of the
885- # packages; the other groups add their packages and signatures below.
886- if : ${{ strategy.job-index == 0 && steps.gh-release.outputs.exists != 'true' }}
812+ if : steps.gh-release.outputs.exists != 'true'
887813 uses : aio-libs/create-release@v1.6.6
888814 with :
889815 changes_file : CHANGES.rst
@@ -895,52 +821,28 @@ jobs:
895821 :issue:`(\d+)`
896822 fix_issue_repl : >-
897823 #\1
898- - name : Wait for the GitHub Release
899- # The other groups do not create the Release; they wait for the first
900- # group to create it before they publish or upload anything, so a failure
901- # to create the Release blocks the irreversible PyPI upload too. Only a
902- # 404 counts as "not yet"; any other API failure re-raises immediately
903- # instead of silently retrying for the whole timeout.
904- if : ${{ strategy.job-index != 0 }}
905- env :
906- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
907- run : |
908- set -euo pipefail
909- for _ in $(seq 1 150); do
910- if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
911- --silent 2>err; then
912- exit 0
913- fi
914- if ! grep -q 'HTTP 404' err; then
915- cat err >&2
916- exit 1
917- fi
918- sleep 2
919- done
920- echo "GitHub Release ${TAG} did not appear in time" >&2
921- exit 1
922- - name : Publish 🐍📦 to PyPI
824+
825+ - name : >-
826+ Publish 🐍📦 to PyPI
923827 uses: pypa/gh-action-pypi-publish@release/v1
924828 with:
925- # Allow re-running after a partial PyPI upload without failing on
926- # dists that a prior attempt already published.
829+ # Allow re-running the deploy job after a partial PyPI upload
830+ # without failing on dists that were already published.
927831 skip-existing: true
832+
928833 - name : Sign the dists with Sigstore
929- uses : sigstore/gh-action-sigstore-python@v3.4 .0
834+ uses : sigstore/gh-action-sigstore-python@v3.5 .0
930835 with :
931- inputs : ${{ steps.group.outputs.sigstore-inputs }}
836+ inputs : >-
837+ ./dist/*.tar.gz
838+ ./dist/*.whl
839+
932840 - name : Upload artifact signatures to GitHub Release
933841 # Confusingly, this action also supports updating releases, not
934- # just creating them. This is what we want here, since the first group
842+ # just creating them. This is what we want here, since we've manually
935843 # created the release above.
936- #
937- # The groups run this concurrently against the same release, which is safe:
938- # each group's files are a disjoint share, so asset names never collide, and
939- # with no body/name inputs the action preserves the existing release
940- # metadata (it writes back what it reads) rather than clearing it, so the
941- # concurrent metadata updates are identical no-ops. The Wait step above
942- # guarantees the release (with its notes) already exists first.
943844 uses : softprops/action-gh-release@v3.0.2
944845 with :
945- # dist/ holds this group's packages plus their Sigstore signatures.
846+ # dist/ contains the built packages plus the Sigstore signatures
847+ # and certificates emitted alongside them by the signing step above.
946848 files : dist/**
0 commit comments