name: Release
on:
push:
tags: ['v*.*.*']
workflow_dispatch:
permissions:
contents: write
jobs:
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, suffix: linux-x86_64, ext: '' }
- { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, suffix: linux-aarch64, ext: '', cross: true }
- { os: macos-latest, target: x86_64-apple-darwin, suffix: macos-x86_64, ext: '' }
- { os: macos-latest, target: aarch64-apple-darwin, suffix: macos-aarch64, ext: '' }
- { os: windows-latest, target: x86_64-pc-windows-msvc, suffix: windows-x86_64, ext: '.exe' }
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install cross-compile toolchain (linux/aarch64)
if: matrix.cross
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
mkdir -p .cargo
cat >> .cargo/config.toml <<EOF
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
EOF
- name: Build release
run: cargo build --release --locked --target ${{ matrix.target }}
- name: Package (unix)
if: runner.os != 'Windows'
shell: bash
run: |
name="agtop-${{ matrix.suffix }}"
mkdir -p "dist/$name"
cp "target/${{ matrix.target }}/release/agtop${{ matrix.ext }}" "dist/$name/"
cp README.md LICENSE "dist/$name/"
cd dist
tar -czf "${name}.tar.gz" "$name"
- name: Package (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$name = "agtop-${{ matrix.suffix }}"
New-Item -ItemType Directory -Force -Path "dist/$name" | Out-Null
Copy-Item "target/${{ matrix.target }}/release/agtop${{ matrix.ext }}" "dist/$name/"
Copy-Item "README.md","LICENSE" "dist/$name/"
Compress-Archive -Path "dist/$name" -DestinationPath "dist/$name.zip" -Force
- uses: softprops/action-gh-release@v3
with:
files: |
dist/agtop-${{ matrix.suffix }}.tar.gz
dist/agtop-${{ matrix.suffix }}.zip
fail_on_unmatched_files: false
generate_release_notes: true
sha256sums:
name: Aggregate SHA256SUMS
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Download release assets, hash, and upload SHA256SUMS
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
version=$(awk -F'"' '/^version[[:space:]]*=/{print $2; exit}' Cargo.toml)
tag="v$version"
mkdir -p sums
cd sums
# gh release download retries internally; the build matrix has
# already completed by the time we get here so all 5 assets exist.
gh release download "$tag" --repo "${{ github.repository }}" \
--pattern 'agtop-*.tar.gz' --pattern 'agtop-*.zip'
# Standard `sha256sum`-format file: `<sha> <filename>` per line.
# Sort for determinism so re-runs produce identical bytes.
sha256sum agtop-*.tar.gz agtop-*.zip 2>/dev/null | sort > SHA256SUMS
echo "── SHA256SUMS ──"
cat SHA256SUMS
gh release upload "$tag" SHA256SUMS --clobber \
--repo "${{ github.repository }}"
publish-crates:
name: Publish to crates.io
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- name: cargo publish
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
run: |
if [ -z "$CRATES_IO_TOKEN" ]; then
echo "::error::CRATES_IO_TOKEN secret not set"
exit 1
fi
version=$(awk -F'"' '/^version[[:space:]]*=/{print $2; exit}' Cargo.toml)
# Direct API check (cargo search has index-propagation lag of
# several minutes after a fresh upload).
if curl -sfo /dev/null "https://crates.io/api/v1/crates/agtop/$version"; then
echo "agtop $version already on crates.io — skipping"
exit 0
fi
# Even with the API check above, a re-dispatched run can race
# against a still-propagating publish. Trap the canonical
# "already exists" error and treat as success; hard-fail on
# anything else so silent breakage can't ship.
if out=$(cargo publish --token "$CRATES_IO_TOKEN" 2>&1); then
echo "$out"
exit 0
fi
echo "$out"
if echo "$out" | grep -q "already exists on crates.io index"; then
echo "agtop $version already on crates.io (caught via publish error) — treating as success"
exit 0
fi
exit 1
publish-npm:
name: Publish to npm (@mbrassey/agtop)
needs: [build, sha256sums]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Build npm tarball
run: bash packages/npm/build.sh
- name: Publish (skip if version already on npm)
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "$NODE_AUTH_TOKEN" ]; then
echo "::error::NPM_TOKEN secret not set"
exit 1
fi
cd packages/npm/build
version=$(node -p "require('./package.json').version")
name=$(node -p "require('./package.json').name")
published=$(npm view "$name@$version" version 2>/dev/null || true)
if [ -n "$published" ]; then
echo "$name@$version already on npm — skipping"
exit 0
fi
npm publish --access public
publish-aur:
name: Publish to AUR
needs: build
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- name: Install host deps
run: |
pacman -Sy --noconfirm --needed git openssh sudo base-devel curl awk
useradd -m builder
echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
- uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup SSH for AUR
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
run: |
if [ -z "$AUR_SSH_PRIVATE_KEY" ]; then
echo "::error::AUR_SSH_PRIVATE_KEY secret not set"
exit 1
fi
mkdir -p /home/builder/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > /home/builder/.ssh/id_aur
chmod 600 /home/builder/.ssh/id_aur
ssh-keyscan -t ed25519,rsa,ecdsa aur.archlinux.org > /home/builder/.ssh/known_hosts 2>/dev/null
cat > /home/builder/.ssh/config <<'EOF'
Host aur.archlinux.org
IdentityFile ~/.ssh/id_aur
User aur
StrictHostKeyChecking yes
IdentitiesOnly yes
EOF
chmod 600 /home/builder/.ssh/config /home/builder/.ssh/known_hosts
chown -R builder:builder /home/builder/.ssh
- name: Resolve version
id: ver
run: |
v=$(awk -F'"' '/^version[[:space:]]*=/{print $2; exit}' Cargo.toml)
echo "version=$v" >> "$GITHUB_OUTPUT"
- name: Compute release tarball sha256
id: sha
run: |
url="https://github.com/mbrassey/agtop/archive/v${{ steps.ver.outputs.version }}.tar.gz"
for i in $(seq 1 10); do
if curl -sfL -o /tmp/agtop.tar.gz "$url"; then break; fi
echo "tarball not ready (attempt $i) — sleeping 10s"
sleep 10
done
sha=$(sha256sum /tmp/agtop.tar.gz | awk '{print $1}')
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Update AUR repo
env:
VERSION: ${{ steps.ver.outputs.version }}
SHA256: ${{ steps.sha.outputs.sha }}
run: |
# The Actions checkout lives at $GITHUB_WORKSPACE inside the
# container. Stage the PKGBUILD into builder's homedir so
# makepkg can run as the unprivileged user (it refuses root).
cp packages/pacman/PKGBUILD /home/builder/PKGBUILD.in
chown builder:builder /home/builder/PKGBUILD.in
sudo -u builder -H bash -se <<INNER
set -euo pipefail
cd "\$HOME"
git clone ssh://aur@aur.archlinux.org/agtop.git aur-repo
cd aur-repo
cp ../PKGBUILD.in PKGBUILD
# Rewrite source line to fetch from GH archive, stamp real sha256.
# \\\${pkgver} stays literal (PKGBUILD-side variable); \${SHA256}
# and \${VERSION} substitute here from the outer GH Actions step.
sed -i 's|^source=.*|source=("agtop-\${pkgver}.tar.gz::https://github.com/mbrassey/agtop/archive/v\${pkgver}.tar.gz")|' PKGBUILD
sed -i "s|^sha256sums=.*|sha256sums=('${SHA256}')|" PKGBUILD
makepkg --printsrcinfo > .SRCINFO
git config user.name 'mbrassey'
git config user.email 'matt@brassey.io'
git add PKGBUILD .SRCINFO
if git diff --cached --quiet; then
echo "AUR repo already at this version — skipping push"
exit 0
fi
git commit -m "agtop ${VERSION}"
git push
INNER
publish-debian:
name: Publish .deb to apt repo (mbrassey.github.io/apt)
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Resolve version
id: ver
run: |
v=$(awk -F'"' '/^version[[:space:]]*=/{print $2; exit}' Cargo.toml)
echo "version=$v" >> "$GITHUB_OUTPUT"
- name: Install apt-repo tooling
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq dpkg-dev gpg
- name: Download linux-x86_64 prebuilt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p deb-bin
gh release download "v${{ steps.ver.outputs.version }}" \
--repo "${{ github.repository }}" \
--pattern 'agtop-linux-x86_64.tar.gz' --dir deb-bin
tar -xzf deb-bin/agtop-linux-x86_64.tar.gz -C deb-bin
mkdir -p target/release
install -m 0755 deb-bin/agtop-linux-x86_64/agtop target/release/agtop
- name: Build .deb
run: bash packages/deb/build.sh
- name: Import GPG signing key
env:
KEY: ${{ secrets.APT_REPO_GPG_PRIVATE_KEY }}
run: |
if [ -z "$KEY" ]; then
echo "::error::APT_REPO_GPG_PRIVATE_KEY secret not set"; exit 1
fi
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
printf '%s' "$KEY" | gpg --batch --import 2>&1 | tail -3
echo "GPG_KEY_ID=$(gpg --list-secret-keys --with-colons \
| awk -F: '/^sec/{print $5; exit}')" >> "$GITHUB_ENV"
- name: Stage apt repo (clone + add deb + regen metadata)
env:
TOK: ${{ secrets.APT_REPO_TOKEN }}
VERSION: ${{ steps.ver.outputs.version }}
run: |
if [ -z "$TOK" ]; then
echo "::error::APT_REPO_TOKEN secret not set"; exit 1
fi
tap=$(mktemp -d)
git clone --depth=1 \
"https://x-access-token:${TOK}@github.com/MBrassey/apt.git" "$tap"
DEB="packages/deb/agtop_${VERSION}_amd64.deb"
[ -f "$DEB" ] || { echo "::error::missing $DEB"; ls packages/deb/; exit 1; }
# Flat repo layout — no dists/ tree. Users add a single
# `deb [signed-by=...] https://mbrassey.github.io/apt ./` line.
mkdir -p "$tap/pool"
cp "$DEB" "$tap/pool/"
( cd "$tap" && dpkg-scanpackages --multiversion pool /dev/null \
> Packages
gzip -9k -f Packages
apt-ftparchive release \
-o APT::FTPArchive::Release::Origin="MBrassey" \
-o APT::FTPArchive::Release::Label="agtop" \
-o APT::FTPArchive::Release::Suite="stable" \
-o APT::FTPArchive::Release::Codename="agtop" \
-o APT::FTPArchive::Release::Architectures="amd64" \
-o APT::FTPArchive::Release::Components="main" \
. > Release
gpg --batch --yes --default-key "${GPG_KEY_ID}" -abs -o Release.gpg Release
gpg --batch --yes --default-key "${GPG_KEY_ID}" --clearsign -o InRelease Release
)
cd "$tap"
git config user.name "mbrassey"
git config user.email "matt@brassey.io"
git add -A
if git diff --cached --quiet; then
echo "no changes — apt repo already at v${VERSION}"
exit 0
fi
git commit -m "agtop ${VERSION}"
git push
- name: Wipe GPG signing material
if: always()
run: |
gpgconf --kill all 2>/dev/null || true
if [ -d "$HOME/.gnupg" ]; then
find "$HOME/.gnupg" -type f -exec shred -u {} + 2>/dev/null || true
rm -rf "$HOME/.gnupg"
fi
publish-snap:
name: Publish to Snap Store
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: snapcore/action-build@v1
id: snapcraft
- name: Push to Snap Store
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
run: |
if [ -z "$SNAPCRAFT_STORE_CREDENTIALS" ]; then
echo "::warning::SNAPCRAFT_STORE_CREDENTIALS not set — skipping publish (snap built and attached as artifact above)"
exit 0
fi
sudo snap install snapcraft --classic
snapcraft upload --release=stable "${{ steps.snapcraft.outputs.snap }}"
- uses: actions/upload-artifact@v4
with:
name: agtop-snap
path: ${{ steps.snapcraft.outputs.snap }}
publish-homebrew:
name: Publish to Homebrew tap
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Resolve version + sha256
id: meta
run: |
set -euo pipefail
v=$(awk -F'"' '/^version[[:space:]]*=/{print $2; exit}' Cargo.toml)
url="https://github.com/mbrassey/agtop/archive/refs/tags/v${v}.tar.gz"
for i in $(seq 1 10); do
if curl -sfL -o /tmp/src.tar.gz "$url"; then break; fi
sleep 10
done
sha=$(sha256sum /tmp/src.tar.gz | awk '{print $1}')
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "url=$url" >> "$GITHUB_OUTPUT"
- name: Template formula
run: |
mkdir -p homebrew-out
sed \
-e "s|^ url \".*\"\$| url \"${{ steps.meta.outputs.url }}\"|" \
-e "s|^ sha256 \".*\"\$| sha256 \"${{ steps.meta.outputs.sha }}\"|" \
homebrew/agtop.rb > homebrew-out/agtop.rb
echo "── final formula ──"
cat homebrew-out/agtop.rb
- name: Push to mbrassey/homebrew-tap
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
if [ -z "$HOMEBREW_TAP_TOKEN" ]; then
echo "::warning::HOMEBREW_TAP_TOKEN secret not set — skipping tap push (formula was templated above for inspection)"
exit 0
fi
git config --global user.name mbrassey
git config --global user.email matt@brassey.io
tap_dir=$(mktemp -d)
git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/MBrassey/homebrew-tap.git" "$tap_dir"
mkdir -p "$tap_dir/Formula"
cp homebrew-out/agtop.rb "$tap_dir/Formula/agtop.rb"
cd "$tap_dir"
git add Formula/agtop.rb
if git diff --cached --quiet; then
echo "tap formula unchanged — skipping push"
exit 0
fi
git commit -m "agtop ${{ steps.meta.outputs.version }}"
git push