Release #60
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 | |
| skip_winget: | |
| description: 'Skip Winget 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: | |
| # Job 1: Validate Version | |
| # Extract/validate version, ensure tag is on main | |
| # Performance Target: <1 minute | |
| validate-version: | |
| name: Validate Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.extract.outputs.version }} | |
| commit-sha: ${{ steps.validate.outputs.commit-sha }} | |
| steps: | |
| - 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 | |
| # Job 2: Build Release Artifacts | |
| # Rebuild binaries with release version embedded (not reusing dev builds) | |
| # Performance Target: ≤10 minutes (parallel execution) | |
| build-release-artifacts: | |
| name: Build ${{ matrix.platform }} Release | |
| runs-on: ${{ matrix.os }} | |
| needs: [validate-version] | |
| strategy: | |
| matrix: | |
| include: | |
| - os: macos-latest | |
| platform: osx-arm64 | |
| runtime: osx-arm64 | |
| executable: tom | |
| - os: windows-latest | |
| platform: win-x64 | |
| runtime: win-x64 | |
| executable: tom.exe | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.validate-version.outputs.commit-sha }} | |
| - name: Setup .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '9.0.x' | |
| - name: Restore dependencies | |
| run: dotnet restore | |
| - name: Build macOS Extension | |
| if: startsWith(matrix.platform, 'osx-') | |
| shell: bash | |
| run: | | |
| echo "🔨 Building macOS Extension for ${{ matrix.platform }}" | |
| chmod +x src/Extensions/MacOS/build.sh | |
| cd src/Extensions/MacOS && ./build.sh | |
| # Verify extension was built | |
| if [ ! -f ../../../bin/TenSecondTom.Extensions.MacOS.app/Contents/MacOS/notifier ]; then | |
| echo "❌ Error: Extension binary not found after build" | |
| exit 1 | |
| fi | |
| echo "✅ Extension built successfully" | |
| ls -la ../../../bin/TenSecondTom.Extensions.MacOS.app/Contents/MacOS/ | |
| - name: Publish ${{ matrix.platform }} executable with release version | |
| shell: bash | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| echo "📦 Building ${{ matrix.platform }} with version: ${VERSION}" | |
| dotnet publish src/TenSecondTom.csproj \ | |
| --configuration Release \ | |
| --runtime ${{ matrix.runtime }} \ | |
| --self-contained true \ | |
| --output ./publish \ | |
| -p:PublishSingleFile=true \ | |
| -p:Version=${VERSION} | |
| - name: Verify Extension in Publish Output | |
| if: startsWith(matrix.platform, 'osx-') | |
| shell: bash | |
| run: | | |
| EXTENSION_PATH="./publish/TenSecondTom.Extensions.MacOS.app/Contents/MacOS/notifier" | |
| if [ ! -f "$EXTENSION_PATH" ]; then | |
| echo "❌ Error: Extension not found in publish output at $EXTENSION_PATH" | |
| echo "Contents of publish directory:" | |
| find ./publish -type f | |
| exit 1 | |
| fi | |
| echo "✅ Extension verified in publish directory" | |
| ls -lh "$EXTENSION_PATH" | |
| - name: Rename executable to 'tom' or 'tom.exe' | |
| shell: bash | |
| run: | | |
| if [ "${{ matrix.platform }}" = "win-x64" ]; then | |
| mv ./publish/TenSecondTom.exe ./publish/tom.exe | |
| else | |
| mv ./publish/TenSecondTom ./publish/tom | |
| fi | |
| - name: Verify artifact size | |
| shell: bash | |
| run: | | |
| if [ "${{ runner.os }}" = "Windows" ]; then | |
| SIZE=$(stat -c%s ./publish/${{ matrix.executable }}) | |
| else | |
| SIZE=$(stat -f%z ./publish/${{ matrix.executable }}) | |
| fi | |
| SIZE_MB=$((SIZE / 1024 / 1024)) | |
| echo "📦 Executable size: ${SIZE_MB}MB" | |
| # Size limit accounts for: | |
| # - Main binary (~28MB) | |
| # - Microsoft AI Foundry Local SDK (~194MB) | |
| # - Whisper.NET platform runtime (~5-100MB depending on CUDA/CoreML) | |
| # Total expected: ~230-330MB | |
| if [ $SIZE -gt 367001600 ]; then | |
| echo "❌ Error: Executable exceeds 350MB limit (${SIZE_MB}MB)" | |
| exit 1 | |
| fi | |
| echo "✅ Size check passed: ${SIZE_MB}MB" | |
| - name: Calculate checksum | |
| shell: bash | |
| run: | | |
| cd ./publish | |
| if [ "${{ runner.os }}" = "Windows" ]; then | |
| sha256sum ${{ matrix.executable }} > ${{ matrix.executable }}.sha256 | |
| else | |
| shasum -a 256 ${{ matrix.executable }} > ${{ matrix.executable }}.sha256 | |
| fi | |
| echo "📝 SHA256:" | |
| cat ${{ matrix.executable }}.sha256 | |
| - name: Smoke test - version command | |
| shell: bash | |
| run: | | |
| if [ "${{ runner.os }}" != "Windows" ]; then | |
| chmod +x ./publish/${{ matrix.executable }} | |
| fi | |
| echo "🧪 Running smoke test: --version" | |
| ./publish/${{ matrix.executable }} --version | |
| if [ $? -eq 0 ]; then | |
| echo "✅ Smoke test passed" | |
| else | |
| echo "❌ Smoke test failed" | |
| exit 1 | |
| fi | |
| - name: List artifact contents | |
| shell: bash | |
| run: | | |
| echo "📦 Contents of publish directory:" | |
| find ./publish -type f -exec ls -lh {} \; | head -50 | |
| echo "" | |
| echo "📊 Total size:" | |
| du -sh ./publish/ | |
| echo "" | |
| echo "📊 Size by type:" | |
| du -sh ./publish/*.dylib 2>/dev/null || true | |
| du -sh ./publish/*.dll 2>/dev/null || true | |
| du -sh ./publish/runtimes 2>/dev/null || true | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ten-second-tom-${{ matrix.platform }}-release | |
| path: | | |
| ./publish/${{ matrix.executable }} | |
| ./publish/appsettings*.json | |
| ./publish/*.dylib | |
| ./publish/*.dll | |
| ./publish/*.metal | |
| ./publish/runtimes/**/* | |
| ./publish/TenSecondTom.Extensions.MacOS.app/**/* | |
| 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: ubuntu-latest | |
| needs: [validate-version, build-release-artifacts] | |
| outputs: | |
| release-id: ${{ steps.create-release.outputs.id }} | |
| upload-url: ${{ steps.create-release.outputs.upload_url }} | |
| steps: | |
| - 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: | |
| pattern: ten-second-tom-*-release | |
| path: ./artifacts | |
| - name: Package artifacts for distribution | |
| run: | | |
| VERSION="${{ needs.validate-version.outputs.version }}" | |
| echo "📦 Creating distribution packages for version ${VERSION}" | |
| mkdir -p ./release-packages | |
| # Package macOS ARM64 | |
| echo "Packaging macOS ARM64..." | |
| cd ./artifacts/ten-second-tom-osx-arm64-release | |
| tar czf ../../release-packages/ten-second-tom-${VERSION}-osx-arm64.tar.gz * | |
| cd ../.. | |
| # Package Windows x64 | |
| echo "Packaging Windows x64..." | |
| cd ./artifacts/ten-second-tom-win-x64-release | |
| zip -r ../../release-packages/ten-second-tom-${VERSION}-win-x64.zip * | |
| cd ../.. | |
| echo "✅ Distribution packages created:" | |
| ls -lh ./release-packages/ | |
| - 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 package for your platform: | |
| - **macOS (Apple Silicon)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz\` | |
| - **Windows**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-win-x64.zip\` | |
| ### 🚀 Installation | |
| **macOS (Apple Silicon):** | |
| \`\`\`bash | |
| tar xzf ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz | |
| chmod +x tom | |
| ./tom --version | |
| \`\`\` | |
| **Windows:** | |
| Extract the zip file and run \`tom.exe\` from the extracted folder." | |
| 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 package for your platform: | |
| - **macOS (Apple Silicon)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz\` | |
| - **Windows**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-win-x64.zip\` | |
| ### 🚀 Installation | |
| **macOS (Apple Silicon):** | |
| \`\`\`bash | |
| tar xzf ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-arm64.tar.gz | |
| chmod +x tom | |
| ./tom --version | |
| \`\`\` | |
| **Windows:** | |
| Extract the zip file and run \`tom.exe\` from the extracted folder." | |
| 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 distribution packages | |
| echo "📤 Uploading distribution packages..." | |
| gh release upload "$VERSION" \ | |
| ./release-packages/* \ | |
| --clobber | |
| echo "✅ All distribution packages 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: | |
| pattern: ten-second-tom-*-release | |
| 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-release/tom" "$BOTTLE_DIR_ARM64/tom" | |
| cp ./artifacts/ten-second-tom-osx-arm64-release/appsettings*.json "$BOTTLE_DIR_ARM64/" 2>/dev/null || true | |
| cp ./artifacts/ten-second-tom-osx-arm64-release/*.dylib "$BOTTLE_DIR_ARM64/" 2>/dev/null || true | |
| cp ./artifacts/ten-second-tom-osx-arm64-release/*.metal "$BOTTLE_DIR_ARM64/" 2>/dev/null || true | |
| # Copy Whisper.NET CoreML runtimes | |
| if [ -d "./artifacts/ten-second-tom-osx-arm64-release/runtimes" ]; then | |
| cp -R "./artifacts/ten-second-tom-osx-arm64-release/runtimes" "$BOTTLE_DIR_ARM64/" | |
| fi | |
| # Copy macOS extension to bottle (in prefix, not bin) | |
| EXTENSION_DIR_ARM64="ten-second-tom/${VERSION}" | |
| cp -R "./artifacts/ten-second-tom-osx-arm64-release/TenSecondTom.Extensions.MacOS.app" "$EXTENSION_DIR_ARM64/" 2>/dev/null || true | |
| 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" | |
| # Output variables for later steps | |
| echo "bottle-name-arm64=$BOTTLE_NAME_ARM64" >> $GITHUB_OUTPUT | |
| echo "bottle-tag-arm64=$BOTTLE_TAG_ARM64" >> $GITHUB_OUTPUT | |
| echo "bottle-sha-arm64=$BOTTLE_SHA_ARM64" >> $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 }}" | |
| echo "📤 Uploading bottle to GitHub release" | |
| # Upload ARM64 bottle to GitHub release | |
| gh release upload "v${VERSION}" "$BOTTLE_NAME_ARM64" --clobber | |
| echo "✅ Bottle uploaded to release assets" | |
| echo "📦 ARM64: https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${BOTTLE_NAME_ARM64}" | |
| - 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_SHA_ARM64="${{ steps.bottles.outputs.bottle-sha-arm64 }}" | |
| echo "📝 Generating Homebrew formula for version $VERSION with ARM64 bottle" | |
| # 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 ARM64 bottle only | |
| { | |
| echo "class TenSecondTom < Formula" | |
| echo " desc \"CLI tool for daily work summaries using Claude AI with voice entry support\"" | |
| 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 " # Bottle (pre-built binary) for Apple Silicon" | |
| 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}: \"${BOTTLE_SHA_ARM64}\"" | |
| echo " end" | |
| echo "" | |
| echo " # Dependencies for voice entry feature" | |
| echo " depends_on \"ffmpeg\"" | |
| echo "" | |
| echo " def install" | |
| echo " bin.install \"tom\"" | |
| echo " # Install native macOS extension for notifications" | |
| echo " prefix.install \"TenSecondTom.Extensions.MacOS.app\" if OS.mac?" | |
| echo " end" | |
| echo "" | |
| echo " def caveats" | |
| echo " <<~EOS" | |
| echo " Legal: Ten Second Tom is designed for single-user personal use on your own" | |
| echo " device. Recording conversations may require consent in your jurisdiction." | |
| echo " EOS" | |
| 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: Publish to Winget | |
| # Automatically submit package update to Windows Package Manager | |
| # Uses winget-releaser action with Komac under the hood | |
| # Performance Target: <2 minutes | |
| # | |
| # Prerequisites: | |
| # 1. WINGET_TOKEN secret configured in 'production' environment | |
| # - Classic PAT with 'public_repo' scope | |
| # - Create at: https://github.com/settings/tokens/new | |
| # 2. Fork of microsoft/winget-pkgs under same account/org | |
| # 3. First version must be submitted manually to winget-pkgs | |
| # | |
| # First-time setup: | |
| # 1. Fork https://github.com/microsoft/winget-pkgs | |
| # 2. Manually submit first version using wingetcreate: | |
| # wingetcreate new https://github.com/sirkirby/ten-second-tom/releases/download/v1.0.0/ten-second-tom-1.0.0-win-x64.zip | |
| # 3. After first version is merged, automation handles all future updates | |
| publish-winget: | |
| name: Publish to Winget | |
| runs-on: ubuntu-latest | |
| needs: [validate-version, create-github-release] | |
| if: ${{ github.event.inputs.skip_winget != 'true' && github.event.inputs.dry_run != 'true' }} | |
| environment: | |
| name: production | |
| steps: | |
| - name: Publish to Winget | |
| uses: vedantmgoyal9/winget-releaser@v2 | |
| with: | |
| identifier: sirkirby.TenSecondTom | |
| installers-regex: '\.zip$' # Match our Windows zip package | |
| token: ${{ secrets.WINGET_TOKEN }} | |
| # Optional: specify fork user if different from repo owner | |
| # fork-user: sirkirby |