1 name: NuGet Release
2
3 on:
4 push:
5 tags:
6 - "v*.*.*"
7 workflow_dispatch:
8 inputs:
9 tag:
10 description: "Release tag (e.g. v1.5.2)"
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 contents: read
23
24 env:
25 CARGO_TERM_COLOR: always
26 DOTNET_VERSION: 8.0.x
27 NUGET_PACKAGE_ID: SiGit.Code
28
29 jobs:
30 build-native-binaries:
31 name: Build native binary (${{ matrix.build.NAME }})
32 runs-on: ${{ matrix.build.OS }}
33 strategy:
34 fail-fast: false
35 matrix:
36 build:
37 - {
38 NAME: linux-x64,
39 OS: ubuntu-latest,
40 TARGET: x86_64-unknown-linux-gnu,
41 }
42 - {
43 NAME: linux-arm64,
44 OS: ubuntu-24.04-arm,
45 TARGET: aarch64-unknown-linux-gnu,
46 }
47 - {
48 NAME: windows-x64,
49 OS: windows-2022,
50 TARGET: x86_64-pc-windows-msvc,
51 }
52 - {
53 NAME: windows-arm64,
54 OS: windows-2022,
55 TARGET: aarch64-pc-windows-msvc,
56 }
57 - { NAME: darwin-x64, OS: macos-26, TARGET: x86_64-apple-darwin }
58 - { NAME: darwin-arm64, OS: macos-26, TARGET: aarch64-apple-darwin }
59 steps:
60 - name: Checkout
61 uses: actions/checkout@v6
62 with:
63 ref: ${{ github.event.inputs.tag || github.ref }}
64
65 - name: Read Rust toolchain
66 shell: bash
67 run: |
68 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
69 if [ -z "$rust_toolchain" ]; then
70 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
71 exit 1
72 fi
73 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
74
75 - name: Install Rust toolchain
76 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
77 with:
78 toolchain: ${{ env.RUST_TOOLCHAIN }}
79
80 - name: Install Rust target
81 shell: bash
82 run: |
83 rustup target add ${{ matrix.build.TARGET }} --toolchain ${{ env.RUST_TOOLCHAIN }}
84 rustup target list --installed
85
86 - name: Setup Rust cache
87 uses: Swatinem/rust-cache@v2
88 with:
89 key: nuget-${{ matrix.build.TARGET }}
90
91 - name: Build binary
92 shell: bash
93 run: cargo build --locked --release -p sigit --target ${{ matrix.build.TARGET }}
94
95 - name: Prepare artifact
96 shell: bash
97 run: |
98 executable_name="sigit"
99 if [[ "${{ matrix.build.OS }}" == "windows-2022" ]]; then
100 executable_name="sigit.exe"
101 fi
102
103 mkdir -p "nuget-artifacts/native/${{ matrix.build.NAME }}"
104 cp "target/${{ matrix.build.TARGET }}/release/${executable_name}" \
105 "nuget-artifacts/native/${{ matrix.build.NAME }}/${executable_name}"
106
107 - name: Upload native artifact
108 uses: actions/upload-artifact@v4
109 with:
110 name: nuget-native-${{ matrix.build.NAME }}
111 path: nuget-artifacts/native
112
113 pack-nuget:
114 name: Pack NuGet .NET tool
115 runs-on: ubuntu-latest
116 needs: build-native-binaries
117 steps:
118 - name: Checkout
119 uses: actions/checkout@v6
120 with:
121 ref: ${{ github.event.inputs.tag || github.ref }}
122
123 - name: Set the release version
124 shell: bash
125 run: |
126 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
127 release_version="${GITHUB_REF_NAME#v}"
128 else
129 release_version="${{ github.event.inputs.tag }}"
130 release_version="${release_version#v}"
131 fi
132 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
133
134 - name: Download native artifacts
135 uses: actions/download-artifact@v4
136 with:
137 pattern: nuget-native-*
138 path: nuget/sigit/native
139 merge-multiple: true
140
141 - name: Setup .NET SDK
142 uses: actions/setup-dotnet@v4
143 with:
144 dotnet-version: ${{ env.DOTNET_VERSION }}
145
146 - name: Pack .NET tool
147 shell: bash
148 run: |
149 dotnet pack nuget/sigit/SiGit.Code.csproj \
150 --configuration Release \
151 --output nuget/dist \
152 -p:PackageVersion=${RELEASE_VERSION}
153
154 # All six binaries ship in one package, so this sits closer to nuget.org's
155 # 250 MB ceiling than the single-platform npm and PyPI artifacts do. Fail
156 # here with a clear message rather than at `dotnet nuget push`, where the
157 # error is just "the package file exceeds the size limit".
158 - name: Check package size against the NuGet limit
159 shell: bash
160 run: |
161 package="$(ls nuget/dist/*.nupkg | head -n 1)"
162 size_bytes="$(wc -c < "${package}")"
163 limit_bytes=$((250 * 1024 * 1024))
164
165 echo "Package: ${package}"
166 echo "Size: $((size_bytes / 1024 / 1024)) MiB"
167
168 if [ "${size_bytes}" -gt "${limit_bytes}" ]; then
169 echo "::error::${package} is over nuget.org's 250 MB package limit. Split the tool into RID-specific packages (.NET 10 DotnetToolRidPackage) instead of bundling every target." >&2
170 exit 1
171 fi
172
173 - name: Upload package artifact
174 uses: actions/upload-artifact@v4
175 with:
176 name: nuget-package
177 path: nuget/dist/*.nupkg
178
179 smoke-test:
180 name: Smoke test (${{ matrix.os }})
181 runs-on: ${{ matrix.os }}
182 needs: pack-nuget
183 strategy:
184 fail-fast: false
185 matrix:
186 os:
187 - ubuntu-latest
188 - windows-2022
189 - macos-26
190 steps:
191 - name: Checkout
192 uses: actions/checkout@v6
193 with:
194 ref: ${{ github.event.inputs.tag || github.ref }}
195
196 - name: Set the release version
197 shell: bash
198 run: |
199 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
200 release_version="${GITHUB_REF_NAME#v}"
201 else
202 release_version="${{ github.event.inputs.tag }}"
203 release_version="${release_version#v}"
204 fi
205 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
206
207 - name: Download package artifact
208 uses: actions/download-artifact@v4
209 with:
210 name: nuget-package
211 path: nuget/dist
212
213 - name: Setup .NET SDK
214 uses: actions/setup-dotnet@v4
215 with:
216 dotnet-version: ${{ env.DOTNET_VERSION }}
217
218 - name: Install tool from local package
219 shell: pwsh
220 run: |
221 $packageSource = (Resolve-Path "nuget/dist").Path
222 @"
223 <?xml version="1.0" encoding="utf-8"?>
224 <configuration>
225 <packageSources>
226 <clear />
227 <add key="local" value="$packageSource" />
228 </packageSources>
229 </configuration>
230 "@ | Set-Content -Path "nuget/NuGet.Config"
231
232 dotnet tool install `
233 --tool-path ".tool" `
234 --configfile "nuget/NuGet.Config" `
235 --version "$env:RELEASE_VERSION" `
236 "$env:NUGET_PACKAGE_ID"
237
238 - name: Verify bundled native binary
239 shell: pwsh
240 run: |
241 $runtimeIdentifier = if ($IsWindows) {
242 if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "windows-arm64" } else { "windows-x64" }
243 } elseif ($IsMacOS) {
244 if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "darwin-arm64" } else { "darwin-x64" }
245 } else {
246 if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "linux-arm64" } else { "linux-x64" }
247 }
248
249 $binaryName = if ($IsWindows) { "sigit.exe" } else { "sigit" }
250 $nativeBinary = Get-ChildItem ".tool/.store" -Recurse -File | Where-Object {
251 $_.FullName -like "*native*${runtimeIdentifier}*${binaryName}"
252 } | Select-Object -First 1
253
254 if (-not $nativeBinary) {
255 Write-Error "The sigit native binary for ${runtimeIdentifier} was not bundled in the installed .NET tool package."
256 }
257
258 Write-Host "Found bundled native binary at $($nativeBinary.FullName)"
259
260 publish-nuget:
261 name: Publish to NuGet
262 runs-on: ubuntu-latest
263 needs: smoke-test
264 # Trusted Publishing exchanges a GitHub OIDC token for a short-lived NuGet
265 # API key, so no long-lived NUGET_API_KEY secret is stored. Requires the
266 # `sigit-nuget` trusted publisher policy on nuget.org (owner getsigit, repo
267 # sigit, workflow release-nuget.yml).
268 permissions:
269 contents: read
270 id-token: write
271 steps:
272 - name: Set the release version
273 shell: bash
274 run: |
275 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
276 release_version="${GITHUB_REF_NAME#v}"
277 else
278 release_version="${{ github.event.inputs.tag }}"
279 release_version="${release_version#v}"
280 fi
281 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
282
283 - name: Download package artifact
284 uses: actions/download-artifact@v4
285 with:
286 name: nuget-package
287 path: nuget/dist
288
289 - name: Setup .NET SDK
290 uses: actions/setup-dotnet@v4
291 with:
292 dotnet-version: ${{ env.DOTNET_VERSION }}
293
294 - name: Check whether release already exists on NuGet
295 id: nuget-check
296 shell: bash
297 run: |
298 package_index_url="https://api.nuget.org/v3-flatcontainer/sigit.code/index.json"
299
300 # Keep the fetch and the version match separate. Piping curl into
301 # grep hides a failed fetch behind an empty match, so an outage or a
302 # blocked request would read as "not published yet" and send the
303 # publish below into a version that is already there. A 404 here
304 # means the package itself is not on NuGet, which is the first
305 # release and a real "not published".
306 index=$(mktemp)
307 STATUS=$(curl -sS -o "${index}" -w '%{http_code}' \
308 --retry 3 --retry-delay 2 --retry-all-errors "${package_index_url}")
309
310 case "$STATUS" in
311 200)
312 if grep -F "\"${RELEASE_VERSION}\"" "${index}" >/dev/null; then
313 echo "${NUGET_PACKAGE_ID} ${RELEASE_VERSION} already exists on NuGet, skipping publish"
314 echo "exists=true" >> "$GITHUB_OUTPUT"
315 else
316 echo "exists=false" >> "$GITHUB_OUTPUT"
317 fi
318 ;;
319 404)
320 echo "${NUGET_PACKAGE_ID} is not on NuGet yet, publishing its first version"
321 echo "exists=false" >> "$GITHUB_OUTPUT"
322 ;;
323 *)
324 echo "Unexpected ${STATUS} from NuGet while checking whether ${NUGET_PACKAGE_ID} ${RELEASE_VERSION} is published" >&2
325 exit 1
326 ;;
327 esac
328
329 # Exchange the workflow's OIDC token for a short-lived NuGet API key. The
330 # key is written to the environment for the subsequent `dotnet nuget push`
331 # via the NUGET_API_KEY variable this action sets.
332 - name: NuGet trusted-publishing login
333 if: steps.nuget-check.outputs.exists != 'true'
334 uses: NuGet/login@v1
335 id: nuget-login
336 with:
337 user: setoelkahfi
338
339 - name: Publish package
340 if: steps.nuget-check.outputs.exists != 'true'
341 shell: bash
342 run: |
343 dotnet nuget push nuget/dist/*.nupkg \
344 --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
345 --source https://api.nuget.org/v3/index.json \
346 --skip-duplicate