1 name: PyPI Release
2
3 on:
4 push:
5 tags:
6 - "v*.*.*"
7 workflow_dispatch:
8 inputs:
9 tag:
10 description: "Release tag (e.g. v0.1.1)"
11 required: true
12
13 concurrency:
14 # A re-pushed or re-dispatched tag fires this a second time. Queue the
15 # duplicate behind the original instead of cancelling it: cancelling a run
16 # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
17 # just hits the "already published" check and exits clean.
18 group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
19 cancel-in-progress: false
20
21 permissions:
22 id-token: write
23 contents: read
24
25 env:
26 CARGO_TERM_COLOR: always
27
28 jobs:
29 build-wheels:
30 name: Build PyPI wheels
31 runs-on: ${{ matrix.build.OS }}
32 strategy:
33 fail-fast: false
34 matrix:
35 build:
36 - {
37 NAME: linux-x64-glibc,
38 OS: ubuntu-latest,
39 TARGET: x86_64-unknown-linux-gnu,
40 MANYLINUX: "2014",
41 }
42 - {
43 NAME: linux-arm64-glibc,
44 OS: ubuntu-24.04-arm,
45 TARGET: aarch64-unknown-linux-gnu,
46 MANYLINUX: "2014",
47 }
48 - {
49 NAME: win32-x64-msvc,
50 OS: windows-2022,
51 TARGET: x86_64-pc-windows-msvc,
52 }
53 - {
54 NAME: win32-arm64-msvc,
55 OS: windows-2022,
56 TARGET: aarch64-pc-windows-msvc,
57 }
58 - { NAME: darwin-x64, OS: macos-26, TARGET: x86_64-apple-darwin }
59 - { NAME: darwin-arm64, OS: macos-26, TARGET: aarch64-apple-darwin }
60 steps:
61 - name: Checkout
62 uses: actions/checkout@v6
63 with:
64 ref: ${{ github.event.inputs.tag || github.ref }}
65
66 - name: Set the release version
67 shell: bash
68 run: |
69 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
70 release_version="${GITHUB_REF_NAME#v}"
71 else
72 release_version="${{ github.event.inputs.tag }}"
73 release_version="${release_version#v}"
74 fi
75 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
76
77 - name: Read Rust toolchain
78 shell: bash
79 run: |
80 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
81 if [ -z "$rust_toolchain" ]; then
82 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
83 exit 1
84 fi
85 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
86
87 - name: Install Rust toolchain
88 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
89 with:
90 toolchain: ${{ env.RUST_TOOLCHAIN }}
91
92 - name: Install Rust target
93 shell: bash
94 run: |
95 rustup target add ${{ matrix.build.TARGET }} --toolchain ${{ env.RUST_TOOLCHAIN }}
96 rustup target list --installed
97
98 - name: Build wheel
99 uses: PyO3/maturin-action@v1
100 with:
101 target: ${{ matrix.build.TARGET }}
102 manylinux: ${{ matrix.build.MANYLINUX || 'off' }}
103 working-directory: pypi
104 args: --release --locked --compatibility pypi --out dist
105 before-script-linux: yum install -y perl-core
106
107 - name: Upload wheel artifact
108 uses: actions/upload-artifact@v4
109 with:
110 name: wheels-${{ matrix.build.NAME }}
111 path: pypi/dist/*
112
113 build-sdist:
114 name: Build source distribution
115 runs-on: ubuntu-latest
116 steps:
117 - name: Checkout
118 uses: actions/checkout@v6
119 with:
120 ref: ${{ github.event.inputs.tag || github.ref }}
121
122 - name: Read Rust toolchain
123 shell: bash
124 run: |
125 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
126 if [ -z "$rust_toolchain" ]; then
127 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
128 exit 1
129 fi
130 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
131
132 - name: Install Rust toolchain
133 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
134 with:
135 toolchain: ${{ env.RUST_TOOLCHAIN }}
136
137 - name: Build sdist
138 uses: PyO3/maturin-action@v1
139 with:
140 command: sdist
141 working-directory: pypi
142 args: --out dist
143
144 - name: Upload sdist artifact
145 uses: actions/upload-artifact@v4
146 with:
147 name: sdist
148 path: pypi/dist/*
149
150 publish-pypi:
151 name: Publish to PyPI
152 runs-on: ubuntu-latest
153 needs:
154 - build-wheels
155 - build-sdist
156 steps:
157 - name: Checkout
158 uses: actions/checkout@v6
159 with:
160 ref: ${{ github.event.inputs.tag || github.ref }}
161
162 - name: Set the release version
163 shell: bash
164 run: |
165 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
166 release_version="${GITHUB_REF_NAME#v}"
167 else
168 release_version="${{ github.event.inputs.tag }}"
169 release_version="${release_version#v}"
170 fi
171 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
172
173 - name: Download distribution artifacts
174 uses: actions/download-artifact@v4
175 with:
176 pattern: "*"
177 path: dist
178 merge-multiple: true
179
180 - name: Check whether release already exists on PyPI
181 id: pypi-check
182 shell: bash
183 run: |
184 # Branch on the status code rather than on curl's exit status: with
185 # -f every non-2xx answer looks alike, so a registry outage or a
186 # blocked request would read as "not published yet" and send the
187 # publish below into a version that is already there.
188 STATUS=$(curl -sS -o /dev/null -w '%{http_code}' \
189 --retry 3 --retry-delay 2 --retry-all-errors \
190 "https://pypi.org/pypi/sigit-code/${RELEASE_VERSION}/json")
191
192 case "$STATUS" in
193 200)
194 echo "sigit-code ${RELEASE_VERSION} already exists on PyPI, skipping publish"
195 echo "exists=true" >> "$GITHUB_OUTPUT"
196 ;;
197 404)
198 echo "exists=false" >> "$GITHUB_OUTPUT"
199 ;;
200 *)
201 echo "Unexpected ${STATUS} from PyPI while checking whether sigit-code ${RELEASE_VERSION} is published" >&2
202 exit 1
203 ;;
204 esac
205
206 - name: Publish distribution
207 if: steps.pypi-check.outputs.exists != 'true'
208 uses: pypa/gh-action-pypi-publish@release/v1
209 with:
210 skip-existing: true