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
+ steps:
257
+ - name: Set the release version
258
+ shell: bash
259
+ run: |
260
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
261
+ release_version="${GITHUB_REF_NAME#v}"
262
+ else
263
+ release_version="${{ github.event.inputs.tag }}"
264
+ release_version="${release_version#v}"
265
+ fi
266
+ echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
267
+
268
+ - name: Download package artifact
269
+ uses: actions/download-artifact@v4
270
+ with:
271
+ name: nuget-package
272
+ path: nuget/dist
273
+
274
+ - name: Setup .NET SDK
275
+ uses: actions/setup-dotnet@v4
276
+ with:
277
+ dotnet-version: ${{ env.DOTNET_VERSION }}
278
+
279
+ - name: Check whether release already exists on NuGet
280
+ id: nuget-check
281
+ shell: bash
282
+ run: |
283
+ package_index_url="https://api.nuget.org/v3-flatcontainer/sigit.code/index.json"
284
+ if curl -fsS "${package_index_url}" | grep -F "\"${RELEASE_VERSION}\"" >/dev/null; then
285
+ echo "${NUGET_PACKAGE_ID} ${RELEASE_VERSION} already exists on NuGet, skipping publish"
286
+ echo "exists=true" >> "$GITHUB_OUTPUT"
287
+ else
288
+ echo "exists=false" >> "$GITHUB_OUTPUT"
289
+ fi
290
+
291
+ - name: Publish package
292
+ if: steps.nuget-check.outputs.exists != 'true'
293
+ env:
294
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
295
+ shell: bash
296
+ run: |
297
+ dotnet nuget push nuget/dist/*.nupkg \
298
+ --api-key "${NUGET_API_KEY}" \
299
+ --source https://api.nuget.org/v3/index.json \
300
+ --skip-duplicate