diff --git a/.claude/skills/create-release.md b/.claude/skills/create-release.md new file mode 100644 index 0000000..c5dc2a2 --- /dev/null +++ b/.claude/skills/create-release.md @@ -0,0 +1,68 @@ +--- +description: Create a new draft release for Insomnia. Use when the user says "create a release", "new release", "cut a release", or "ship it". +--- + +# Create Release + +Create a new GitHub release for Insomnia with the correct versioning. + +## Steps + +1. Pull latest: `but pull` +2. Determine the next version by checking existing releases: + ```bash + gh release list --repo gordonbeeming/insomnia --limit 5 + ``` +3. Bump the minor version (e.g., v0.2 → v0.3). Never use a patch number in the tag. +4. Create the release: + ```bash + gh release create v{major}.{minor} \ + --repo gordonbeeming/insomnia \ + --target main \ + --title "v{major}.{minor} — {short description}" \ + --notes "$(cat <<'EOF' + # Insomnia v{major}.{minor} — {short description} + + ## What's New + + - {list changes since last release using git log} + + ## Install + + ```bash + brew upgrade --cask gordonbeeming/tap/insomnia + ``` + + Or download the DMG and CLI binary from the assets below. + EOF + )" + ``` +5. The release pipeline will automatically: + - Build + test + - Sign with Developer ID + - Notarize with Apple + - Create DMG + - Upload assets to the release + - Update the Homebrew tap cask +6. Report the release URL and pipeline run to the user + +## Version Format + +- Tags: `v{major}.{minor}` (e.g., `v0.3`) — NO patch number +- Bundle version: `{major}.{minor}.{runNumber}` — CI adds the run number as patch +- The tag `v0.3` with run number 45 produces bundle version `0.3.45` + +## Generating Release Notes + +Use git log to find changes since the last release tag: +```bash +LAST_TAG=$(gh release list --repo gordonbeeming/insomnia --limit 1 --json tagName --jq '.[0].tagName') +git log ${LAST_TAG}..HEAD --oneline +``` + +## Important + +- Never reuse or delete existing release tags +- Always bump the minor version for new releases +- Never use `.0` patch in tags (v0.3 not v0.3.0) +- The release triggers the full CI pipeline — wait for it to complete before telling the user to `brew upgrade` diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4615c40..4644146 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,10 +90,13 @@ jobs: # Runs the build script that produces the CLI binary and .app bundle - name: Build release artifacts run: | + # Extract version from the release tag (e.g., "v0.2" → "0.2") + VERSION="${GITHUB_REF_NAME#v}" # Make the script executable (git may not preserve the +x bit) chmod +x Scripts/build-release.sh - # Run the build — outputs to Distribution/ - ./Scripts/build-release.sh + # Run the build with version and a unique run+attempt number for the bundle + # run_attempt ensures reruns of the same workflow produce unique bundle versions + ./Scripts/build-release.sh "${VERSION}" "${{ github.run_number }}.${{ github.run_attempt }}" # --- Import signing certificate ---------------------------------------- # The Developer ID certificate is stored as a base64-encoded secret. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..120a7e7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# Insomnia + +A macOS caffeinate utility — menu bar app + CLI. Mascot: Dapple the Mushroom. + +## Tech Stack + +- Swift (SwiftUI + AppKit hybrid) +- macOS 14+, Apple Silicon (arm64) only +- IOKit power assertions (native, no shelling out to `caffeinate`) +- Swift ArgumentParser for CLI +- XCTest for unit + integration tests + +## Project Structure + +Three targets in `Package.swift`: +- `InsomniaCore` — shared library (power management, scheduling, IPC, models) +- `Insomnia` — macOS GUI app (MenuBarExtra + AppKit) +- `InsomniaCLI` — CLI tool + +## Build & Test + +```bash +swift build # debug build +swift test # 118+ tests +swift run Insomnia # run GUI locally +swift run InsomniaCLI status # run CLI +``` + +## Code Comments + +85%+ comment coverage required. Every file needs: +- File header explaining purpose +- `///` doc comments on public APIs +- Inline comments explaining intent on significant code blocks + +## Versioning + +- Tags: `v{major}.{minor}` (e.g., `v0.2`, `v0.3`) — no patch number +- Build number: GitHub Actions run number becomes the patch +- Bundle version: `{major}.{minor}.{runNumber}` (e.g., `0.2.42`) +- The `.0` patch in tags is meaningless — never use `v0.2.0` + +## Distribution + +- Homebrew: `brew install --cask gordonbeeming/tap/insomnia` + - Always use fully qualified name (upstream "Insomnia" API client conflicts) + - Cask auto-updated by CI on release via deploy key +- GitHub Releases: signed + notarized DMG + CLI binary +- No App Store (IOKit needs unsandboxed access) + +## CI/CD + +- `.github/workflows/build.yml` +- Push/PR: build + test only +- Release (published): build + test + sign + notarize + DMG + upload + update Homebrew tap +- Secrets: `DEVELOPER_ID_CERTIFICATE`, `DEVELOPER_ID_PASSWORD`, `APPLE_ID`, `APPLE_TEAM_ID`, `APPLE_APP_PASSWORD`, `HOMEBREW_TAP_DEPLOY_KEY` + +## Dev vs Prod + +Debug builds use separate identifiers (`BuildEnvironment.swift`): +- App name: "Insomnia Dev" +- IPC socket: `~/Library/Application Support/Insomnia Dev/insomnia.sock` +- UserDefaults prefix: `com.insomnia.dev.*` +- "(Dev)" suffix in status text + +This lets dev and prod run side-by-side. + +## Key Files + +- `Sources/InsomniaCore/Power/PowerAssertionManager.swift` — IOKit assertion lifecycle +- `Sources/InsomniaCore/IPC/IPCServer.swift` — Unix domain socket server +- `Sources/InsomniaCore/Models/BuildEnvironment.swift` — dev/prod separation +- `Sources/Insomnia/InsomniaApp.swift` — @main, MenuBarExtra scene +- `Sources/Insomnia/Views/MenuBarView.swift` — main dropdown menu +- `Sources/Insomnia/AppDelegate.swift` — lifecycle, IPC server, icon loading +- `Scripts/build-release.sh` — builds CLI + .app bundle (args: version, build number) +- `Resources/AppIcon.icns` — Dapple mushroom icon + +## Window Focus Pattern + +LSUIElement apps need special handling to show windows: +1. `NSApp.setActivationPolicy(.regular)` before opening +2. `openWindow(id:)` to open +3. Reapply app icon via `AppDelegate.reapplyAppIcon()` +4. `NSApp.activate(ignoringOtherApps: true)` after short delay +5. Return to `.accessory` in `onDisappear` (unless dock icon enabled) diff --git a/Scripts/build-release.sh b/Scripts/build-release.sh index a0b3560..ff244e5 100755 --- a/Scripts/build-release.sh +++ b/Scripts/build-release.sh @@ -7,7 +7,14 @@ # project root. # # Usage: -# ./Scripts/build-release.sh +# ./Scripts/build-release.sh +# +# Arguments: +# version — major.minor version string (e.g., "0.2"). Defaults to "1.0". +# build-number — numeric build number (e.g., GitHub Actions run number). Defaults to "1". +# +# Example: +# ./Scripts/build-release.sh 0.2 42 # → bundle version 0.2.42 # # Prerequisites: # - Xcode command-line tools (swift, xcodebuild) @@ -39,6 +46,22 @@ GUI_TARGET="Insomnia" ARCH="arm64" # Build configuration — Release enables optimizations and strips debug symbols BUILD_CONFIG="release" +# Version from the first argument (e.g., "0.2"), defaults to "1.0" +APP_VERSION="${1:-1.0}" +# Build number from the second argument (e.g., GitHub Actions run number), defaults to "1" +BUILD_NUMBER="${2:-1}" + +# --- Validate inputs ---------------------------------------------------------- +# Ensure version looks like digits.digits (e.g., "0.2", "1.0") +if [[ ! "${APP_VERSION}" =~ ^[0-9]+\.[0-9]+$ ]]; then + echo "❌ Invalid version '${APP_VERSION}' — expected format: major.minor (e.g., 0.2)" >&2 + exit 1 +fi +# Ensure build number is numeric (may contain dots for run_attempt, e.g., "42.1") +if [[ ! "${BUILD_NUMBER}" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + echo "❌ Invalid build number '${BUILD_NUMBER}' — expected numeric (e.g., 42 or 42.1)" >&2 + exit 1 +fi # --- Clean previous artifacts ------------------------------------------------ echo "🧹 Cleaning previous Distribution/ contents..." @@ -145,9 +168,9 @@ cat > "${CONTENTS_DIR}/Info.plist" <CFBundleExecutable ${GUI_TARGET} CFBundleVersion - 1.0.0 + ${APP_VERSION}.${BUILD_NUMBER} CFBundleShortVersionString - 1.0.0 + ${APP_VERSION}.${BUILD_NUMBER} CFBundlePackageType APPL CFBundleIconFile