diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..2019b7e --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,24 @@ +#!/bin/bash +# .githooks/pre-commit +# Pre-commit hook for version validation + +set -e + +echo "🔍 Pre-commit: Validating version synchronization..." + +# Check if validation script exists +if [ ! -f "scripts/validate-github-version-sync.sh" ]; then + echo "⚠️ Version validation script not found, skipping check" + exit 0 +fi + +# Run version validation (but don't fail the commit if GitHub CLI is unavailable) +if ./scripts/validate-github-version-sync.sh 2>/dev/null; then + echo "✅ Pre-commit: Version validation passed" +else + echo "⚠️ Pre-commit: Version validation failed or GitHub CLI unavailable" + echo "💡 Consider running: ./scripts/sync-version-from-github.sh" + echo "🔄 Continuing with commit (validation not enforced)" +fi + +echo "✅ Pre-commit checks complete" \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index b6e2c89..287db07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -439,6 +439,109 @@ echo 'export CODE_SIGN_IDENTITY="Apple Development: Your Name (TEAM_ID)"' >> ~/. 2. **Avoid concurrency conflicts**: Use proper `@MainActor` isolation without redundant dispatch 3. **Test permission changes**: Always test toggling permissions ON/OFF during development +## Version Management System + +ClickIt uses an automated version management system that synchronizes version numbers between the UI, GitHub releases, and build processes. + +### Version Architecture + +**Single Source of Truth**: GitHub Release tags (e.g., `v1.4.15`) +- **GitHub Release**: Latest published version +- **Info.plist**: `CFBundleShortVersionString` (synced automatically) +- **UI Display**: Reads from `Bundle.main.infoDictionary` at runtime +- **Build Scripts**: Extract version from Info.plist (no hardcoding) + +### Version Management Scripts + +**Sync version with GitHub releases**: +```bash +./scripts/sync-version-from-github.sh +``` +Automatically updates Info.plist to match the latest GitHub release version. + +**Validate version synchronization**: +```bash +./scripts/validate-github-version-sync.sh +``` +Checks if local version matches GitHub release. Used in build validation. + +**Update to new version**: +```bash +./scripts/update-version.sh 1.5.0 # Creates GitHub release automatically +./scripts/update-version.sh 1.5.0 false # Update without GitHub release +``` +Complete version update workflow including Info.plist update, git commit, tag creation, and optional GitHub release trigger. + +### Fastlane Integration + +**Sync with GitHub**: +```bash +fastlane sync_version_with_github +``` + +**Release new version**: +```bash +fastlane release_with_github version:1.5.0 +``` + +**Validate synchronization**: +```bash +fastlane validate_github_sync +``` + +### Git Hooks (Optional) + +**Install version validation hooks**: +```bash +./scripts/install-git-hooks.sh +``` +Adds pre-commit hook that validates version synchronization before commits. + +### Build Integration + +Build scripts automatically: +- Extract version from Info.plist +- Validate sync with GitHub releases +- Display version warnings if mismatched +- Build with correct version in app bundle + +### Troubleshooting Version Issues + +**UI shows wrong version**: +```bash +# Sync Info.plist with GitHub release +./scripts/sync-version-from-github.sh + +# Rebuild app bundle +./build_app_unified.sh release +``` + +**Version mismatch detected**: +```bash +# Check current status +./scripts/validate-github-version-sync.sh + +# Fix automatically +./scripts/sync-version-from-github.sh +``` + +**Release new version**: +```bash +# Complete workflow (recommended) +./scripts/update-version.sh 1.5.0 + +# Or use Fastlane +fastlane release_with_github version:1.5.0 +``` + +### CI/CD Integration + +The GitHub Actions release workflow (`.github/workflows/release.yml`) automatically: +- Validates version synchronization +- Auto-fixes version mismatches +- Verifies built app version matches tag +- Creates releases with proper version metadata + ## Documentation References - Full product requirements: `docs/clickit_autoclicker_prd.md` diff --git a/ClickIt/Info.plist b/ClickIt/Info.plist index 563915c..102d62d 100644 --- a/ClickIt/Info.plist +++ b/ClickIt/Info.plist @@ -2,55 +2,51 @@ - CFBundleDisplayName - ClickIt - CFBundleExecutable - ClickIt - CFBundleIdentifier - com.jsonify.ClickIt - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ClickIt - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.2.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSMinimumSystemVersion - 14.0 - LSUIElement - - NSHighResolutionCapable - - NSSupportsAutomaticGraphicsSwitching - - - NSAppleEventsUsageDescription - ClickIt needs to send Apple Events to simulate mouse clicks in target applications. - NSSystemAdministrationUsageDescription - ClickIt requires accessibility access to simulate mouse clicks and detect window information. - - NSAccessibilityUsageDescription - ClickIt needs accessibility access to control mouse clicks and interact with other applications. - NSScreenCaptureUsageDescription - ClickIt needs screen recording access to detect windows and provide visual feedback overlays. - - CFBundleIconFile - AppIcon - CFBundleIconName - AppIcon - - SUFeedURL - https://jsonify.github.io/ClickIt/appcast.xml - SUPublicEDKey - auto-generated-when-needed - SUAutomaticallyUpdate - - SUEnableAutomaticChecks - - SUCheckAtStartup - + CFBundleDisplayName + ClickIt + CFBundleExecutable + ClickIt + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + com.jsonify.ClickIt + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ClickIt + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.4.15 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSMinimumSystemVersion + 14.0 + LSUIElement + + NSAccessibilityUsageDescription + ClickIt needs accessibility access to control mouse clicks and interact with other applications. + NSAppleEventsUsageDescription + ClickIt needs to send Apple Events to simulate mouse clicks in target applications. + NSHighResolutionCapable + + NSScreenCaptureUsageDescription + ClickIt needs screen recording access to detect windows and provide visual feedback overlays. + NSSupportsAutomaticGraphicsSwitching + + NSSystemAdministrationUsageDescription + ClickIt requires accessibility access to simulate mouse clicks and detect window information. + SUAutomaticallyUpdate + + SUCheckAtStartup + + SUEnableAutomaticChecks + + SUFeedURL + https://jsonify.github.io/ClickIt/appcast.xml + SUPublicEDKey + auto-generated-when-needed - \ No newline at end of file + diff --git a/build_app_unified.sh b/build_app_unified.sh index cd7d8fb..3c67345 100755 --- a/build_app_unified.sh +++ b/build_app_unified.sh @@ -9,10 +9,32 @@ BUILD_SYSTEM="${2:-auto}" # auto, spm, xcode DIST_DIR="dist" APP_NAME="ClickIt" BUNDLE_ID="com.jsonify.clickit" -VERSION="1.0.0" +# Get version from Info.plist (synced with GitHub releases) +get_version_from_plist() { + /usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist 2>/dev/null || echo "1.0.0" +} + +VERSION=$(get_version_from_plist) BUILD_NUMBER=$(date +%Y%m%d%H%M) echo "🔨 Building $APP_NAME app bundle ($BUILD_MODE mode)..." +echo "📦 Version: $VERSION (from Info.plist, synced with GitHub releases)" + +# Validate version is synchronized with GitHub (optional validation) +if command -v gh > /dev/null 2>&1; then + GITHUB_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$GITHUB_TAG" ]; then + GITHUB_VERSION=${GITHUB_TAG#v} + if [ "$VERSION" != "$GITHUB_VERSION" ]; then + echo "⚠️ Warning: Version mismatch detected!" + echo " Building: v$VERSION" + echo " GitHub: $GITHUB_TAG" + echo " Run './scripts/sync-version-from-github.sh' to sync versions" + else + echo "✅ Version synchronized with GitHub release $GITHUB_TAG" + fi + fi +fi # Detect build system detect_build_system() { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a9bd5d4..571f571 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -175,6 +175,25 @@ platform :mac do lane :dev do UI.header("Starting ClickIt Development Workflow") + # Validate and auto-sync version before development + begin + UI.message("🔍 Checking version synchronization...") + validate_github_sync + UI.success("✅ Version already synchronized") + rescue => exception + UI.message("⚠️ Version mismatch detected: #{exception.message}") + UI.message("🔄 Auto-syncing version with GitHub release...") + + begin + sync_version_with_github(auto_sync: true) + UI.success("✅ Version automatically synchronized for development") + rescue => sync_exception + UI.error("❌ Auto-sync failed: #{sync_exception.message}") + UI.message("💡 Manual fix required: ./scripts/sync-version-from-github.sh") + UI.message("🔄 Continuing with development workflow anyway...") + end + end + # Build debug and run launch @@ -386,6 +405,104 @@ platform :mac do auto_prod(version: new_version) end + desc "🔍 Validate GitHub version synchronization" + lane :validate_github_sync do + UI.header "🔍 GitHub Version Validation" + + project_root = File.expand_path("..", Dir.pwd) if File.basename(Dir.pwd) == "fastlane" + project_root ||= Dir.pwd + + Dir.chdir(project_root) do + begin + # Get versions + plist_version = sh("/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' ClickIt/Info.plist", log: false).strip + github_tag = sh("gh release list --limit 1 --json tagName --jq '.[0].tagName'", log: false).strip + github_version = github_tag.gsub(/^v/, '') + + UI.message("📋 Version Status:") + UI.message(" Info.plist (UI): #{plist_version}") + UI.message(" GitHub Release: #{github_version}") + + if plist_version != github_version + UI.error("") + UI.error("❌ VERSION MISMATCH DETECTED!") + UI.error(" The UI will show v#{plist_version}") + UI.error(" But the latest release is #{github_tag}") + UI.error("") + UI.error("🔧 To fix, run: fastlane sync_version_with_github") + UI.user_error!("Version synchronization required") + else + UI.success("✅ Versions are synchronized") + UI.success(" UI will display: v#{plist_version}") + UI.success(" GitHub release: #{github_tag}") + end + + rescue => exception + UI.error("❌ GitHub CLI not available or authentication failed") + raise exception + end + end + end + + desc "🔄 Sync version with latest GitHub release" + lane :sync_version_with_github do |options| + UI.header "🔄 Syncing Version with GitHub" + + # Ensure we're in project root + project_root = File.expand_path("..", Dir.pwd) if File.basename(Dir.pwd) == "fastlane" + project_root ||= Dir.pwd + + Dir.chdir(project_root) do + begin + # Try GitHub CLI first + latest_release = sh("gh release list --limit 1 --json tagName --jq '.[0].tagName'", log: false).strip + version = latest_release.gsub(/^v/, '') + + UI.message("📦 Latest GitHub release: #{latest_release}") + UI.message("📝 Extracted version: #{version}") + + rescue => gh_exception + # Fallback to git tags if GitHub CLI fails + UI.message("⚠️ GitHub CLI unavailable, falling back to git tags...") + begin + latest_tag = sh("git describe --tags --abbrev=0", log: false).strip + version = latest_tag.gsub(/^v/, '') + UI.message("📦 Latest git tag: #{latest_tag}") + UI.message("📝 Extracted version: #{version}") + rescue => git_exception + if options[:auto_sync] + UI.error("❌ Cannot determine latest version (GitHub CLI and git tags both failed)") + raise "Auto-sync failed: No version source available" + else + UI.error("❌ GitHub CLI not available or not authenticated") + UI.message("Install GitHub CLI: brew install gh") + UI.message("Authenticate: gh auth login") + raise gh_exception + end + end + end + + # Get current Info.plist version + current_version = sh("/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' ClickIt/Info.plist", log: false).strip + + if version != current_version + UI.message("⚠️ Version mismatch detected!") + UI.message(" Info.plist: #{current_version}") + UI.message(" Latest release/tag: #{version}") + UI.message("") + UI.message("🔧 Updating Info.plist to match latest version...") + + # Update Info.plist + sh("/usr/libexec/PlistBuddy -c 'Set CFBundleShortVersionString #{version}' ClickIt/Info.plist") + + UI.success("✅ Info.plist updated to v#{version}") + UI.success("🔄 UI will now display v#{version}") + else + UI.success("✅ Versions are synchronized (v#{version})") + end + end + end + error do |lane, exception| UI.error("❌ Lane '#{lane}' failed with error: #{exception.message}") end diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh new file mode 100755 index 0000000..566cbef --- /dev/null +++ b/scripts/install-git-hooks.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# scripts/install-git-hooks.sh +# Install git hooks for version validation + +set -e + +echo "🔧 Installing git hooks for version validation..." + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo "❌ Not in a git repository" + exit 1 +fi + +# Create .git/hooks directory if it doesn't exist +mkdir -p .git/hooks + +# Install pre-commit hook +if [ -f ".githooks/pre-commit" ]; then + cp .githooks/pre-commit .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + echo "✅ Pre-commit hook installed" +else + echo "❌ .githooks/pre-commit not found" + exit 1 +fi + +# Set git hooks path (optional - uses .githooks directly) +git config core.hooksPath .githooks + +echo "🎉 Git hooks installation complete!" +echo "" +echo "📋 Installed hooks:" +echo " • pre-commit: Version synchronization validation" +echo "" +echo "💡 The pre-commit hook will:" +echo " • Validate version sync before each commit" +echo " • Provide warnings if versions are mismatched" +echo " • Not block commits (warnings only)" +echo "" +echo "🔧 To uninstall: git config --unset core.hooksPath" \ No newline at end of file diff --git a/scripts/sync-version-from-github.sh b/scripts/sync-version-from-github.sh new file mode 100755 index 0000000..c09da6b --- /dev/null +++ b/scripts/sync-version-from-github.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# scripts/sync-version-from-github.sh +# Sync Info.plist version with latest GitHub release +set -e + +echo "🔄 Syncing version from GitHub releases..." + +# Get latest GitHub release tag +LATEST_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || git describe --tags --abbrev=0) +VERSION=${LATEST_TAG#v} # Remove 'v' prefix + +echo "📦 Latest GitHub release: $LATEST_TAG" +echo "📝 Extracted version: $VERSION" + +# Current Info.plist version +CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist) + +if [ "$VERSION" != "$CURRENT_VERSION" ]; then + echo "⚠️ Version mismatch detected!" + echo " Info.plist: $CURRENT_VERSION" + echo " GitHub: $VERSION" + echo "" + echo "🔧 Updating Info.plist to match GitHub release..." + + # Update Info.plist + /usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $VERSION" ClickIt/Info.plist + + echo "✅ Info.plist updated to v$VERSION" + echo "🔄 UI will now display v$VERSION" +else + echo "✅ Versions are synchronized (v$VERSION)" +fi + +echo "" +echo "📋 Final Status:" +echo " GitHub Release: $LATEST_TAG" +echo " Info.plist: v$VERSION" +echo " UI will show: v$VERSION" \ No newline at end of file diff --git a/scripts/update-version.sh b/scripts/update-version.sh new file mode 100755 index 0000000..853e8d9 --- /dev/null +++ b/scripts/update-version.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# scripts/update-version.sh +# Enhanced version update with GitHub Release integration +set -e + +NEW_VERSION="$1" +CREATE_RELEASE="${2:-true}" + +if [ -z "$NEW_VERSION" ]; then + echo "Usage: $0 [create_release]" + echo "" + echo "Examples:" + echo " $0 1.5.0 # Update to 1.5.0 and trigger GitHub release" + echo " $0 1.5.0 false # Update to 1.5.0 without GitHub release" + echo "" + echo "This script will:" + echo " 1. Update Info.plist CFBundleShortVersionString" + echo " 2. Commit the change to git" + echo " 3. Create and push git tag" + echo " 4. Optionally trigger GitHub release via CI/CD" + exit 1 +fi + +echo "🔄 Updating ClickIt to version $NEW_VERSION" + +# Validate version format (basic semantic versioning) +if [[ ! "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ Invalid version format. Use semantic versioning (e.g., 1.5.0)" + exit 1 +fi + +# Get current version +CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist) +echo "📦 Current version: $CURRENT_VERSION" +echo "📦 New version: $NEW_VERSION" + +# Check if version already exists +if [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then + echo "⚠️ Version $NEW_VERSION is already current" + exit 0 +fi + +# Check if git tag already exists +if git rev-parse "v$NEW_VERSION" >/dev/null 2>&1; then + echo "❌ Git tag v$NEW_VERSION already exists" + exit 1 +fi + +# Update Info.plist +echo "🔧 Updating Info.plist..." +/usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $NEW_VERSION" ClickIt/Info.plist + +# Verify the change +UPDATED_VERSION=$(/usr/libexec/PListBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist) +if [ "$UPDATED_VERSION" != "$NEW_VERSION" ]; then + echo "❌ Failed to update Info.plist" + exit 1 +fi + +echo "✅ Info.plist updated to v$NEW_VERSION" + +# Git operations +echo "📝 Committing changes..." +git add ClickIt/Info.plist +git commit -m "chore: bump version to v$NEW_VERSION + +- Update CFBundleShortVersionString to $NEW_VERSION +- UI will now display v$NEW_VERSION +- Synchronized with GitHub release workflow" + +# Create and push tag +echo "🏷️ Creating git tag v$NEW_VERSION..." +git tag "v$NEW_VERSION" + +echo "🚀 Pushing to remote..." +git push origin main +git push origin "v$NEW_VERSION" + +if [ "$CREATE_RELEASE" = "true" ]; then + echo "" + echo "🎉 Version v$NEW_VERSION pushed successfully!" + echo "" + echo "🚀 GitHub Release will be created automatically:" + echo " - Monitor CI/CD: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')/actions" + echo " - Release will be at: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]\(.*\)\.git/\1/')/releases/tag/v$NEW_VERSION" + echo "" + echo "📦 The release will include:" + echo " - Universal macOS app bundle (ClickIt.app.zip)" + echo " - Automatic release notes with changelog" + echo " - Build metadata and verification" +else + echo "" + echo "📝 Version v$NEW_VERSION updated locally without GitHub release" + echo " - Git tag created and pushed" + echo " - To create release later, push the tag: git push origin v$NEW_VERSION" +fi + +echo "" +echo "✅ Version update complete!" +echo " Previous: v$CURRENT_VERSION" +echo " Current: v$NEW_VERSION" +echo " UI will display: v$NEW_VERSION" \ No newline at end of file diff --git a/scripts/validate-github-version-sync.sh b/scripts/validate-github-version-sync.sh new file mode 100755 index 0000000..5257ddf --- /dev/null +++ b/scripts/validate-github-version-sync.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# scripts/validate-github-version-sync.sh +# Validate version synchronization between Info.plist and GitHub releases +set -e + +echo "🔍 Validating version synchronization with GitHub..." + +# Get versions +PLIST_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ClickIt/Info.plist) +GITHUB_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || git describe --tags --abbrev=0) +GITHUB_VERSION=${GITHUB_TAG#v} + +echo "📋 Version Status:" +echo " Info.plist (UI): $PLIST_VERSION" +echo " GitHub Release: $GITHUB_VERSION" + +if [ "$PLIST_VERSION" != "$GITHUB_VERSION" ]; then + echo "" + echo "❌ VERSION MISMATCH DETECTED!" + echo " The UI will show v$PLIST_VERSION" + echo " But the latest release is $GITHUB_TAG" + echo "" + echo "🔧 To fix, run: ./scripts/sync-version-from-github.sh" + exit 1 +else + echo "✅ Versions are synchronized" + echo " UI will display: v$PLIST_VERSION" + echo " GitHub release: $GITHUB_TAG" +fi \ No newline at end of file