Skip to content

fixing add-dir missing test edge case and adding i18n support for cli #53

fixing add-dir missing test edge case and adding i18n support for cli

fixing add-dir missing test edge case and adding i18n support for cli #53

Workflow file for this run

name: Release
on:
push:
branches:
- main
- alpha
- beta
workflow_dispatch:
inputs:
version:
description: 'Version to release (leave empty for auto)'
required: false
channel:
description: 'Release channel'
required: true
default: 'beta'
type: choice
options:
- alpha
- beta
- 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=release" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/beta" ]; then
echo "channel=beta" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/alpha" ]; then
echo "channel=alpha" >> $GITHUB_OUTPUT
else
echo "channel=beta" >> $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 # Pin version - 1.3.x breaks --external in compiled binaries
- 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 }}"
# 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" ]; then
NEW_VERSION="${CURRENT_VERSION}"
echo "🎯 Using existing package.json version: ${NEW_VERSION}"
else
# Parse semantic version
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3 | cut -d- -f1)
# Always bump patch (use manual version override for minor/major bumps)
PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
# Add prerelease tag for non-release channels
if [ "$CHANNEL" = "alpha" ]; then
BUILD_NUM=$(date +%Y%m%d%H%M%S)
NEW_VERSION="${NEW_VERSION}-alpha.${BUILD_NUM}"
elif [ "$CHANNEL" = "beta" ]; then
BUILD_NUM=$(date +%Y%m%d%H%M)
NEW_VERSION="${NEW_VERSION}-beta.${BUILD_NUM}"
fi
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 # Pin version - 1.3.x breaks --external in compiled binaries
- 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 # Pin version - 1.3.x breaks --external in compiled binaries
- name: Install dependencies
run: bun install
- name: Build TypeScript
run: bun run build
- name: Compile binary
run: |
mkdir -p binaries
bun build ./src/index.ts --compile --target=bun-${{ matrix.target }} --external react-devtools-core --outfile ./binaries/${{ matrix.artifact }}
- name: Verify binary
if: runner.os != 'Windows'
run: |
chmod +x ./binaries/${{ matrix.artifact }}
file ./binaries/${{ matrix.artifact }}
- 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
run: |
bun --version
VERSION="${{ needs.prepare.outputs.version }}"
echo "Updating to version: $VERSION"
npm version $VERSION --no-git-tag-version --allow-same-version
git add package.json
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: Generate changelog
id: changelog
uses: actions/github-script@v7
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
with:
script: |
const { execSync } = require('child_process');
const version = process.env.RELEASE_VERSION;
// 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 = '';
// 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';
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('{0} v{1}', needs.prepare.outputs.channel, needs.prepare.outputs.version) }}
body: ${{ steps.changelog.outputs.changelog }}
files: |
release-binaries/*
install.sh
draft: false
prerelease: ${{ needs.prepare.outputs.channel != 'release' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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