fixing add-dir missing test edge case and adding i18n support for cli #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |