feat: enhance macOS notification system with configuration options an… #58
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: | |
| # 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-x64 | |
| runtime: osx-x64 | |
| executable: tom | |
| - 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" | |
| if [ $SIZE -gt 52428800 ]; then | |
| echo "❌ Error: Executable exceeds 50MB 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: 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/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 x64 | |
| echo "Packaging macOS x64..." | |
| cd ./artifacts/ten-second-tom-osx-x64-release | |
| tar czf ../../release-packages/ten-second-tom-${VERSION}-osx-x64.tar.gz * | |
| cd ../.. | |
| # 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 (Intel)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-x64.tar.gz\` | |
| - **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:** | |
| \`\`\`bash | |
| tar xzf ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-x64.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 (Intel)**: \`ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-x64.tar.gz\` | |
| - **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:** | |
| \`\`\`bash | |
| tar xzf ten-second-tom-${{ needs.validate-version.outputs.version }}-osx-x64.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 | |
| # 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" | |
| # 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-release/tom" "$BOTTLE_DIR_X64/tom" | |
| cp ./artifacts/ten-second-tom-osx-x64-release/appsettings*.json "$BOTTLE_DIR_X64/" 2>/dev/null || true | |
| cp ./artifacts/ten-second-tom-osx-x64-release/*.dylib "$BOTTLE_DIR_X64/" 2>/dev/null || true | |
| # Copy macOS extension to bottle (in prefix, not bin) | |
| EXTENSION_DIR_X64="ten-second-tom/${VERSION}" | |
| cp -R "./artifacts/ten-second-tom-osx-x64-release/TenSecondTom.Extensions.MacOS.app" "$EXTENSION_DIR_X64/" 2>/dev/null || true | |
| 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 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 " # 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 " # 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 " Voice Entry Setup (Optional):" | |
| echo "" | |
| echo " For local transcription (privacy-focused, offline):" | |
| echo " 1. Install whisper.cpp: brew install whisper-cpp" | |
| echo " 2. Download model to default location (base.en, 142 MB):" | |
| echo " mkdir -p ~/.cache/whisper" | |
| echo " curl -L -o ~/.cache/whisper/ggml-base.en.bin \\\\" | |
| echo " https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin" | |
| echo " 3. Done! Tom looks for the model at ~/.cache/whisper/ggml-base.en.bin by default" | |
| echo "" | |
| echo " OR use OpenAI transcription (requires API key, no local setup):" | |
| echo " tom today --voice --stt=openai" | |
| echo "" | |
| 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 "" | |
| echo " Storage: Audio recordings use ~4.7MB per 5-minute recording (16kHz mono WAV)." | |
| 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: 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: ubuntu-latest | |
| needs: [validate-version, create-github-release] | |
| if: success() # Run even if other jobs fail | |
| steps: | |
| - 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: ubuntu-latest | |
| needs: [validate-version, create-github-release] | |
| if: success() # Run even if other jobs fail | |
| steps: | |
| - 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)" |