1 name: GitHub 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 contents: write
23 actions: write
24
25 env:
26 PROJECT_NAME: sigit
27 CARGO_TERM_COLOR: always
28
29 jobs:
30 build:
31 name: Build binary (${{ matrix.name }})
32 runs-on: ${{ matrix.runner }}
33
34 strategy:
35 fail-fast: false
36 matrix:
37 include:
38 - name: linux-amd64
39 runner: ubuntu-latest
40 target: x86_64-unknown-linux-gnu
41 - name: linux-arm64
42 runner: ubuntu-24.04-arm
43 target: aarch64-unknown-linux-gnu
44 - name: win-amd64
45 runner: windows-latest
46 target: x86_64-pc-windows-msvc
47 - name: win-arm64
48 runner: windows-latest
49 target: aarch64-pc-windows-msvc
50 - name: macos-amd64
51 runner: macos-26
52 target: x86_64-apple-darwin
53 - name: macos-arm64
54 runner: macos-26
55 target: aarch64-apple-darwin
56
57 steps:
58 - name: Checkout
59 uses: actions/checkout@v6
60 with:
61 ref: ${{ github.event.inputs.tag || github.ref }}
62
63 - name: Set the release version
64 shell: bash
65 run: |
66 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
67 release_version="${GITHUB_REF_NAME#v}"
68 else
69 release_version="${{ github.event.inputs.tag }}"
70 release_version="${release_version#v}"
71 fi
72 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
73
74 - name: Read Rust toolchain
75 shell: bash
76 run: |
77 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
78 if [ -z "$rust_toolchain" ]; then
79 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
80 exit 1
81 fi
82 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
83
84 - name: Install Rust toolchain
85 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
86 with:
87 toolchain: ${{ env.RUST_TOOLCHAIN }}
88 targets: ${{ matrix.target }}
89
90 - name: Setup Rust cache
91 uses: Swatinem/rust-cache@v2
92 with:
93 key: github-${{ matrix.target }}
94
95 - name: Build binary
96 shell: bash
97 run: cargo build --locked --release --target ${{ matrix.target }}
98
99 - name: Package binary
100 shell: bash
101 run: |
102 BIN_SUFFIX=""
103 if [[ "${{ matrix.runner }}" == "windows-latest" ]]; then
104 BIN_SUFFIX=".exe"
105 fi
106
107 BIN_OUTPUT="target/${{ matrix.target }}/release/${PROJECT_NAME}${BIN_SUFFIX}"
108 BIN_RELEASE="${PROJECT_NAME}-${{ matrix.name }}${BIN_SUFFIX}"
109
110 mkdir -p ./release
111 mv "${BIN_OUTPUT}" "./release/${BIN_RELEASE}"
112
113 - name: Package macOS tarball for Homebrew
114 if: contains(matrix.target, 'apple-darwin')
115 shell: bash
116 run: |
117 ARCHIVE_NAME="${PROJECT_NAME}-${{ matrix.name }}.tar.gz"
118
119 mkdir -p staging-brew
120 cp "./release/${PROJECT_NAME}-${{ matrix.name }}" "staging-brew/${PROJECT_NAME}"
121 strip "staging-brew/${PROJECT_NAME}"
122
123 tar -C staging-brew -czf "${ARCHIVE_NAME}" "${PROJECT_NAME}"
124
125 shasum -a 256 "${ARCHIVE_NAME}" | awk '{print $1}' > "${ARCHIVE_NAME}.sha256"
126
127 echo "Archive: ${ARCHIVE_NAME}"
128 echo "SHA256: $(cat "${ARCHIVE_NAME}.sha256")"
129
130 # Debian and RPM packages are built from the binary staged above rather
131 # than by re-invoking cargo, so each Linux target compiles once and gets
132 # packaged twice.
133 - name: Build Linux packages
134 if: contains(matrix.target, 'linux')
135 shell: bash
136 env:
137 NFPM_VERSION: "2.43.0"
138 run: |
139 case "$(uname -m)" in
140 x86_64) nfpm_arch="x86_64"; export PKG_ARCH="amd64" ;;
141 aarch64) nfpm_arch="arm64"; export PKG_ARCH="arm64" ;;
142 *) echo "Unsupported Linux build host $(uname -m)" >&2; exit 1 ;;
143 esac
144
145 curl -sSfL \
146 "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_Linux_${nfpm_arch}.tar.gz" \
147 | tar -xz -C /tmp nfpm
148
149 export PKG_VERSION="${RELEASE_VERSION}"
150 export PKG_BINARY="./release/${PROJECT_NAME}-${{ matrix.name }}"
151
152 # An empty version is worse than a hard failure here: nfpm accepts it
153 # and silently emits a "0.0.0~rc0" package that would be attached to
154 # the release. Same for a missing binary, which only surfaces as an
155 # opaque glob error. Fail loudly instead.
156 if [ -z "${PKG_VERSION}" ]; then
157 echo "RELEASE_VERSION is empty; refusing to build packages" >&2
158 exit 1
159 fi
160 if [ ! -f "${PKG_BINARY}" ]; then
161 echo "Staged binary ${PKG_BINARY} not found" >&2
162 exit 1
163 fi
164
165 # nfpm expands ${VAR} in most fields but NOT in contents.src, so the
166 # config is rendered up front and nfpm only ever sees literal values.
167 # envsubst comes from gettext-base, preinstalled on the Ubuntu runner
168 # images. The explicit variable list keeps it from clobbering any
169 # other shell-looking text added to the config later.
170 if ! command -v envsubst >/dev/null 2>&1; then
171 sudo apt-get update && sudo apt-get install -y gettext-base
172 fi
173
174 envsubst '${PKG_ARCH} ${PKG_VERSION} ${PKG_BINARY}' \
175 < packaging/nfpm.yaml > /tmp/nfpm.rendered.yaml
176
177 /tmp/nfpm package --config /tmp/nfpm.rendered.yaml --packager deb --target ./release/
178 /tmp/nfpm package --config /tmp/nfpm.rendered.yaml --packager rpm --target ./release/
179
180 # Guard against a packager silently producing nothing.
181 for ext in deb rpm; do
182 if ! compgen -G "./release/*.${ext}" >/dev/null; then
183 echo "No .${ext} produced" >&2
184 exit 1
185 fi
186 done
187
188 ls -la ./release/
189
190 # Homebrew has always had a checksum because the tap needs one. Scoop,
191 # winget, and the AUR PKGBUILD each need one too, and they consume the
192 # raw binaries rather than the macOS tarball, so every asset gets a
193 # sidecar. Windows runners use bash from Git for Windows, which has
194 # sha256sum; macOS only has shasum.
195 - name: Checksum release assets
196 shell: bash
197 run: |
198 cd release
199 for asset in *; do
200 case "${asset}" in *.sha256) continue ;; esac
201
202 if command -v sha256sum >/dev/null 2>&1; then
203 sha256sum "${asset}" | awk '{print $1}' > "${asset}.sha256"
204 else
205 shasum -a 256 "${asset}" | awk '{print $1}' > "${asset}.sha256"
206 fi
207
208 echo "${asset}: $(cat "${asset}.sha256")"
209 done
210
211 - name: Upload binary artifact
212 uses: actions/upload-artifact@v4
213 with:
214 name: binary-${{ matrix.name }}
215 path: release/*
216
217 - name: Upload macOS Homebrew artifacts
218 if: contains(matrix.target, 'apple-darwin')
219 uses: actions/upload-artifact@v4
220 with:
221 name: homebrew-${{ matrix.name }}
222 path: |
223 ${{ env.PROJECT_NAME }}-${{ matrix.name }}.tar.gz
224 ${{ env.PROJECT_NAME }}-${{ matrix.name }}.tar.gz.sha256
225
226 release:
227 name: Create GitHub Release
228 needs: build
229 runs-on: ubuntu-latest
230 if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
231
232 steps:
233 - name: Resolve tag
234 id: tag
235 shell: bash
236 run: |
237 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
238 echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
239 else
240 echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
241 fi
242
243 - name: Download binary artifacts
244 uses: actions/download-artifact@v4
245 with:
246 pattern: "binary-*"
247 path: release
248 merge-multiple: true
249
250 - name: Download Homebrew artifacts
251 uses: actions/download-artifact@v4
252 with:
253 pattern: "homebrew-*"
254 path: release
255 merge-multiple: true
256
257 - name: Release
258 uses: softprops/action-gh-release@v2
259 with:
260 tag_name: ${{ steps.tag.outputs.tag }}
261 files: release/*
262
263 # These channels all publish somewhere outside this repo (a tap, a Scoop
264 # bucket, microsoft/winget-pkgs, the AUR) and every one of them reads
265 # checksums off the release created above, so they hang off this job
266 # rather than firing on the tag directly.
267 - name: Trigger OS package manager releases
268 uses: actions/github-script@v7
269 with:
270 script: |
271 const tag = '${{ steps.tag.outputs.tag }}'
272 const workflows = [
273 'release-homebrew.yml',
274 'release-scoop.yml',
275 'release-winget.yml',
276 // New AUR account registration is temporarily closed
277 //'release-aur.yml',
278 ]
279
280 for (const workflow_id of workflows) {
281 try {
282 await github.rest.actions.createWorkflowDispatch({
283 owner: context.repo.owner,
284 repo: context.repo.repo,
285 workflow_id,
286 ref: 'main',
287 inputs: { tag },
288 })
289 console.log(`Dispatched ${workflow_id} for tag ${tag}`)
290 } catch (error) {
291 // One packaging channel being unconfigured (a missing secret,
292 // a bucket repo that does not exist yet) should not take the
293 // rest of the fan-out down with it.
294 core.warning(`Failed to dispatch ${workflow_id}: ${error.message}`)
295 }
296 }