I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.

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