claude/linked-issue-acp-tui-u4vvpa
claude/github-issue-40-0au5ua
claude/linked-issue-acp-tui-u4vvpa
claude/minor-release-cut-vkugow
claude/sigit-acp-local-chat-cx6380
claude/sigit-cloud-agent-expansion-reox0a
claude/tool-permission-system
claude/zen-feynman-0u78dk
development
feature/acp-cancel-request
feature/acp-session-list-cursor
feature/acp-v1-conformance
feature/agent-tools-multiedit-glob-todos-remember
feature/cargo-workspace
feature/hooks
feature/load-local-model-explicitly
feature/sigit-agent-profile
feature/sigit-code-cloud
feature/xcode-mcp-server
fix/acp-replay-loaded-session-history
fix/acp-replay-reasoning-and-attachments
fix/dsml-doubled-bar-markers
main
release/v1.5.10
release/v1.5.6
release/v1.6.1
| 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: Checkout tag |
| 244 | # The release job otherwise only downloads artifacts; the changelog |
| 245 | # has to be read from the tree at the tag being released. |
| 246 | uses: actions/checkout@v6 |
| 247 | with: |
| 248 | ref: ${{ steps.tag.outputs.tag }} |
| 249 | |
| 250 | - name: Compose release notes |
| 251 | # The release page carries the section of CHANGELOG.md for this |
| 252 | # version, not a bare "Full changelog" default. A missing section is |
| 253 | # a hard failure: publishing a release whose notes don't match its |
| 254 | # changelog is worse than stopping the job. |
| 255 | shell: bash |
| 256 | run: | |
| 257 | version="${{ steps.tag.outputs.tag }}" |
| 258 | version="${version#v}" |
| 259 | section="$(awk -v ver="${version}" ' |
| 260 | $0 == "## " ver { found = 1; next } |
| 261 | found && /^## / { exit } |
| 262 | found { print } |
| 263 | ' CHANGELOG.md)" |
| 264 | if [ -z "${section//[$' \t\n']/}" ]; then |
| 265 | echo "No ## ${version} section found in CHANGELOG.md" >&2 |
| 266 | exit 1 |
| 267 | fi |
| 268 | { |
| 269 | printf '%s\n' "${section}" |
| 270 | printf '\n---\n\n' |
| 271 | printf 'Full changelog: https://github.com/%s/blob/%s/CHANGELOG.md\n' \ |
| 272 | "${GITHUB_REPOSITORY}" "${{ steps.tag.outputs.tag }}" |
| 273 | printf '\n---\n\n' |
| 274 | printf 'About siGit Code: [sigit.si/code](https://sigit.si/code)\n' |
| 275 | } > release_notes.md |
| 276 | cat release_notes.md |
| 277 | |
| 278 | - name: Download binary artifacts |
| 279 | uses: actions/download-artifact@v4 |
| 280 | with: |
| 281 | pattern: "binary-*" |
| 282 | path: release |
| 283 | merge-multiple: true |
| 284 | |
| 285 | - name: Download Homebrew artifacts |
| 286 | uses: actions/download-artifact@v4 |
| 287 | with: |
| 288 | pattern: "homebrew-*" |
| 289 | path: release |
| 290 | merge-multiple: true |
| 291 | |
| 292 | - name: Release |
| 293 | uses: softprops/action-gh-release@v2 |
| 294 | with: |
| 295 | tag_name: ${{ steps.tag.outputs.tag }} |
| 296 | body_path: release_notes.md |
| 297 | files: release/* |
| 298 | |
| 299 | # These channels all publish somewhere outside this repo (a tap, a Scoop |
| 300 | # bucket, microsoft/winget-pkgs, the AUR) and every one of them reads |
| 301 | # checksums off the release created above, so they hang off this job |
| 302 | # rather than firing on the tag directly. |
| 303 | - name: Trigger OS package manager releases |
| 304 | uses: actions/github-script@v7 |
| 305 | with: |
| 306 | script: | |
| 307 | const tag = '${{ steps.tag.outputs.tag }}' |
| 308 | const workflows = [ |
| 309 | 'release-homebrew.yml', |
| 310 | 'release-scoop.yml', |
| 311 | 'release-winget.yml', |
| 312 | // New AUR account registration is temporarily closed |
| 313 | //'release-aur.yml', |
| 314 | ] |
| 315 | |
| 316 | for (const workflow_id of workflows) { |
| 317 | try { |
| 318 | await github.rest.actions.createWorkflowDispatch({ |
| 319 | owner: context.repo.owner, |
| 320 | repo: context.repo.repo, |
| 321 | workflow_id, |
| 322 | ref: 'main', |
| 323 | inputs: { tag }, |
| 324 | }) |
| 325 | console.log(`Dispatched ${workflow_id} for tag ${tag}`) |
| 326 | } catch (error) { |
| 327 | // One packaging channel being unconfigured (a missing secret, |
| 328 | // a bucket repo that does not exist yet) should not take the |
| 329 | // rest of the fan-out down with it. |
| 330 | core.warning(`Failed to dispatch ${workflow_id}: ${error.message}`) |
| 331 | } |
| 332 | } |