Skip to content

homebrew: ship prebuilt macos binaries instead of source build #23

homebrew: ship prebuilt macos binaries instead of source build

homebrew: ship prebuilt macos binaries instead of source build #23

Workflow file for this run

name: release

Check failure on line 1 in .github/workflows/release.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/release.yml

Invalid workflow file

(Line: 438, Col: 13): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.WINGET_TOKEN != ''
# Streaming release: the GitHub release is created up front, then every job
# uploads ITS artifact the moment it's built (gh release upload --clobber).
# A slow leg (aarch64/i686 under QEMU, or the windows vcpkg build) never blocks
# the release or the artifacts that are already done.
#
# Triggers:
# • pushing a tag `vX.Y.Z` → builds that tag, publishes the release
# • manual run (workflow_dispatch) → builds the chosen ref, publishes `tag`
#
# Artifacts: agentty-linux-{x86_64,aarch64,i686}, agentty-macos-{arm64,x86_64},
# agentty-windows-x86_64.exe, agentty-windows-x86_64.msi (signed installer),
# agentty_<v>_{amd64,arm64}.deb, agentty-<v>-1.{x86_64,aarch64}.rpm,
# agentty-bin-<v>-1-x86_64.pkg.tar.zst, agentty-<v>.tar.gz, SHA256SUMS.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish (e.g. v0.1.0)'
required: true
default: v0.1.0
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
GH_TOKEN: ${{ github.token }}
jobs:
# Create/refresh the release immediately + upload the source tarball.
prepare:
name: prepare release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init maya submodule (HTTPS)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --recursive
- name: Resolve version + tag
id: meta
run: |
V=$(grep -m1 'project(agentty' CMakeLists.txt | sed -E 's/.*VERSION ([0-9.]+).*/\1/')
echo "version=$V" >> "$GITHUB_OUTPUT"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi
- name: Ensure release exists + upload source tarball
env:
V: ${{ steps.meta.outputs.version }}
TAG: ${{ steps.meta.outputs.tag }}
run: |
gh release view "$TAG" >/dev/null 2>&1 \
|| gh release create "$TAG" --title "$TAG" \
--notes "See [CHANGELOG.md](https://github.com/1ay1/agentty/blob/master/CHANGELOG.md)."
pipx run git-archive-all "agentty-${V}.tar.gz"
gh release upload "$TAG" "agentty-${V}.tar.gz" --clobber
# Linux static (musl) binaries — one job per arch, NOT a matrix. A matrix
# job only reports complete when its slowest leg finishes (aarch64 under
# QEMU, ~13 min), and `needs:` can't target a single matrix leg — so a
# matrix would re-gate the fast packages behind the slow build, and a failed
# aarch64 leg would block x86 packaging entirely. Separate jobs let the x86
# binary + packages publish in ~5 min while aarch64 streams in later, and
# isolate each arch's failures from the others.
build-linux-x86_64:
name: linux x86_64
needs: prepare
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init maya submodule (HTTPS)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --recursive
- name: Build static binary (Alpine 3.21 / musl / GCC 14)
run: |
docker run --rm --platform linux/amd64 \
-e CMARCH='avx2' \
-v "$PWD":/src -w /src alpine:3.21 sh -c '
set -ex
apk add --no-cache build-base cmake ninja git linux-headers pkgconfig \
openssl-dev openssl-libs-static nghttp2-dev nghttp2-static zlib-static
git config --global --add safe.directory /src
rm -rf build-rel
cmake -S . -B build-rel -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_FULLY_STATIC=ON \
-DAGENTTY_AUTO_PULL_MAYA=OFF -DAGENTTY_USE_MIMALLOC=OFF \
-DAGENTTY_ARCH="$CMARCH"
cmake --build build-rel -j"$(nproc)"
strip build-rel/agentty
cp build-rel/agentty agentty-linux-x86_64
'
- name: Upload to release + stash for packaging
run: gh release upload "$TAG" agentty-linux-x86_64 --clobber
- uses: actions/upload-artifact@v4
with:
name: bin-linux-x86_64
path: agentty-linux-x86_64
if-no-files-found: error
build-linux-aarch64:
name: linux aarch64
needs: prepare
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init maya submodule (HTTPS)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Build static binary (Alpine 3.21 / musl / GCC 14)
run: |
docker run --rm --platform linux/arm64 \
-e CMARCH='native' \
-v "$PWD":/src -w /src alpine:3.21 sh -c '
set -ex
apk add --no-cache build-base cmake ninja git linux-headers pkgconfig \
openssl-dev openssl-libs-static nghttp2-dev nghttp2-static zlib-static
git config --global --add safe.directory /src
rm -rf build-rel
cmake -S . -B build-rel -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_FULLY_STATIC=ON \
-DAGENTTY_AUTO_PULL_MAYA=OFF -DAGENTTY_USE_MIMALLOC=OFF \
-DAGENTTY_ARCH="$CMARCH"
cmake --build build-rel -j"$(nproc)"
strip build-rel/agentty
cp build-rel/agentty agentty-linux-aarch64
'
- name: Upload to release + stash for packaging
run: gh release upload "$TAG" agentty-linux-aarch64 --clobber
- uses: actions/upload-artifact@v4
with:
name: bin-linux-aarch64
path: agentty-linux-aarch64
if-no-files-found: error
build-linux-i686:
name: linux i686
needs: prepare
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init maya submodule (HTTPS)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --recursive
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Build static binary (Alpine 3.21 / musl / GCC 14)
run: |
docker run --rm --platform linux/386 \
-e CMARCH='sse2' \
-v "$PWD":/src -w /src alpine:3.21 sh -c '
set -ex
apk add --no-cache build-base cmake ninja git linux-headers pkgconfig \
openssl-dev openssl-libs-static nghttp2-dev nghttp2-static zlib-static
git config --global --add safe.directory /src
rm -rf build-rel
cmake -S . -B build-rel -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_FULLY_STATIC=ON \
-DAGENTTY_AUTO_PULL_MAYA=OFF -DAGENTTY_USE_MIMALLOC=OFF \
-DAGENTTY_ARCH="$CMARCH"
cmake --build build-rel -j"$(nproc)"
strip build-rel/agentty
cp build-rel/agentty agentty-linux-i686
'
- name: Upload to release
run: gh release upload "$TAG" agentty-linux-i686 --clobber
build-windows:
name: windows x86_64
needs: prepare
runs-on: windows-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init maya submodule (HTTPS)
shell: bash
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --recursive
- name: Install deps (vcpkg static)
shell: pwsh
run: |
git -C "$env:VCPKG_INSTALLATION_ROOT" fetch --depth 1 origin master
git -C "$env:VCPKG_INSTALLATION_ROOT" reset --hard FETCH_HEAD
& "$env:VCPKG_INSTALLATION_ROOT\bootstrap-vcpkg.bat"
& "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install openssl nghttp2 --triplet x64-windows-static
- name: Configure + build
shell: pwsh
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release `
-DAGENTTY_STANDALONE=ON -DAGENTTY_AUTO_PULL_MAYA=OFF `
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" `
-DVCPKG_TARGET_TRIPLET=x64-windows-static
cmake --build build --config Release -j
- name: Stage binary
shell: pwsh
run: |
$exe = Get-ChildItem -Recurse build -Filter agentty.exe | Select-Object -First 1
Copy-Item $exe.FullName agentty-windows-x86_64.exe
- name: Build (and sign) MSI installer
shell: pwsh
env:
VERSION: ${{ needs.prepare.outputs.version }}
# Code signing is OPTIONAL. Two ways, pick whichever you have:
# (a) any-CA cert: base64 your .pfx into WINDOWS_CERT_BASE64 (+ password)
# (b) Azure Trusted Signing: set the TRUSTED_SIGNING_* secrets
# No secrets → a valid UNSIGNED installer is still produced.
WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
TRUSTED_SIGNING_ACCOUNT: ${{ secrets.TRUSTED_SIGNING_ACCOUNT }}
TRUSTED_SIGNING_PROFILE: ${{ secrets.TRUSTED_SIGNING_PROFILE }}
run: |
$sign = if ($env:WINDOWS_CERT_BASE64 -or $env:TRUSTED_SIGNING_ENDPOINT) { '-Sign' } else { '' }
./packaging/windows/build-msi.ps1 `
-Version "$env:VERSION" `
-Exe agentty-windows-x86_64.exe `
-Arch x64 $sign
- name: Upload to release
shell: bash
run: gh release upload "$TAG" agentty-windows-x86_64.exe agentty-windows-x86_64.msi --clobber
build-macos:
name: macos ${{ matrix.arch }}
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-15 # Apple Silicon
- arch: x86_64
runner: macos-15-intel # Intel (macos-13 retired)
runs-on: ${{ matrix.runner }}
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- name: Init maya submodule (HTTPS)
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --recursive
- name: Install deps (Homebrew)
# GCC, NOT AppleClang: agentty + maya require C++26 (std::expected,
# std::format) requested via CMake's cxx_std_26 compile feature, which
# AppleClang doesn't advertise (configure fails in maya). openssl@3
# ships a static .a; nghttp2 ships only the dylib, so it's built from
# source below.
run: brew install gcc openssl@3 ninja
- name: Build static nghttp2 (Homebrew ships only the dylib)
run: |
ver=1.64.0
curl -fsSL "https://github.com/nghttp2/nghttp2/releases/download/v${ver}/nghttp2-${ver}.tar.gz" | tar xz
cd "nghttp2-${ver}"
./configure --prefix="$PWD/../nghttp2-static" \
--enable-lib-only --enable-static --disable-shared CFLAGS="-O2"
make -j"$(sysctl -n hw.ncpu)"
make install
- name: Configure + build (standalone)
run: |
gcc_pfx="$(brew --prefix gcc)"
# Resolve the versioned compilers (gcc-15, g++-15 …) without pinning
# a version Homebrew may bump out from under us.
GCC="$(ls "$gcc_pfx"/bin/gcc-* | grep -E 'gcc-[0-9]+$' | sort -V | tail -1)"
GXX="$(ls "$gcc_pfx"/bin/g++-* | grep -E 'g\+\+-[0-9]+$' | sort -V | tail -1)"
# Fold GCC's runtime into the binary so it runs without Homebrew GCC
# present: -static-libstdc++/-static-libgcc cover libstdc++/libgcc,
# but libquadmath has no -static flag — link its archive by full path
# (macOS ld picks the dylib otherwise). Result: only the system
# frameworks + libSystem remain dynamic.
QUADMATH="$gcc_pfx/lib/gcc/current/libquadmath.a"
PKG_CONFIG_PATH="$PWD/nghttp2-static/lib/pkgconfig" \
CC="$GCC" CXX="$GXX" cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release \
-DAGENTTY_STANDALONE=ON -DAGENTTY_AUTO_PULL_MAYA=OFF \
-DAGENTTY_USE_MIMALLOC=OFF \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
-DCMAKE_EXE_LINKER_FLAGS="-static-libstdc++ -static-libgcc $QUADMATH"
cmake --build build -j"$(sysctl -n hw.ncpu)"
strip build/agentty
cp build/agentty agentty-macos-${{ matrix.arch }}
- name: Verify portability (no Homebrew dylibs)
run: |
otool -L agentty-macos-${{ matrix.arch }}
if otool -L agentty-macos-${{ matrix.arch }} | grep -qE '/opt/homebrew|/usr/local'; then
echo "::error::binary links Homebrew dylibs — not portable"; exit 1
fi
- name: Upload to release
run: gh release upload "$TAG" agentty-macos-${{ matrix.arch }} --clobber
# x86_64 packages — deb (amd64) / rpm (x86_64) / Arch (x86_64). Needs only
# the x86 build, so it publishes ~5 min in without waiting on aarch64.
package-linux-x86_64:
name: packages x86_64
needs: [prepare, build-linux-x86_64]
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
with:
name: bin-linux-x86_64
path: bins
- name: Tooling
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends rpm libarchive-tools zstd
- name: Build + upload .deb / .rpm / .pkg.tar.zst
run: |
mkdir -p dist
chmod +x bins/agentty-linux-*
# deb/build.sh cd's into a tmpdir, so the output dir must be absolute.
bash packaging/deb/build.sh "$V" amd64 bins/agentty-linux-x86_64 "$PWD/dist"
bash packaging/rpm/build.sh "$V" x86_64 bins/agentty-linux-x86_64 "$PWD/dist"
# Arch .pkg.tar.zst built by hand (bsdtar + zstd) — same philosophy as
# the deb builder. Avoids makepkg's fakeroot, which is unreliable in
# containers ("libfakeroot: payload not recognized").
aroot="$(mktemp -d)"
install -Dm755 bins/agentty-linux-x86_64 "$aroot/usr/bin/agentty"
asize=$(du -sb "$aroot" | cut -f1)
{
echo "pkgname = agentty-bin"
echo "pkgbase = agentty-bin"
echo "pkgver = $V-1"
echo "pkgdesc = Blazing-fast Claude in your terminal (C++26, static binary)"
echo "url = https://github.com/1ay1/agentty"
echo "builddate = $(date +%s)"
echo "packager = agentty CI <noreply@github.com>"
echo "size = $asize"
echo "arch = x86_64"
echo "license = MIT"
echo "provides = agentty"
echo "conflict = agentty"
} > "$aroot/.PKGINFO"
bsdtar -C "$aroot" -czf "$aroot/.MTREE" --format=mtree \
--options='!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link' \
.PKGINFO usr
bsdtar -C "$aroot" -cf - .PKGINFO .MTREE usr \
| zstd -19 -T0 > "dist/agentty-bin-${V}-1-x86_64.pkg.tar.zst"
gh release upload "$TAG" dist/*.deb dist/*.rpm dist/*.pkg.tar.zst --clobber
# aarch64 packages — deb (arm64) / rpm (aarch64). Needs only the aarch64
# build, so it streams in when that QEMU build finishes (no Arch pkg: the
# AUR agentty-bin is x86_64-only).
package-linux-aarch64:
name: packages aarch64
needs: [prepare, build-linux-aarch64]
runs-on: ubuntu-latest
env:
V: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
with:
name: bin-linux-aarch64
path: bins
- name: Tooling
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends rpm
- name: Build + upload .deb / .rpm
run: |
mkdir -p dist
chmod +x bins/agentty-linux-*
# deb/build.sh cd's into a tmpdir, so the output dir must be absolute.
bash packaging/deb/build.sh "$V" arm64 bins/agentty-linux-aarch64 "$PWD/dist"
bash packaging/rpm/build.sh "$V" aarch64 bins/agentty-linux-aarch64 "$PWD/dist"
gh release upload "$TAG" dist/*.deb dist/*.rpm --clobber
# Runs last; regenerates SHA256SUMS over whatever is on the release. The
# binaries are already live regardless of whether this finishes — it only
# adds/refreshes the checksum file. `always()` so a single slow/failed build
# leg still gets a checksum file for everything that did land.
checksums:
name: checksums
needs:
- prepare
- build-linux-x86_64
- build-linux-aarch64
- build-linux-i686
- build-macos
- build-windows
- package-linux-x86_64
- package-linux-aarch64
if: always()
runs-on: ubuntu-latest
env:
TAG: ${{ needs.prepare.outputs.tag }}
steps:
- uses: actions/checkout@v5 # gh needs a git repo context for release ops
- name: Download release assets, hash, re-upload
run: |
mkdir -p dist && cd dist
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*' --skip-existing || true
rm -f SHA256SUMS
sha256sum -- * > SHA256SUMS
gh release upload "$TAG" SHA256SUMS --clobber
# Submit the Windows .msi to the winget community repo. winget is the
# zero-cost, no-SmartScreen-warning install path: `winget install agentty`.
# The action computes the installer SHA from the published .msi and opens a
# PR to microsoft/winget-pkgs. Requires a PAT (classic, public_repo scope)
# stored as the WINGET_TOKEN secret; skipped automatically if absent.
publish-winget:
name: publish to winget
needs: [prepare, build-windows]
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest
steps:
- name: Submit manifest to winget-pkgs
if: ${{ secrets.WINGET_TOKEN != '' }}
uses: vedantmgoyal9/winget-releaser@main
with:
identifier: agentty.agentty
version: ${{ needs.prepare.outputs.version }}
installers-regex: 'agentty-windows-x86_64\.msi$'
token: ${{ secrets.WINGET_TOKEN }}