Skip to content

making it faster for prioritising new tools #128

making it faster for prioritising new tools

making it faster for prioritising new tools #128

Workflow file for this run

name: Release
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version:
description: 'Version to release (leave empty for auto)'
required: false
channel:
description: 'Release channel'
required: true
default: 'release'
type: choice
options:
- alpha
- release
permissions:
contents: write
pull-requests: write
issues: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
channel: ${{ steps.determine.outputs.channel }}
version: ${{ steps.version.outputs.version }}
should_release: ${{ steps.check.outputs.should_release }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Determine release channel
id: determine
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "channel=${{ github.event.inputs.channel }}" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "channel=alpha" >> $GITHUB_OUTPUT
else
echo "channel=alpha" >> $GITHUB_OUTPUT
fi
- name: Check if should release
id: check
run: |
# Allow manual trigger to force release
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "should_release=true" >> $GITHUB_OUTPUT
echo "📦 Manual trigger - forcing release"
# Check if last commit is a version bump (skip if so)
elif git log -1 --pretty=%B | grep -q "chore(release):"; then
echo "should_release=false" >> $GITHUB_OUTPUT
echo "⏭️ Skipping - last commit is a release commit"
else
echo "should_release=true" >> $GITHUB_OUTPUT
fi
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.22
- name: Install dependencies
run: bun install
- name: Get version
id: version
run: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
CHANNEL="${{ steps.determine.outputs.channel }}"
SHORT_SHA="${GITHUB_SHA::7}"
# Manual version override takes priority
if [ -n "${{ github.event.inputs.version }}" ]; then
NEW_VERSION="${{ github.event.inputs.version }}"
echo "🎯 Using manual version override: ${NEW_VERSION}"
# For manual trigger without version, use current package.json version
elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "$CHANNEL" = "release" ]; then
NEW_VERSION="${CURRENT_VERSION}"
echo "🎯 Using existing package.json version: ${NEW_VERSION}"
elif [ "$CHANNEL" = "alpha" ]; then
# Alpha: bump patch from current version and append -alpha.<sha>
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3 | cut -d- -f1)
PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}-alpha.${SHORT_SHA}"
echo "🎯 Alpha version: ${NEW_VERSION}"
else
# Fallback: bump patch
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3 | cut -d- -f1)
PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
fi
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
echo "🎯 Version: ${NEW_VERSION} (${CHANNEL})"
test:
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.22
- name: Install dependencies
run: bun install
- name: Type check
run: bun run typecheck
- name: Run tests
run: bun run test
build:
needs: [prepare, test]
if: needs.prepare.outputs.should_release == 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: macos-latest
target: darwin-arm64
artifact: autohand-macos-arm64
- os: macos-latest
target: darwin-x64
artifact: autohand-macos-x64
- os: ubuntu-latest
target: linux-x64
artifact: autohand-linux-x64
- os: ubuntu-latest
target: linux-arm64
artifact: autohand-linux-arm64
- os: windows-latest
target: windows-x64
artifact: autohand-windows-x64.exe
steps:
- uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.22
- name: Install dependencies
run: bun install
- name: Update version before build
run: |
echo "Setting version to: ${{ needs.prepare.outputs.version }}"
npm version "${{ needs.prepare.outputs.version }}" --no-git-tag-version --allow-same-version
- name: Build TypeScript
run: bun run build
- name: Compile binary
run: |
mkdir -p binaries
bun build ./src/index.ts --compile --target=bun-${{ matrix.target }} --outfile ./binaries/${{ matrix.artifact }}
- name: Verify binary
if: runner.os != 'Windows' && !contains(matrix.target, 'arm64') || matrix.os == 'macos-latest' && contains(matrix.target, 'arm64')
run: |
chmod +x ./binaries/${{ matrix.artifact }}
file ./binaries/${{ matrix.artifact }}
- name: Smoke test binary
if: runner.os != 'Windows' && !contains(matrix.target, 'arm64') || matrix.os == 'macos-latest' && contains(matrix.target, 'arm64')
shell: bash
run: |
# Helper: run a command with a timeout (works on both Linux and macOS)
run_with_timeout() {
local secs="$1"; shift
"$@" &
local pid=$!
( sleep "$secs" && kill "$pid" 2>/dev/null ) &
local watchdog=$!
wait "$pid" 2>/dev/null
local rc=$?
kill "$watchdog" 2>/dev/null
wait "$watchdog" 2>/dev/null || true
return $rc
}
echo "Testing --version..."
run_with_timeout 10 ./binaries/${{ matrix.artifact }} --version < /dev/null
echo ""
echo "Testing --help..."
run_with_timeout 10 ./binaries/${{ matrix.artifact }} --help < /dev/null > /dev/null
echo "Smoke test passed!"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ./binaries/${{ matrix.artifact }}
retention-days: 1
release:
needs: [prepare, build]
if: needs.prepare.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Update version in package.json (stable only)
if: needs.prepare.outputs.channel == 'release'
run: |
bun --version
VERSION="${{ needs.prepare.outputs.version }}"
echo "Updating to version: $VERSION"
npm version $VERSION --no-git-tag-version --allow-same-version
node scripts/sync-homebrew-version.cjs
git add package.json homebrew/autohand.rb
git commit -m "chore(release): v$VERSION [skip ci]" || echo "No changes to commit"
git push origin ${{ github.ref_name }} || echo "No changes to push"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release binaries
run: |
mkdir -p release-binaries
find artifacts -type f -exec cp {} release-binaries/ \;
ls -lh release-binaries/
- name: Create bundled archives for installers and ACP registry
run: |
set -euo pipefail
cd release-binaries
RIPGREP_REPO="BurntSushi/ripgrep"
RIPGREP_VERSION=15.1.0
echo "Using ripgrep ${RIPGREP_VERSION}"
verify_checksum() {
local archive="$1"
local checksum_file="$2"
local expected
local actual
expected=$(awk '{print $1}' "$checksum_file")
actual=$(sha256sum "$archive" | awk '{print $1}')
if [ "$expected" != "$actual" ]; then
echo "Checksum verification failed for $archive" >&2
exit 1
fi
}
bundle_unix() {
local binary="$1"
local rg_target="$2"
local temp_dir
temp_dir=$(mktemp -d)
local rg_archive="ripgrep-${RIPGREP_VERSION}-${rg_target}.tar.gz"
local rg_url="https://github.com/${RIPGREP_REPO}/releases/download/${RIPGREP_VERSION}/${rg_archive}"
curl -fsSL "$rg_url" -o "${temp_dir}/${rg_archive}"
curl -fsSL "${rg_url}.sha256" -o "${temp_dir}/${rg_archive}.sha256"
verify_checksum "${temp_dir}/${rg_archive}" "${temp_dir}/${rg_archive}.sha256"
tar -xzf "${temp_dir}/${rg_archive}" -C "$temp_dir"
mkdir -p "${temp_dir}/bundle"
cp "$binary" "${temp_dir}/bundle/autohand"
cp "${temp_dir}/ripgrep-${RIPGREP_VERSION}-${rg_target}/rg" "${temp_dir}/bundle/rg"
chmod +x "${temp_dir}/bundle/autohand" "${temp_dir}/bundle/rg"
tar -czf "${binary}.tar.gz" -C "${temp_dir}/bundle" autohand rg
sha256sum "${binary}.tar.gz" > "${binary}.tar.gz.sha256"
rm -rf "$temp_dir"
echo "Created ${binary}.tar.gz"
}
bundle_windows() {
local binary="$1"
local rg_target="$2"
local archive_name="$3"
local output_path="${PWD}/${archive_name}"
local temp_dir
temp_dir=$(mktemp -d)
local rg_archive="ripgrep-${RIPGREP_VERSION}-${rg_target}.zip"
local rg_url="https://github.com/${RIPGREP_REPO}/releases/download/${RIPGREP_VERSION}/${rg_archive}"
curl -fsSL "$rg_url" -o "${temp_dir}/${rg_archive}"
curl -fsSL "${rg_url}.sha256" -o "${temp_dir}/${rg_archive}.sha256"
verify_checksum "${temp_dir}/${rg_archive}" "${temp_dir}/${rg_archive}.sha256"
unzip -q "${temp_dir}/${rg_archive}" -d "$temp_dir"
mkdir -p "${temp_dir}/bundle"
cp "$binary" "${temp_dir}/bundle/autohand.exe"
cp "${temp_dir}/ripgrep-${RIPGREP_VERSION}-${rg_target}/rg.exe" "${temp_dir}/bundle/rg.exe"
(
cd "${temp_dir}/bundle"
zip -q "$output_path" autohand.exe rg.exe
)
sha256sum "${archive_name}" > "${archive_name}.sha256"
rm -rf "$temp_dir"
echo "Created ${archive_name}"
}
# Create tar.gz bundles for Unix platforms
if [ -f "autohand-macos-arm64" ]; then
chmod +x autohand-macos-arm64
bundle_unix "autohand-macos-arm64" "aarch64-apple-darwin"
fi
if [ -f "autohand-macos-x64" ]; then
chmod +x autohand-macos-x64
bundle_unix "autohand-macos-x64" "x86_64-apple-darwin"
fi
if [ -f "autohand-linux-x64" ]; then
chmod +x autohand-linux-x64
bundle_unix "autohand-linux-x64" "x86_64-unknown-linux-musl"
fi
if [ -f "autohand-linux-arm64" ]; then
chmod +x autohand-linux-arm64
bundle_unix "autohand-linux-arm64" "aarch64-unknown-linux-musl"
fi
# Create bundled zip for Windows
if [ -f "autohand-windows-x64.exe" ]; then
bundle_windows "autohand-windows-x64.exe" "x86_64-pc-windows-msvc" "autohand-windows-x64.zip"
fi
ls -lh *.tar.gz *.tar.gz.sha256 *.zip *.zip.sha256 2>/dev/null || true
- name: Generate changelog
id: changelog
uses: actions/github-script@v7
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
RELEASE_CHANNEL: ${{ needs.prepare.outputs.channel }}
with:
script: |
const { execSync } = require('child_process');
const version = process.env.RELEASE_VERSION;
const channel = process.env.RELEASE_CHANNEL;
// Get commits since last tag
let commits;
let lastTag = null;
try {
lastTag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim();
commits = execSync(`git log ${lastTag}..HEAD --pretty=format:"%s"`, { encoding: 'utf8' });
} catch {
commits = execSync('git log --pretty=format:"%s" -n 20', { encoding: 'utf8' });
}
const lines = commits.split('\n').filter(line => line.trim() && !line.includes('chore(release)'));
// Helper to humanize commit messages
const humanize = (msg) => {
return msg
.replace(/^feat(\([^)]+\))?:\s*/i, '')
.replace(/^fix(\([^)]+\))?:\s*/i, '')
.replace(/^chore(\([^)]+\))?:\s*/i, '')
.replace(/^docs(\([^)]+\))?:\s*/i, '')
.replace(/^refactor(\([^)]+\))?:\s*/i, '')
.replace(/^test(\([^)]+\))?:\s*/i, '')
.replace(/^ci(\([^)]+\))?:\s*/i, '')
.replace(/^perf(\([^)]+\))?:\s*/i, '')
.trim();
};
// Capitalize first letter
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
// Categorize commits
const features = [];
const fixes = [];
const improvements = [];
const breaking = [];
for (const msg of lines) {
const clean = humanize(msg);
if (!clean) continue;
if (msg.includes('BREAKING CHANGE') || msg.includes('!:')) {
breaking.push(capitalize(clean));
} else if (msg.match(/^feat(\(|:)/i)) {
features.push(capitalize(clean));
} else if (msg.match(/^fix(\(|:)/i)) {
fixes.push(capitalize(clean));
} else if (msg.match(/^(refactor|perf|chore|docs|test|ci)(\(|:)/i)) {
improvements.push(capitalize(clean));
}
}
// Build a friendly changelog
let changelog = '';
// Channel badge for alpha
if (channel === 'alpha') {
changelog += '> **Alpha Release** — This is a pre-release build from the latest `main` branch. It may contain bugs or incomplete features.\n\n';
}
// Intro
if (lastTag) {
changelog += `Hey there! We've been busy making Autohand better. Here's what's new since ${lastTag}:\n\n`;
} else {
changelog += `Hey there! Here's what's new in this release:\n\n`;
}
// Breaking changes (serious tone)
if (breaking.length > 0) {
changelog += '### Heads up! Breaking Changes\n\n';
changelog += 'These changes might require updates to your setup:\n\n';
breaking.forEach(item => { changelog += `- ${item}\n`; });
changelog += '\n';
}
// Features (excited tone)
if (features.length > 0) {
changelog += '### New Stuff\n\n';
features.forEach(item => { changelog += `- ${item}\n`; });
changelog += '\n';
}
// Fixes (helpful tone)
if (fixes.length > 0) {
changelog += '### Bug Fixes\n\n';
if (fixes.length === 1) {
changelog += `We squashed a bug:\n\n`;
} else {
changelog += `We squashed ${fixes.length} bugs:\n\n`;
}
fixes.forEach(item => { changelog += `- ${item}\n`; });
changelog += '\n';
}
// Improvements (casual tone)
if (improvements.length > 0 && improvements.length <= 8) {
changelog += '### Under the Hood\n\n';
changelog += 'Some housekeeping and improvements:\n\n';
improvements.forEach(item => { changelog += `- ${item}\n`; });
changelog += '\n';
}
// If nothing categorized, add a generic message
if (features.length === 0 && fixes.length === 0 && improvements.length === 0 && breaking.length === 0) {
changelog += 'Minor updates and improvements to keep things running smoothly.\n\n';
}
// Installation section
const cb = '`' + '`' + '`';
changelog += '---\n\n';
changelog += '### Get it\n\n';
if (channel === 'alpha') {
changelog += '**Install this alpha build:**\n';
changelog += cb + 'bash\ncurl -fsSL https://autohand.ai/install.sh | sh -s -- --alpha\n' + cb + '\n\n';
changelog += '**Or install the latest stable release:**\n';
changelog += cb + 'bash\ncurl -fsSL https://autohand.ai/install.sh | sh\n' + cb + '\n\n';
} else {
changelog += '**Quickest way:**\n';
changelog += cb + 'bash\ncurl -fsSL https://autohand.ai/install.sh | sh\n' + cb + '\n\n';
changelog += '**Via npm or bun:**\n';
changelog += cb + 'bash\nnpm install -g autohand-cli\n' + cb + '\n\n';
}
changelog += '**Or grab a binary below** for your platform.\n\n';
changelog += '| Platform | Architecture | Binary |\n';
changelog += '|----------|--------------|--------|\n';
changelog += '| macOS | Apple Silicon | `autohand-macos-arm64` |\n';
changelog += '| macOS | Intel | `autohand-macos-x64` |\n';
changelog += '| Linux | x64 | `autohand-linux-x64` |\n';
changelog += '| Linux | ARM64 | `autohand-linux-arm64` |\n';
changelog += '| Windows | x64 | `autohand-windows-x64.exe` |\n';
core.setOutput('changelog', changelog);
return changelog;
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.prepare.outputs.version }}
name: ${{ needs.prepare.outputs.channel == 'release' && format('Release v{0}', needs.prepare.outputs.version) || format('Alpha v{0}', needs.prepare.outputs.version) }}
body: ${{ steps.changelog.outputs.changelog }}
files: |
release-binaries/*
install.sh
install.ps1
draft: false
prerelease: ${{ needs.prepare.outputs.channel != 'release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update Homebrew tap (release only)
if: needs.prepare.outputs.channel == 'release'
env:
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: |
if [ -z "$TAP_GITHUB_TOKEN" ]; then
echo "⚠️ TAP_GITHUB_TOKEN not set, skipping Homebrew tap update"
else
VERSION="${{ needs.prepare.outputs.version }}"
echo "Updating Homebrew tap to v${VERSION}..."
# Wait for release assets to be available
sleep 10
# Download release archives and compute sha256
SHA_MACOS_ARM64=$(curl -sL "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-macos-arm64.tar.gz" | shasum -a 256 | cut -d' ' -f1)
SHA_MACOS_X64=$(curl -sL "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-macos-x64.tar.gz" | shasum -a 256 | cut -d' ' -f1)
SHA_LINUX_X64=$(curl -sL "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-linux-x64.tar.gz" | shasum -a 256 | cut -d' ' -f1)
SHA_LINUX_ARM64=$(curl -sL "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-linux-arm64.tar.gz" | shasum -a 256 | cut -d' ' -f1)
echo "SHA256 checksums computed:"
echo " macOS ARM64: ${SHA_MACOS_ARM64}"
echo " macOS x64: ${SHA_MACOS_X64}"
echo " Linux x64: ${SHA_LINUX_X64}"
echo " Linux ARM64: ${SHA_LINUX_ARM64}"
# Clone tap repo
git clone "https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/autohandai/homebrew-code.git" homebrew-tap
cd homebrew-tap
# Write updated formula using sed replacements on the existing template
cp Formula/autohand-code.rb Formula/autohand-code.rb.bak 2>/dev/null || true
cat > Formula/autohand-code.rb << FORMULA_EOF
class AutohandCode < Formula
desc "Autonomous LLM-powered coding agent CLI"
homepage "https://autohand.ai"
version "${VERSION}"
license "Apache-2.0"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-macos-arm64.tar.gz"
sha256 "${SHA_MACOS_ARM64}"
else
url "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-macos-x64.tar.gz"
sha256 "${SHA_MACOS_X64}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-linux-arm64.tar.gz"
sha256 "${SHA_LINUX_ARM64}"
else
url "https://github.com/autohandai/code-cli/releases/download/v${VERSION}/autohand-linux-x64.tar.gz"
sha256 "${SHA_LINUX_X64}"
end
end
def install
bin.install "autohand" => "autohand-code"
end
test do
assert_match version.to_s, shell_output("#{bin}/autohand-code --version")
end
end
FORMULA_EOF
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/autohand-code.rb
git commit -m "Update autohand-code to v${VERSION}"
git push
echo "Homebrew tap updated to v${VERSION}"
fi
- name: Build JS dist for npm
if: needs.prepare.outputs.channel == 'release'
run: |
bun install
bun run build
ls -lh dist/
- name: Publish to npm (release only)
if: needs.prepare.outputs.channel == 'release'
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "$NPM_TOKEN" ]; then
echo "⚠️ NPM_TOKEN not set, skipping npm publish"
exit 0
fi
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
npm publish --access public