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