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