I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.
development
| 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 | } |