fix: handle root user in GitHub CLI installation script #16
Workflow file for this run
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
| # Release Workflow | |
| # | |
| # This workflow automates the release process when semantic version tags are pushed. | |
| # It reuses build artifacts from the build workflow (must merge to main first), validates | |
| # the version, creates GitHub releases, and publishes to package managers. | |
| # | |
| # IMPORTANT: Tags must be on commits that have been merged to main and built by build.yml | |
| # | |
| # Triggers: Push of semantic version tag (e.g., v1.2.3) | |
| # Performance Target: ≤10 minutes total (excluding manual approval) | |
| # Artifacts: Reuses build artifacts, creates release packages | |
| # | |
| # Manual Testing on PR: | |
| # 1. Run Build workflow manually on your PR branch first | |
| # 2. Note the commit SHA from the build run | |
| # 3. Use Actions tab → Release → Run workflow | |
| # - Select your PR branch | |
| # - Enter test version (e.g., v0.0.1-test) | |
| # - Check 'Skip Homebrew' to avoid tap updates | |
| # - Check 'Dry run' to skip actual release creation | |
| # 4. Review logs to verify workflow logic | |
| # | |
| # Production Release: | |
| # 1. Merge PR to main → triggers build.yml automatically | |
| # 2. Wait for build to complete successfully | |
| # 3. Tag the merge commit: git tag v1.0.0 && git push origin v1.0.0 | |
| # 4. Release workflow triggers automatically | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' # Semantic version tags only (e.g., v1.2.3, v0.1.0) | |
| workflow_dispatch: # Allow manual triggering for testing | |
| inputs: | |
| tag: | |
| description: 'Version tag to release (e.g., v0.0.1-test for testing)' | |
| required: true | |
| type: string | |
| skip_homebrew: | |
| description: 'Skip Homebrew publication (for PR testing)' | |
| required: false | |
| type: boolean | |
| default: false | |
| dry_run: | |
| description: 'Dry run - skip release creation (logs only)' | |
| required: false | |
| type: boolean | |
| default: false | |
| # Prevent concurrent releases to avoid conflicts | |
| # Do not cancel in-progress releases as they involve publishing to external services | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| # Workflow-level permissions | |
| permissions: | |
| contents: write # Required for creating releases and reading repo | |
| actions: read # Required for downloading artifacts from build workflow | |
| packages: write # Required for GitHub Packages (bottles) | |
| issues: write # Required for creating Winget/Chocolatey publication issues | |
| jobs: | |
| select-runner: | |
| name: Select Linux Runner | |
| runs-on: ubuntu-latest | |
| outputs: | |
| linux: ${{ steps.detect.outputs.linux }} | |
| steps: | |
| - name: Detect available self-hosted runners | |
| id: detect | |
| env: | |
| GH_TOKEN: ${{ secrets.RUNNER_QUERY_TOKEN }} | |
| run: | | |
| echo "🔍 Checking for online self-hosted Linux runners" | |
| # Fallback to ubuntu-latest if token not available or API fails | |
| if [ -z "$GH_TOKEN" ]; then | |
| echo "⚠️ RUNNER_QUERY_TOKEN secret not configured; using ubuntu-latest" | |
| echo "linux=ubuntu-latest" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if ! RUNNERS=$(gh api repos/${{ github.repository }}/actions/runners --paginate 2>&1); then | |
| echo "⚠️ Unable to query runner API; using ubuntu-latest" | |
| echo "Error: $RUNNERS" | |
| echo "linux=ubuntu-latest" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Debug: show what we got | |
| echo "📊 Runner API response:" | |
| echo "$RUNNERS" | jq -r '.runners[]? | " - \(.name): status=\(.status), os=\(.os), labels=\(.labels | map(.name) | join(","))"' | |
| # Match runners with status=online, os=Linux (case-insensitive), and self-hosted label | |
| LINUX_COUNT=$(echo "$RUNNERS" | jq '[.runners[]? | select(.status == "online" and (.os | ascii_downcase) == "linux" and (.labels[]?.name == "self-hosted"))] | length') | |
| if [ "$LINUX_COUNT" -gt 0 ]; then | |
| echo "🤖 Found $LINUX_COUNT online self-hosted Linux runner(s); selecting self-hosted" | |
| echo "linux=self-hosted" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "☁️ No online self-hosted Linux runners found; using ubuntu-latest" | |
| echo "linux=ubuntu-latest" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Job 1: Validate Version and Find Artifacts | |
| # Extract/validate version, ensure tag is on main, find build artifacts | |
| # Performance Target: <2 minutes | |
| validate-version: | |
| name: Validate Version & Find Artifacts | |
| needs: select-runner | |
| runs-on: ${{ needs.select-runner.outputs.linux }} | |
| outputs: | |
| version: ${{ steps.extract.outputs.version }} | |
| commit-sha: ${{ steps.validate.outputs.commit-sha }} | |
| build-run-id: ${{ steps.find-artifacts.outputs.run-id }} | |
| steps: | |
| - name: Install GitHub CLI (self-hosted runner) | |
| run: | | |
| if ! command -v gh &> /dev/null; then | |
| echo "📦 Installing GitHub CLI..." | |
| # Detect if running as root (self-hosted runners may run as root) | |
| if [ "$(id -u)" -eq 0 ]; then | |
| SUDO="" | |
| else | |
| SUDO="sudo" | |
| fi | |
| type -p curl >/dev/null || ($SUDO apt update && $SUDO apt install curl -y) | |
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | $SUDO dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | |
| $SUDO chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | $SUDO tee /etc/apt/sources.list.d/github-cli.list > /dev/null | |
| $SUDO apt update | |
| $SUDO apt install gh -y | |
| echo "✅ GitHub CLI installed" | |
| else | |
| echo "✅ GitHub CLI already installed" | |
| fi | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for release notes | |
| - name: Extract version from tag | |
| id: extract | |
| run: | | |
| # Get tag name (either from push event or manual input) | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| TAG_NAME="${{ github.event.inputs.tag }}" | |
| else | |
| TAG_NAME="${GITHUB_REF#refs/tags/}" | |
| fi | |
| # Remove 'v' prefix to get version | |
| VERSION="${TAG_NAME#v}" | |
| echo "📦 Tag: $TAG_NAME" | |
| echo "🔢 Version: $VERSION" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Validate semantic version format | |
| run: | | |
| VERSION="${{ steps.extract.outputs.version }}" | |
| # Allow semver with optional pre-release/build metadata | |
| # Valid formats: 1.2.3, 1.2.3-beta.1, 1.2.3+build.123, 1.2.3-rc.1+build.456 | |
| if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$'; then | |
| echo "❌ Error: Invalid semantic version format: $VERSION" | |
| echo "Expected format: MAJOR.MINOR.PATCH with optional pre-release and build metadata" | |
| echo "Examples: 1.2.3, 0.1.0-beta.1, 1.0.0-rc.1+build.123" | |
| exit 1 | |
| fi | |
| echo "✅ Valid version format: $VERSION" | |
| - name: Validate tag is on main branch | |
| id: validate | |
| run: | | |
| # Get the commit SHA for this tag | |
| COMMIT_SHA=$(git rev-list -n 1 ${{ github.ref }}) | |
| echo "commit-sha=$COMMIT_SHA" >> $GITHUB_OUTPUT | |
| echo "📍 Tag commit: $COMMIT_SHA" | |
| # For manual testing, allow any branch | |
| # For production tag push, enforce main branch only | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| # Check if this commit is on main branch | |
| if ! git branch -r --contains $COMMIT_SHA | grep -q "origin/main"; then | |
| echo "❌ Error: Tag must be on a commit that exists on main branch" | |
| echo "Please merge to main first, then tag the merge commit" | |
| exit 1 | |
| fi | |
| echo "✅ Tag is on main branch" | |
| else | |
| echo "⚠️ Manual dispatch: Skipping main branch check" | |
| echo "✅ Running on branch: ${{ github.ref_name }}" | |
| fi | |
| - name: Check for duplicate version in releases | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ steps.extract.outputs.version }}" | |
| # Check if this version already exists as a release | |
| if gh release view "v$VERSION" &>/dev/null; then | |
| # For test versions, just warn instead of failing | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ] && echo "$VERSION" | grep -q -- "-"; then | |
| echo "⚠️ Warning: Release v$VERSION already exists (test version)" | |
| echo "💡 Consider using a different test version or deleting the existing one" | |
| else | |
| echo "❌ Error: Release v$VERSION already exists" | |
| echo "Please use a new version number" | |
| exit 1 | |
| fi | |
| else | |
| echo "✅ Version v$VERSION is unique" | |
| fi | |
| - name: Find build workflow artifacts | |
| id: find-artifacts | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| COMMIT_SHA="${{ steps.validate.outputs.commit-sha }}" | |
| echo "🔍 Looking for build workflow run for commit $COMMIT_SHA..." | |
| # Find the build workflow run for this commit | |
| RUN_ID=$(gh run list \ | |
| --workflow=build.yml \ | |
| --commit=$COMMIT_SHA \ | |
| --status=success \ | |
| --json databaseId \ | |
| --jq '.[0].databaseId') | |
| if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then | |
| echo "❌ Error: No successful build workflow run found for commit $COMMIT_SHA" | |
| echo "" | |
| echo "The release workflow reuses artifacts from the build workflow to avoid redundancy." | |
| echo "Please ensure:" | |
| echo " 1. Code has been merged to main branch" | |
| echo " 2. Build workflow has completed successfully" | |
| echo " 3. Tag is created on the same commit that was built" | |
| echo "" | |
| echo "Typical workflow:" | |
| echo " 1. Merge PR to main → triggers build.yml" | |
| echo " 2. Wait for build to complete" | |
| echo " 3. Tag the merge commit: git tag v1.0.0 && git push origin v1.0.0" | |
| exit 1 | |
| fi | |
| echo "run-id=$RUN_ID" >> $GITHUB_OUTPUT | |
| echo "✅ Found build workflow run: $RUN_ID" | |
| echo "🔗 https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" | |
| # Job 2: Download Build Artifacts | |
| # Reuse artifacts from the build workflow to avoid redundant builds | |
| # Performance Target: <1 minute | |
| download-artifacts: | |
| name: Download Build Artifacts | |
| runs-on: ${{ needs.select-runner.outputs.linux }} | |
| needs: [select-runner, validate-version] | |
| steps: | |
| - name: Install GitHub CLI (self-hosted runner) | |
| run: | | |
| if ! command -v gh &> /dev/null; then | |
| echo "📦 Installing GitHub CLI..." | |
| # Detect if running as root (self-hosted runners may run as root) | |
| if [ "$(id -u)" -eq 0 ]; then | |
| SUDO="" | |
| else | |
| SUDO="sudo" | |
| fi | |
| type -p curl >/dev/null || ($SUDO apt update && $SUDO apt install curl -y) | |
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | $SUDO dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | |
| $SUDO chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | $SUDO tee /etc/apt/sources.list.d/github-cli.list > /dev/null | |
| $SUDO apt update | |
| $SUDO apt install gh -y | |
| echo "✅ GitHub CLI installed" | |
| else | |
| echo "✅ GitHub CLI already installed" | |
| fi | |
| - name: Download artifacts from build workflow | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| RUN_ID="${{ needs.validate-version.outputs.build-run-id }}" | |
| echo "📥 Downloading artifacts from build run $RUN_ID..." | |
| # Download all three platform artifacts | |
| gh run download $RUN_ID \ | |
| --repo ${{ github.repository }} \ | |
| --dir ./artifacts | |
| echo "✅ Downloaded artifacts:" | |
| ls -lah ./artifacts/ | |
| # Verify all expected artifacts exist | |
| for artifact in ten-second-tom-osx-x64 ten-second-tom-osx-arm64 ten-second-tom-win-x64; do | |
| if [ ! -d "./artifacts/$artifact" ]; then | |
| echo "❌ Error: Expected artifact '$artifact' not found" | |
| exit 1 | |
| fi | |
| echo " ✅ $artifact" | |
| done | |
| - name: Upload artifacts for release jobs | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: ./artifacts/ | |
| retention-days: 90 | |
| # Job 3: Create GitHub Release | |
| # Generate release notes and create GitHub release with all binaries | |
| # Performance Target: ≤2 minutes | |
| create-github-release: | |
| name: Create GitHub Release | |
| runs-on: ${{ needs.select-runner.outputs.linux }} | |
| needs: [select-runner, validate-version, download-artifacts] | |
| outputs: | |
| release-id: ${{ steps.create-release.outputs.id }} | |
| upload-url: ${{ steps.create-release.outputs.upload_url }} | |
| steps: | |
| - name: Install GitHub CLI (self-hosted runner) | |
| run: | | |
| if ! command -v gh &> /dev/null; then | |
| echo "📦 Installing GitHub CLI..." | |
| # Detect if running as root (self-hosted runners may run as root) | |
| if [ "$(id -u)" -eq 0 ]; then | |
| SUDO="" | |
| else | |
| SUDO="sudo" | |
| fi | |
| type -p curl >/dev/null || ($SUDO apt update && $SUDO apt install curl -y) | |
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | $SUDO dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | |
| $SUDO chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | $SUDO tee /etc/apt/sources.list.d/github-cli.list > /dev/null | |
| $SUDO apt update | |
| $SUDO apt install gh -y | |
| echo "✅ GitHub CLI installed" | |
| else | |
| echo "✅ GitHub CLI already installed" | |
| fi | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for release notes | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: ./artifacts | |
| - name: Generate release notes | |
| id: release-notes | |
| run: | | |
| VERSION="v${{ needs.validate-version.outputs.version }}" | |
| # Get previous tag for changelog | |
| PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") | |
| if [ -z "$PREV_TAG" ]; then | |
| echo "📝 First release - no previous tag found" | |
| NOTES="## 🎉 Initial Release | |
| This is the first release of Ten Second Tom. | |
| ### 📦 Downloads | |
| Choose the appropriate binary for your platform: | |
| - **macOS (Intel)**: \`tom\` (from ten-second-tom-osx-x64) | |
| - **macOS (Apple Silicon)**: \`tom\` (from ten-second-tom-osx-arm64) | |
| - **Windows**: \`tom.exe\` (from ten-second-tom-win-x64) | |
| ### ✅ Verification | |
| Use \`shasum -a 256\` or \`Get-FileHash\` to verify the downloaded binaries." | |
| else | |
| echo "📝 Generating changelog from $PREV_TAG to $VERSION" | |
| # Generate commit log | |
| COMMITS=$(git log $PREV_TAG..HEAD --pretty=format:"- %s (%h)" --no-merges) | |
| NOTES="## 📋 Changes | |
| $COMMITS | |
| ### 📦 Downloads | |
| Choose the appropriate binary for your platform: | |
| - **macOS (Intel)**: \`tom\` (from ten-second-tom-osx-x64) | |
| - **macOS (Apple Silicon)**: \`tom\` (from ten-second-tom-osx-arm64) | |
| - **Windows**: \`tom.exe\` (from ten-second-tom-win-x64) | |
| ### ✅ Verification | |
| Use \`shasum -a 256\` or \`Get-FileHash\` to verify the downloaded binaries." | |
| fi | |
| # Save notes to file (handle multiline) | |
| echo "$NOTES" > release-notes.md | |
| cat release-notes.md | |
| - name: Create GitHub Release | |
| id: create-release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="v${{ needs.validate-version.outputs.version }}" | |
| # Check if this is a dry run | |
| if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then | |
| echo "🧪 DRY RUN - Would create release $VERSION" | |
| echo "📝 Release notes:" | |
| cat release-notes.md | |
| echo "⏭️ Skipping actual release creation" | |
| exit 0 | |
| fi | |
| # Create release with notes | |
| gh release create "$VERSION" \ | |
| --title "Release $VERSION" \ | |
| --notes-file release-notes.md \ | |
| --draft=false \ | |
| --latest | |
| echo "✅ GitHub release created: $VERSION" | |
| # Check if this is a dry run | |
| if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then | |
| echo "🧪 DRY RUN - Skipping artifact uploads" | |
| exit 0 | |
| fi | |
| # Upload all artifacts | |
| echo "📤 Uploading artifacts..." | |
| # Upload macOS x64 | |
| gh release upload "$VERSION" \ | |
| ./artifacts/ten-second-tom-osx-x64/tom \ | |
| --clobber | |
| # Upload macOS ARM64 | |
| gh release upload "$VERSION" \ | |
| ./artifacts/ten-second-tom-osx-arm64/tom \ | |
| --clobber | |
| # Upload Windows x64 | |
| gh release upload "$VERSION" \ | |
| ./artifacts/ten-second-tom-win-x64/tom.exe \ | |
| --clobber | |
| echo "✅ All artifacts uploaded" | |
| # Job 4: Publish to Homebrew with Bottles | |
| # Create Homebrew bottles and upload to GitHub Packages, then update tap formula | |
| # Performance Target: ≤7 minutes | |
| # Requires: HOMEBREW_TAP_TOKEN secret configured in 'production' environment | |
| # Note: VS Code may show "'production' is not valid" - this is a false positive | |
| # Can be skipped with workflow_dispatch skip_homebrew input for PR testing | |
| publish-homebrew: | |
| name: Publish to Homebrew | |
| runs-on: macos-latest # Changed to macOS for bottle creation | |
| needs: [validate-version, create-github-release] | |
| if: ${{ github.event.inputs.skip_homebrew != 'true' && github.event.inputs.dry_run != 'true' }} | |
| environment: | |
| name: production # Configured in GitHub repository settings | |
| permissions: | |
| contents: write # Required for uploading bottles to release | |
| packages: write # Required for GitHub Packages (bottles) | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-artifacts | |
| path: ./artifacts | |
| - name: Create Homebrew bottles | |
| id: bottles | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| echo "🍾 Creating Homebrew bottles for version $VERSION" | |
| # Determine macOS version for bottle tag | |
| MACOS_VERSION=$(sw_vers -productVersion | cut -d '.' -f 1) | |
| case $MACOS_VERSION in | |
| 12) MACOS_TAG="monterey" ;; | |
| 13) MACOS_TAG="ventura" ;; | |
| 14) MACOS_TAG="sonoma" ;; | |
| 15) MACOS_TAG="sequoia" ;; | |
| *) MACOS_TAG="monterey" ;; # Default fallback | |
| esac | |
| echo "📦 Building bottles for macOS $MACOS_TAG (both architectures)" | |
| # Create ARM64 bottle | |
| BOTTLE_TAG_ARM64="arm64_${MACOS_TAG}" | |
| BOTTLE_NAME_ARM64="ten-second-tom--${VERSION}.${BOTTLE_TAG_ARM64}.bottle.tar.gz" | |
| BOTTLE_DIR_ARM64="ten-second-tom/${VERSION}/bin" | |
| mkdir -p "$BOTTLE_DIR_ARM64" | |
| cp "./artifacts/ten-second-tom-osx-arm64/tom" "$BOTTLE_DIR_ARM64/tom" | |
| chmod +x "$BOTTLE_DIR_ARM64/tom" | |
| tar czf "$BOTTLE_NAME_ARM64" ten-second-tom | |
| BOTTLE_SHA_ARM64=$(shasum -a 256 "$BOTTLE_NAME_ARM64" | cut -d ' ' -f 1) | |
| echo "✅ ARM64 bottle: $BOTTLE_NAME_ARM64" | |
| echo " SHA256: $BOTTLE_SHA_ARM64" | |
| # Clean up for x64 bottle | |
| rm -rf ten-second-tom | |
| # Create x64 bottle | |
| BOTTLE_TAG_X64="${MACOS_TAG}" | |
| BOTTLE_NAME_X64="ten-second-tom--${VERSION}.${BOTTLE_TAG_X64}.bottle.tar.gz" | |
| BOTTLE_DIR_X64="ten-second-tom/${VERSION}/bin" | |
| mkdir -p "$BOTTLE_DIR_X64" | |
| cp "./artifacts/ten-second-tom-osx-x64/tom" "$BOTTLE_DIR_X64/tom" | |
| chmod +x "$BOTTLE_DIR_X64/tom" | |
| tar czf "$BOTTLE_NAME_X64" ten-second-tom | |
| BOTTLE_SHA_X64=$(shasum -a 256 "$BOTTLE_NAME_X64" | cut -d ' ' -f 1) | |
| echo "✅ x64 bottle: $BOTTLE_NAME_X64" | |
| echo " SHA256: $BOTTLE_SHA_X64" | |
| # Output variables for later steps | |
| echo "bottle-name-arm64=$BOTTLE_NAME_ARM64" >> $GITHUB_OUTPUT | |
| echo "bottle-name-x64=$BOTTLE_NAME_X64" >> $GITHUB_OUTPUT | |
| echo "bottle-tag-arm64=$BOTTLE_TAG_ARM64" >> $GITHUB_OUTPUT | |
| echo "bottle-tag-x64=$BOTTLE_TAG_X64" >> $GITHUB_OUTPUT | |
| echo "bottle-sha-arm64=$BOTTLE_SHA_ARM64" >> $GITHUB_OUTPUT | |
| echo "bottle-sha-x64=$BOTTLE_SHA_X64" >> $GITHUB_OUTPUT | |
| - name: Upload bottles to GitHub release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| BOTTLE_NAME_ARM64="${{ steps.bottles.outputs.bottle-name-arm64 }}" | |
| BOTTLE_NAME_X64="${{ steps.bottles.outputs.bottle-name-x64 }}" | |
| echo "📤 Uploading bottles to GitHub release" | |
| # Upload both bottles to GitHub release | |
| gh release upload "v${VERSION}" "$BOTTLE_NAME_ARM64" "$BOTTLE_NAME_X64" --clobber | |
| echo "✅ Bottles uploaded to release assets" | |
| echo "📦 ARM64: https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${BOTTLE_NAME_ARM64}" | |
| echo "📦 x64: https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${BOTTLE_NAME_X64}" | |
| - name: Generate Homebrew formula with bottle blocks | |
| id: formula | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| REPO_NAME="${{ github.event.repository.name }}" | |
| BOTTLE_TAG_ARM64="${{ steps.bottles.outputs.bottle-tag-arm64 }}" | |
| BOTTLE_TAG_X64="${{ steps.bottles.outputs.bottle-tag-x64 }}" | |
| BOTTLE_SHA_ARM64="${{ steps.bottles.outputs.bottle-sha-arm64 }}" | |
| BOTTLE_SHA_X64="${{ steps.bottles.outputs.bottle-sha-x64 }}" | |
| echo "📝 Generating Homebrew formula for version $VERSION with bottle support" | |
| # Download source tarball to calculate SHA256 | |
| SOURCE_TARBALL_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/v${VERSION}.tar.gz" | |
| curl -sSL "$SOURCE_TARBALL_URL" -o source.tar.gz | |
| SOURCE_SHA=$(shasum -a 256 source.tar.gz | cut -d ' ' -f 1) | |
| echo "Source tarball SHA256: $SOURCE_SHA" | |
| # Create formula file with bottle block using echo to avoid heredoc YAML issues | |
| # Calculate spacing for bottle digest alignment | |
| ARM64_LEN=${#BOTTLE_TAG_ARM64} | |
| X64_LEN=${#BOTTLE_TAG_X64} | |
| # Add spaces to shorter tag to align the SHA256 values | |
| if [ $ARM64_LEN -gt $X64_LEN ]; then | |
| SPACES=$((ARM64_LEN - X64_LEN)) | |
| X64_PADDING=$(printf '%*s' $SPACES '') | |
| ARM64_PADDING="" | |
| else | |
| SPACES=$((X64_LEN - ARM64_LEN)) | |
| ARM64_PADDING=$(printf '%*s' $SPACES '') | |
| X64_PADDING="" | |
| fi | |
| { | |
| echo "class TenSecondTom < Formula" | |
| echo " desc \"CLI tool for daily work summaries using Claude AI\"" | |
| echo " homepage \"https://github.com/${REPO_OWNER}/${REPO_NAME}\"" | |
| echo " url \"https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/v${VERSION}.tar.gz\"" | |
| echo " sha256 \"${SOURCE_SHA}\"" | |
| echo " license \"MIT\"" | |
| echo "" | |
| echo " # Bottles (pre-built binaries) for fast installation" | |
| echo " bottle do" | |
| echo " root_url \"https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/v${VERSION}\"" | |
| echo " sha256 cellar: :any_skip_relocation, ${BOTTLE_TAG_ARM64}:${ARM64_PADDING} \"${BOTTLE_SHA_ARM64}\"" | |
| echo " sha256 cellar: :any_skip_relocation, ${BOTTLE_TAG_X64}:${X64_PADDING} \"${BOTTLE_SHA_X64}\"" | |
| echo " end" | |
| echo "" | |
| echo " def install" | |
| echo " bin.install \"tom\"" | |
| echo " end" | |
| echo "" | |
| echo " test do" | |
| echo " system \"#{bin}/tom\", \"--version\"" | |
| echo " end" | |
| echo "end" | |
| } > ten-second-tom.rb | |
| cat ten-second-tom.rb | |
| - name: Validate formula syntax | |
| run: | | |
| echo "✅ Formula syntax validation (basic check)" | |
| # Basic syntax validation - full validation requires Homebrew installation | |
| if ! grep -q "class TenSecondTom" ten-second-tom.rb; then | |
| echo "❌ Error: Invalid formula structure" | |
| exit 1 | |
| fi | |
| # Check that source URL contains the version | |
| if ! grep -q "archive/refs/tags/v${{ needs.validate-version.outputs.version }}.tar.gz" ten-second-tom.rb; then | |
| echo "❌ Error: Version not found in source URL" | |
| exit 1 | |
| fi | |
| # Check that source has SHA256 | |
| if ! grep -q "sha256 \"" ten-second-tom.rb; then | |
| echo "❌ Error: Source SHA256 not found in formula" | |
| exit 1 | |
| fi | |
| if ! grep -q "bottle do" ten-second-tom.rb; then | |
| echo "❌ Error: Bottle block not found in formula" | |
| exit 1 | |
| fi | |
| echo "✅ Basic formula validation passed (including bottle block)" | |
| - name: Push formula to Homebrew tap | |
| env: | |
| # Note: This secret is configured in the 'production' environment | |
| # Go to Settings → Environments → production → Environment secrets | |
| TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| run: | | |
| if [ -z "$TAP_TOKEN" ]; then | |
| echo "❌ Error: HOMEBREW_TAP_TOKEN secret not configured" | |
| echo "Please configure the secret in the 'production' environment" | |
| echo "Settings → Environments → production → Environment secrets" | |
| exit 1 | |
| fi | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| TAP_REPO="homebrew-ten-second-tom" | |
| echo "📤 Pushing formula with bottles to ${REPO_OWNER}/${TAP_REPO}" | |
| # Clone tap repository | |
| git clone "https://x-access-token:${TAP_TOKEN}@github.com/${REPO_OWNER}/${TAP_REPO}.git" tap-repo | |
| cd tap-repo | |
| # Configure git | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Copy formula | |
| mkdir -p Formula | |
| cp ../ten-second-tom.rb Formula/ten-second-tom.rb | |
| # Commit and push | |
| git add Formula/ten-second-tom.rb | |
| git commit -m "Release version ${VERSION} with bottles" | |
| git push origin main | |
| echo "✅ Formula with bottle support published to Homebrew tap" | |
| # Job 5: Document Winget Publication | |
| # Generate Winget manifest and create issue for manual publication (Phase 1) | |
| # Performance Target: <1 minute | |
| # Note: Phase 2 will automate the publication process | |
| document-winget: | |
| name: Document Winget Publication | |
| runs-on: ${{ needs.select-runner.outputs.linux }} | |
| needs: [select-runner, validate-version, create-github-release] | |
| if: success() # Run even if other jobs fail | |
| steps: | |
| - name: Install GitHub CLI (self-hosted runner) | |
| run: | | |
| if ! command -v gh &> /dev/null; then | |
| echo "📦 Installing GitHub CLI..." | |
| # Detect if running as root (self-hosted runners may run as root) | |
| if [ "$(id -u)" -eq 0 ]; then | |
| SUDO="" | |
| else | |
| SUDO="sudo" | |
| fi | |
| type -p curl >/dev/null || ($SUDO apt update && $SUDO apt install curl -y) | |
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | $SUDO dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | |
| $SUDO chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | $SUDO tee /etc/apt/sources.list.d/github-cli.list > /dev/null | |
| $SUDO apt update | |
| $SUDO apt install gh -y | |
| echo "✅ GitHub CLI installed" | |
| else | |
| echo "✅ GitHub CLI already installed" | |
| fi | |
| - name: Generate Winget manifest template | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| REPO_NAME="${{ github.event.repository.name }}" | |
| cat > winget-manifest.yaml <<EOF | |
| # Winget Package Manifest | |
| # This manifest should be submitted to microsoft/winget-pkgs repository | |
| # Manual submission process (Phase 1) - will be automated in Phase 2 | |
| PackageIdentifier: ${REPO_OWNER}.TenSecondTom | |
| PackageVersion: ${VERSION} | |
| PackageName: Ten Second Tom | |
| Publisher: ${REPO_OWNER} | |
| License: MIT | |
| ShortDescription: CLI tool for daily work summaries using Claude AI | |
| PackageUrl: https://github.com/${REPO_OWNER}/${REPO_NAME} | |
| Installers: | |
| - Architecture: x64 | |
| InstallerType: zip | |
| InstallerUrl: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/v${VERSION}/tom.exe | |
| # TODO: Add SHA256 checksum from release artifact | |
| ManifestType: singleton | |
| ManifestVersion: 1.0.0 | |
| EOF | |
| cat winget-manifest.yaml | |
| - name: Create GitHub issue for manual publication | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| gh issue create \ | |
| --repo ${{ github.repository }} \ | |
| --title "📦 Publish v${VERSION} to Winget" \ | |
| --body "## Manual Winget Publication Required | |
| Version **v${VERSION}** has been released and needs to be published to Winget. | |
| ### Steps: | |
| 1. Fork microsoft/winget-pkgs repository | |
| 2. Use the generated manifest template (see workflow artifacts) | |
| 3. Create a PR to microsoft/winget-pkgs | |
| 4. Wait for validation and merge | |
| ### References: | |
| - [Winget Package Manifest](https://github.com/microsoft/winget-pkgs) | |
| - [Release v${VERSION}](https://github.com/${{ github.repository }}/releases/tag/v${VERSION}) | |
| ### Automation: | |
| Phase 2 will automate this process." \ | |
| --label "release,winget" || echo "⚠️ Issue creation skipped (may already exist)" | |
| # Job 6: Document Chocolatey Publication | |
| # Generate Chocolatey nuspec and create issue for manual publication (Phase 1) | |
| # Performance Target: <1 minute | |
| # Note: Phase 2 will automate the publication process | |
| document-chocolatey: | |
| name: Document Chocolatey Publication | |
| runs-on: ${{ needs.select-runner.outputs.linux }} | |
| needs: [select-runner, validate-version, create-github-release] | |
| if: success() # Run even if other jobs fail | |
| steps: | |
| - name: Install GitHub CLI (self-hosted runner) | |
| run: | | |
| if ! command -v gh &> /dev/null; then | |
| echo "📦 Installing GitHub CLI..." | |
| # Detect if running as root (self-hosted runners may run as root) | |
| if [ "$(id -u)" -eq 0 ]; then | |
| SUDO="" | |
| else | |
| SUDO="sudo" | |
| fi | |
| type -p curl >/dev/null || ($SUDO apt update && $SUDO apt install curl -y) | |
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | $SUDO dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg | |
| $SUDO chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | $SUDO tee /etc/apt/sources.list.d/github-cli.list > /dev/null | |
| $SUDO apt update | |
| $SUDO apt install gh -y | |
| echo "✅ GitHub CLI installed" | |
| else | |
| echo "✅ GitHub CLI already installed" | |
| fi | |
| - name: Generate Chocolatey nuspec template | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| REPO_NAME="${{ github.event.repository.name }}" | |
| cat > ten-second-tom.nuspec <<EOF | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <!-- Chocolatey Package Specification --> | |
| <!-- Manual submission process (Phase 1) - will be automated in Phase 2 --> | |
| <package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"> | |
| <metadata> | |
| <id>ten-second-tom</id> | |
| <version>${VERSION}</version> | |
| <title>Ten Second Tom</title> | |
| <authors>${REPO_OWNER}</authors> | |
| <projectUrl>https://github.com/${REPO_OWNER}/${REPO_NAME}</projectUrl> | |
| <licenseUrl>https://github.com/${REPO_OWNER}/${REPO_NAME}/blob/main/LICENSE</licenseUrl> | |
| <requireLicenseAcceptance>false</requireLicenseAcceptance> | |
| <description>CLI tool for daily work summaries using Claude AI</description> | |
| <summary>Daily work summary CLI powered by Claude AI</summary> | |
| <tags>cli productivity ai claude summary</tags> | |
| </metadata> | |
| <files> | |
| <file src="tools\**" target="tools" /> | |
| </files> | |
| </package> | |
| EOF | |
| cat ten-second-tom.nuspec | |
| - name: Create GitHub issue for manual publication | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| gh issue create \ | |
| --repo ${{ github.repository }} \ | |
| --title "📦 Publish v${VERSION} to Chocolatey" \ | |
| --body "## Manual Chocolatey Publication Required | |
| Version **v${VERSION}** has been released and needs to be published to Chocolatey. | |
| ### Steps: | |
| 1. Create Chocolatey account (if not exists) | |
| 2. Use the generated nuspec template (see workflow artifacts) | |
| 3. Package and test locally: \`choco pack\` | |
| 4. Publish: \`choco push ten-second-tom.${VERSION}.nupkg --source https://push.chocolatey.org/\` | |
| ### References: | |
| - [Chocolatey Package Docs](https://docs.chocolatey.org/en-us/create/create-packages) | |
| - [Release v${VERSION}](https://github.com/${{ github.repository }}/releases/tag/v${VERSION}) | |
| ### Automation: | |
| Phase 2 will automate this process." \ | |
| --label "release,chocolatey" || echo "⚠️ Issue creation skipped (may already exist)" |