name: CI
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: Test (${{ matrix.os }})
needs: lint
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run tests
run: cargo test --all-features --verbose
publish:
name: Publish to crates.io
needs: test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Verify version matches tag
shell: bash
run: |
CARGO_VERSION="v$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
TAG_VERSION="${GITHUB_REF#refs/tags/}"
if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
echo "::error::Cargo.toml version ($CARGO_VERSION) does not match tag ($TAG_VERSION)"
exit 1
fi
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --allow-dirty
build:
name: Build (${{ matrix.target }})
needs: test
if: startsWith(github.ref, 'refs/tags/v')
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: x86_64-apple-darwin
os: macos-latest
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-latest
archive: tar.gz
- target: x86_64-pc-windows-msvc
os: windows-latest
archive: zip
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross-compilation deps (aarch64-linux)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Get version from tag
id: version
shell: bash
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Generate shell completions and build .deb (Linux x86_64)
if: matrix.target == 'x86_64-unknown-linux-gnu'
shell: bash
run: |
# Generate shell completions from the just-built binary
YAAK="target/${{ matrix.target }}/release/yaak"
mkdir -p target/completions
"$YAAK" --completions bash > target/completions/yaak.bash
"$YAAK" --completions zsh > target/completions/_yaak
"$YAAK" --completions fish > target/completions/yaak.fish
# Build .deb package
cargo install cargo-deb
cargo deb --target ${{ matrix.target }} --no-build
DEB=$(find target/${{ matrix.target }}/debian -name '*.deb' | head -1)
cp "$DEB" "yaak_${{ steps.version.outputs.version }}_amd64.deb"
- name: Package (unix)
if: matrix.archive == 'tar.gz'
shell: bash
run: |
STAGING="yaak-${{ steps.version.outputs.version }}-${{ matrix.target }}"
mkdir "$STAGING"
cp target/${{ matrix.target }}/release/yaak "$STAGING/"
cp README.md LICENSE* "$STAGING/" 2>/dev/null || true
tar czf "$STAGING.tar.gz" "$STAGING"
echo "ASSET=$STAGING.tar.gz" >> $GITHUB_ENV
- name: Package (windows)
if: matrix.archive == 'zip'
shell: bash
run: |
STAGING="yaak-${{ steps.version.outputs.version }}-${{ matrix.target }}"
mkdir "$STAGING"
cp target/${{ matrix.target }}/release/yaak.exe "$STAGING/"
cp README.md LICENSE* "$STAGING/" 2>/dev/null || true
7z a "$STAGING.zip" "$STAGING"
echo "ASSET=$STAGING.zip" >> $GITHUB_ENV
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: ${{ env.ASSET }}
- name: Upload .deb artifact
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: actions/upload-artifact@v4
with:
name: debian-package
path: "yaak_${{ steps.version.outputs.version }}_amd64.deb"
release:
name: Release
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: List artifacts
run: find artifacts -type f | sort
- name: Generate checksums
shell: bash
run: |
cd artifacts
find . -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' \) \
-exec sh -c 'sha256sum "$1" >> ../SHA256SUMS.txt' _ {} \;
cd ..
cat SHA256SUMS.txt
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
artifacts/**/*.tar.gz
artifacts/**/*.zip
artifacts/**/*.deb
SHA256SUMS.txt
homebrew:
name: Update Homebrew tap
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get version from tag
id: version
shell: bash
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
- name: Download macOS release tarballs and compute SHA256
id: sha
shell: bash
run: |
VERSION="${{ steps.version.outputs.tag }}"
BASE_URL="https://github.com/hanneshapke/yaak/releases/download/${VERSION}"
curl -fSL -o x86_64.tar.gz "${BASE_URL}/yaak-${VERSION}-x86_64-apple-darwin.tar.gz"
curl -fSL -o aarch64.tar.gz "${BASE_URL}/yaak-${VERSION}-aarch64-apple-darwin.tar.gz"
curl -fSL -o linux_x86_64.tar.gz "${BASE_URL}/yaak-${VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fSL -o linux_aarch64.tar.gz "${BASE_URL}/yaak-${VERSION}-aarch64-unknown-linux-gnu.tar.gz"
echo "sha_mac_x86=$(sha256sum x86_64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "sha_mac_arm=$(sha256sum aarch64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "sha_linux_x86=$(sha256sum linux_x86_64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "sha_linux_arm=$(sha256sum linux_aarch64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
- name: Generate Homebrew formula
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="${{ steps.version.outputs.tag }}"
cat > yaak.rb <<FORMULA
class Yaak < Formula
desc "Translate natural language to bash commands using an OpenAI-compatible LLM"
homepage "https://www.hanneshapke.com/yaak/"
version "${VERSION}"
license "Apache-2.0"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-aarch64-apple-darwin.tar.gz"
sha256 "${{ steps.sha.outputs.sha_mac_arm }}"
else
url "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-x86_64-apple-darwin.tar.gz"
sha256 "${{ steps.sha.outputs.sha_mac_x86 }}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-aarch64-unknown-linux-gnu.tar.gz"
sha256 "${{ steps.sha.outputs.sha_linux_arm }}"
else
url "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-x86_64-unknown-linux-gnu.tar.gz"
sha256 "${{ steps.sha.outputs.sha_linux_x86 }}"
end
end
def install
bin.install "yaak"
end
test do
assert_match "yaak", shell_output("#{bin}/yaak --version")
end
end
FORMULA
# Remove leading whitespace from heredoc
sed -i 's/^ //' yaak.rb
cat yaak.rb
- name: Push formula to Homebrew tap
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.HOMEBREW_TAP_TOKEN }}
with:
source_file: yaak.rb
destination_repo: hanneshapke/homebrew-yaak
destination_folder: Formula
destination_branch: main
user_email: github-actions[bot]@users.noreply.github.com
user_name: github-actions[bot]
commit_message: "Update yaak formula to ${{ steps.version.outputs.tag }}"
aur:
name: Update AUR packages
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- pkgname: yaak-cli
asset_suffix: ""
- pkgname: yaak-cli-bin
asset_suffix: "-x86_64-unknown-linux-gnu.tar.gz"
steps:
- uses: actions/checkout@v4
- name: Get version from tag
id: version
shell: bash
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
- name: Compute source hash (yaak-cli)
if: matrix.pkgname == 'yaak-cli'
id: hash_source
shell: bash
run: |
TAG="${{ steps.version.outputs.tag }}"
URL="https://github.com/hanneshapke/yaak/archive/refs/tags/${TAG}.tar.gz"
SHA=$(curl -fSL "$URL" | sha256sum | awk '{print $1}')
echo "sha256=$SHA" >> $GITHUB_OUTPUT
- name: Compute binary hash (yaak-cli-bin)
if: matrix.pkgname == 'yaak-cli-bin'
id: hash_bin
shell: bash
run: |
TAG="${{ steps.version.outputs.tag }}"
URL="https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-x86_64-unknown-linux-gnu.tar.gz"
SHA=$(curl -fSL "$URL" | sha256sum | awk '{print $1}')
echo "sha256=$SHA" >> $GITHUB_OUTPUT
- name: Update PKGBUILD
shell: bash
run: |
PKGBUILD="packaging/aur/${{ matrix.pkgname }}/PKGBUILD"
VERSION="${{ steps.version.outputs.version }}"
# Update pkgver
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
# Reset pkgrel to 1 on version bump
sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
# Update sha256sums
if [ "${{ matrix.pkgname }}" = "yaak-cli" ]; then
SHA="${{ steps.hash_source.outputs.sha256 }}"
else
SHA="${{ steps.hash_bin.outputs.sha256 }}"
fi
sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" "$PKGBUILD"
echo "--- Updated PKGBUILD ---"
cat "$PKGBUILD"
- name: Generate .SRCINFO
uses: addnab/docker-run-action@v3
with:
image: archlinux:latest
options: -v ${{ github.workspace }}:/work
run: |
pacman -Syu --noconfirm --needed base-devel
useradd -m builder
echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder
chown -R builder:builder /work
su builder -c "cd /work/packaging/aur/${{ matrix.pkgname }} && makepkg --printsrcinfo > .SRCINFO"
- name: Publish to AUR
shell: bash
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
run: |
# Set up SSH for AUR access
mkdir -p ~/.ssh
echo "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan -t ed25519,rsa aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
cat >> ~/.ssh/config <<CFG
Host aur.archlinux.org
IdentityFile ~/.ssh/aur
User aur
CFG
# Clone the AUR repo (may be empty for first push)
PKGNAME="${{ matrix.pkgname }}"
TMPDIR="$(mktemp -d)"
git clone "ssh://aur@aur.archlinux.org/${PKGNAME}.git" "$TMPDIR" || {
# First push — initialize empty repo
git init "$TMPDIR"
git -C "$TMPDIR" remote add origin "ssh://aur@aur.archlinux.org/${PKGNAME}.git"
}
# Copy PKGBUILD and .SRCINFO into the AUR repo
cp "packaging/aur/${PKGNAME}/PKGBUILD" "$TMPDIR/"
cp "packaging/aur/${PKGNAME}/.SRCINFO" "$TMPDIR/" 2>/dev/null || true
# Commit and push
cd "$TMPDIR"
git config user.name "yaak-bot"
git config user.email "yaak-bot@users.noreply.github.com"
git add PKGBUILD .SRCINFO
git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "Update to ${{ steps.version.outputs.tag }}"
git push origin HEAD:master
scoop:
name: Update Scoop bucket
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get version from tag
id: version
shell: bash
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
- name: Compute Windows binary hash
id: hash
shell: bash
run: |
TAG="${{ steps.version.outputs.tag }}"
URL="https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-x86_64-pc-windows-msvc.zip"
SHA=$(curl -fSL "$URL" | sha256sum | awk '{print $1}')
echo "sha256=$SHA" >> $GITHUB_OUTPUT
- name: Generate Scoop manifest
shell: bash
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="${{ steps.version.outputs.tag }}"
SHA="${{ steps.hash.outputs.sha256 }}"
cat > yaak.json <<MANIFEST
{
"version": "${VERSION}",
"description": "Translate natural language to bash commands using any OpenAI-compatible LLM",
"homepage": "https://github.com/hanneshapke/yaak",
"license": "Apache-2.0",
"architecture": {
"64bit": {
"url": "https://github.com/hanneshapke/yaak/releases/download/${TAG}/yaak-${TAG}-x86_64-pc-windows-msvc.zip",
"hash": "${SHA}",
"extract_dir": "yaak-${TAG}-x86_64-pc-windows-msvc"
}
},
"bin": "yaak.exe",
"checkver": {
"github": "https://github.com/hanneshapke/yaak"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://github.com/hanneshapke/yaak/releases/download/v\$version/yaak-v\$version-x86_64-pc-windows-msvc.zip",
"extract_dir": "yaak-v\$version-x86_64-pc-windows-msvc"
}
},
"hash": {
"url": "https://github.com/hanneshapke/yaak/releases/download/v\$version/SHA256SUMS.txt"
}
}
}
MANIFEST
# Remove leading whitespace from heredoc
sed -i 's/^ //' yaak.json
echo "--- Generated manifest ---"
cat yaak.json
- name: Push manifest to Scoop bucket
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.SCOOP_BUCKET_TOKEN }}
with:
source_file: yaak.json
destination_repo: hanneshapke/scoop-yaak
destination_folder: .
destination_branch: main
destination_branch_create: main
user_email: github-actions[bot]@users.noreply.github.com
user_name: github-actions[bot]
commit_message: "Update yaak to ${{ steps.version.outputs.tag }}"
nix:
name: Verify Nix flake
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Build with Nix
working-directory: packaging/nix
run: |
nix build .#yaak --print-build-logs
./result/bin/yaak --version