diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index d9f85ff..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(xcodebuild:*)", - "Bash(git -C /Users/piotrek/Work/menuapp status)", - "Bash(git -C /Users/piotrek/Work/menuapp diff)", - "Bash(git -C /Users/piotrek/Work/menuapp log --oneline -5)" - ] - } -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5cbafbe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: macos-26 + steps: + - uses: actions/checkout@v5 + + - name: Build + run: | + xcodebuild -project ActionBar.xcodeproj -scheme ActionBar -configuration Release \ + -destination 'generic/platform=macOS' \ + CODE_SIGNING_ALLOWED=NO \ + build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bfa14ff --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,163 @@ +name: Release + +# Push a tag like `v2.0` to build, sign, notarize and publish a release. +# CHANGELOG.md must have a `## ` section; it becomes the release notes. +# Tags with a suffix (`v2.0-pre1`) are published as GitHub pre-releases and use +# the notes of the base version (`## 2.0`). Pre-releases never become "latest", +# so the public download link and update feed keep pointing at the last stable release. +# +# Required repository secrets: +# DEVELOPER_ID_CERT_P12 Base64-encoded "Developer ID Application" certificate (.p12) +# DEVELOPER_ID_CERT_PASSWORD Password for the .p12 file +# APPLE_ID Apple ID email used for notarization +# APPLE_APP_PASSWORD App-specific password for that Apple ID +# APPLE_TEAM_ID Apple Developer Team ID +# SPARKLE_PRIVATE_KEY Sparkle EdDSA private key (generate_keys -x) + +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + runs-on: macos-26 + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set paths and version + run: | + { + echo "ARCHIVE_PATH=$RUNNER_TEMP/ActionBar.xcarchive" + echo "EXPORT_PATH=$RUNNER_TEMP/export" + echo "UPDATES_PATH=$RUNNER_TEMP/updates" + echo "SPM_PATH=$RUNNER_TEMP/spm" + echo "KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db" + } >> "$GITHUB_ENV" + + VERSION="${GITHUB_REF_NAME#v}" + if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+)*(-[0-9A-Za-z.]+)?$ ]]; then + echo "Tag must look like v1.2, v1.2.3 or v1.2-pre1, got $GITHUB_REF_NAME" >&2 + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + echo "NOTES_VERSION=${VERSION%%-*}" >> "$GITHUB_ENV" + if [[ "$VERSION" == *-* ]]; then + echo "PRERELEASE_FLAG=--prerelease" >> "$GITHUB_ENV" + fi + # Sparkle compares build numbers, so this must always increase. + echo "BUILD_NUMBER=$(git rev-list --count HEAD)" >> "$GITHUB_ENV" + + - name: Extract release notes + run: | + mkdir -p "$UPDATES_PATH" + # Sparkle picks up notes named like the archive (ActionBar.md for ActionBar.dmg). + awk -v v="$NOTES_VERSION" '/^## / { if (found) exit; found = ($2 == v); next } found' CHANGELOG.md \ + | sed -e '/./,$!d' > "$UPDATES_PATH/ActionBar.md" + if ! grep -q '[^[:space:]]' "$UPDATES_PATH/ActionBar.md"; then + echo "CHANGELOG.md has no notes for $NOTES_VERSION. Add a '## $NOTES_VERSION' section." >&2 + exit 1 + fi + cat "$UPDATES_PATH/ActionBar.md" + + - name: Import signing certificate + env: + CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }} + CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }} + run: | + KEYCHAIN_PASSWORD="$(uuidgen)" + echo "$CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import "$RUNNER_TEMP/cert.p12" -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychains -d user -s "$KEYCHAIN_PATH" "$HOME/Library/Keychains/login.keychain-db" + rm "$RUNNER_TEMP/cert.p12" + + - name: Archive + run: | + xcodebuild -project ActionBar.xcodeproj -scheme ActionBar -configuration Release \ + -destination 'generic/platform=macOS' \ + -archivePath "$ARCHIVE_PATH" \ + -clonedSourcePackagesDirPath "$SPM_PATH" \ + DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" \ + CODE_SIGN_STYLE=Manual \ + CODE_SIGN_IDENTITY="Developer ID Application" \ + MARKETING_VERSION="$VERSION" \ + CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \ + archive + + - name: Export app + run: | + xcodebuild -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportPath "$EXPORT_PATH" \ + -exportOptionsPlist Config/ExportOptions.plist + + - name: Notarize app + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + ditto -c -k --keepParent "$EXPORT_PATH/ActionBar.app" "$RUNNER_TEMP/ActionBar.zip" + xcrun notarytool submit "$RUNNER_TEMP/ActionBar.zip" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + xcrun stapler staple "$EXPORT_PATH/ActionBar.app" + + - name: Create DMG + run: | + mkdir -p "$RUNNER_TEMP/dmg" + cp -R "$EXPORT_PATH/ActionBar.app" "$RUNNER_TEMP/dmg/" + ln -s /Applications "$RUNNER_TEMP/dmg/Applications" + hdiutil create -volname ActionBar -srcfolder "$RUNNER_TEMP/dmg" -ov -format UDZO "$UPDATES_PATH/ActionBar.dmg" + codesign --sign "Developer ID Application" --timestamp "$UPDATES_PATH/ActionBar.dmg" + + - name: Notarize DMG + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + xcrun notarytool submit "$UPDATES_PATH/ActionBar.dmg" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + xcrun stapler staple "$UPDATES_PATH/ActionBar.dmg" + + - name: Generate appcast + env: + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: | + echo "$SPARKLE_PRIVATE_KEY" | "$SPM_PATH/artifacts/sparkle/Sparkle/bin/generate_appcast" \ + --ed-key-file - \ + --embed-release-notes \ + --download-url-prefix "https://github.com/$GITHUB_REPOSITORY/releases/download/$GITHUB_REF_NAME/" \ + --full-release-notes-url "https://github.com/$GITHUB_REPOSITORY/releases/tag/$GITHUB_REF_NAME" \ + "$UPDATES_PATH" + cat "$UPDATES_PATH/appcast.xml" + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + "$UPDATES_PATH/ActionBar.dmg" \ + "$UPDATES_PATH/appcast.xml" \ + --title "ActionBar $VERSION" \ + --notes-file "$UPDATES_PATH/ActionBar.md" \ + ${PRERELEASE_FLAG:+"$PRERELEASE_FLAG"} + + - name: Clean up + if: always() + run: | + security delete-keychain "$KEYCHAIN_PATH" || true diff --git a/.gitignore b/.gitignore index 0620f39..616b2ea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,14 @@ .swiftpm/ *.app xcuserdata/ -*.xcodeproj/project.xcworkspace/ +*.xcodeproj/project.xcworkspace/xcuserdata/ *.xcodeproj/xcuserdata/ DerivedData/ .DS_Store + +# Local agent config +.claude/settings.local.json +CLAUDE.md + +# Local signing config +Config/Local.xcconfig diff --git a/ActionBar.xcodeproj/project.pbxproj b/ActionBar.xcodeproj/project.pbxproj index 9dba639..2020705 100644 --- a/ActionBar.xcodeproj/project.pbxproj +++ b/ActionBar.xcodeproj/project.pbxproj @@ -25,11 +25,12 @@ A10001000000000000000015 /* MenuBarIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000015 /* MenuBarIcon.swift */; }; A10001000000000000000016 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000016 /* Assets.xcassets */; }; A10001000000000000000017 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000017 /* SettingsView.swift */; }; + A10001000000000000000018 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A1000C000000000000000001 /* Sparkle */; }; + A10001000000000000000019 /* UpdaterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10002000000000000000019 /* UpdaterService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 24E6B8522F2D684B00884177 /* MyIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = MyIcon.icon; sourceTree = ""; }; - 24F874A92F2DF07C00553174 /* ActionBarRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ActionBarRelease.entitlements; sourceTree = ""; }; A10002000000000000000001 /* ActionBarApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarApp.swift; sourceTree = ""; }; A10002000000000000000002 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A10002000000000000000003 /* WorkflowRun.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowRun.swift; sourceTree = ""; }; @@ -48,6 +49,9 @@ A10002000000000000000016 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A10002000000000000000017 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A10003000000000000000001 /* ActionBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ActionBar.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A10002000000000000000018 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Base.xcconfig; path = Config/Base.xcconfig; sourceTree = ""; }; + A10002000000000000000019 /* UpdaterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterService.swift; sourceTree = ""; }; + A10002000000000000000020 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -55,6 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A10001000000000000000018 /* Sparkle in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -64,7 +69,7 @@ A10005000000000000000001 = { isa = PBXGroup; children = ( - 24F874A92F2DF07C00553174 /* ActionBarRelease.entitlements */, + A10002000000000000000018 /* Base.xcconfig */, A10005000000000000000003 /* Sources */, A10005000000000000000008 /* Resources */, A10005000000000000000002 /* Products */, @@ -117,6 +122,7 @@ A10002000000000000000006 /* KeychainService.swift */, A10002000000000000000007 /* GitHubAPIClient.swift */, A10002000000000000000008 /* OAuthService.swift */, + A10002000000000000000019 /* UpdaterService.swift */, ); path = Services; sourceTree = ""; @@ -139,6 +145,7 @@ children = ( A10002000000000000000016 /* Assets.xcassets */, 24E6B8522F2D684B00884177 /* MyIcon.icon */, + A10002000000000000000020 /* Info.plist */, ); path = Resources; sourceTree = ""; @@ -168,6 +175,9 @@ dependencies = ( ); name = ActionBar; + packageProductDependencies = ( + A1000C000000000000000001 /* Sparkle */, + ); productName = GitHubActionsMenu; productReference = A10003000000000000000001 /* ActionBar.app */; productType = "com.apple.product-type.application"; @@ -191,6 +201,9 @@ Base, ); mainGroup = A10005000000000000000001; + packageReferences = ( + A1000B000000000000000001 /* XCRemoteSwiftPackageReference "Sparkle" */, + ); productRefGroup = A10005000000000000000002 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -219,6 +232,7 @@ files = ( A10001000000000000000001 /* ActionBarApp.swift in Sources */, A10001000000000000000002 /* AppDelegate.swift in Sources */, + A10001000000000000000019 /* UpdaterService.swift in Sources */, A10001000000000000000003 /* WorkflowRun.swift in Sources */, A10001000000000000000004 /* AppState.swift in Sources */, A10001000000000000000005 /* NotificationService.swift in Sources */, @@ -269,7 +283,6 @@ COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = ZX8SV23YKJ; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -323,7 +336,6 @@ COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = ZX8SV23YKJ; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -344,6 +356,7 @@ }; A1000A000000000000000003 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A10002000000000000000018 /* Base.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = MyIcon; @@ -355,12 +368,11 @@ DEAD_CODE_STRIPPING = YES; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ActionBar; - INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSUIElement = YES; - MARKETING_VERSION = 1.7; - PRODUCT_BUNDLE_IDENTIFIER = com.ptrchm.actionbar; + MARKETING_VERSION = 2.0; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; @@ -370,35 +382,23 @@ }; A1000A000000000000000004 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A10002000000000000000018 /* Base.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = MyIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; - CODE_SIGN_ENTITLEMENTS = ActionBarRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 9; DEAD_CODE_STRIPPING = YES; - ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ActionBar; - INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSUIElement = YES; - MARKETING_VERSION = 1.7; - PRODUCT_BUNDLE_IDENTIFIER = com.ptrchm.actionbar; + MARKETING_VERSION = 2.0; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; @@ -428,6 +428,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + A1000B000000000000000001 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.7.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A1000C000000000000000001 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = A1000B000000000000000001 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = A10009000000000000000001 /* Project object */; } diff --git a/ActionBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ActionBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..542ae1a --- /dev/null +++ b/ActionBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "e721da7f9826abdffcb6185e886155efa2514bd6234475f1afa893e29eb258d6", + "pins" : [ + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "eef1a539a373c1f1a320624b1130fc5de7b2e100", + "version" : "2.10.0" + } + } + ], + "version" : 3 +} diff --git a/ActionBarRelease.entitlements b/ActionBarRelease.entitlements deleted file mode 100644 index 0c67376..0000000 --- a/ActionBarRelease.entitlements +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ef4f88d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 2.0 + +ActionBar is now open source and distributed directly instead of through the Mac App Store. + +- Automatic updates via Sparkle. Use "Check for Updates…" in About to check manually. +- New option to include workflow runs triggered by other users. +- The menu bar icon now shows the status of running and queued workflows first. +- Fewer API requests: unchanged workflow runs are served from cache using conditional requests. +- Fixed the repository list layout in Settings. + +If you're coming from the App Store version, you'll need to sign in again. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 8025697..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,16 +0,0 @@ -# CLAUDE.md - -This is the source code for a macOS menu bar app that displays GitHub Actions workflow run statuses for selected repositories. - -The app is: - -- Built using Swift and SwiftUI -- Uses GitHub's REST API to fetch workflow run data -- Displays workflow run statuses in the macOS menu bar -- Is built with Xcode -- Is distributed through the Mac App Store - -## Rules - -- Ask questions before implementing to ensure you understand the request. -- Always use Context7 MCP when I need library/API documentation, code generation, setup or configuration steps without me having to explicitly ask. diff --git a/Config/Base.xcconfig b/Config/Base.xcconfig new file mode 100644 index 0000000..71e0cc6 --- /dev/null +++ b/Config/Base.xcconfig @@ -0,0 +1,10 @@ +// Shared build settings for the ActionBar target. +// +// To build and sign with your own Apple Developer account, copy +// Config/Local.xcconfig.example to Config/Local.xcconfig and fill it in. +// Local.xcconfig is git-ignored. + +DEVELOPMENT_TEAM = +PRODUCT_BUNDLE_IDENTIFIER = com.ptrchm.actionbar + +#include? "Local.xcconfig" diff --git a/Config/ExportOptions.plist b/Config/ExportOptions.plist new file mode 100644 index 0000000..8e4670c --- /dev/null +++ b/Config/ExportOptions.plist @@ -0,0 +1,12 @@ + + + + + method + developer-id + signingStyle + manual + signingCertificate + Developer ID Application + + diff --git a/Config/Local.xcconfig.example b/Config/Local.xcconfig.example new file mode 100644 index 0000000..c5d0697 --- /dev/null +++ b/Config/Local.xcconfig.example @@ -0,0 +1,7 @@ +// Copy to Config/Local.xcconfig (git-ignored) and set your own values. + +// Your Apple Developer Team ID (Xcode → Settings → Accounts, or developer.apple.com/account). +DEVELOPMENT_TEAM = XXXXXXXXXX + +// A bundle identifier registered to your team. +PRODUCT_BUNDLE_IDENTIFIER = com.example.actionbar diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11eeebe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Piotr Chmolowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 04c5986..725b267 100644 --- a/Makefile +++ b/Makefile @@ -4,20 +4,11 @@ SCHEME = $(APP_NAME) BUILD_DIR = $(shell xcodebuild -project $(PROJECT) -scheme $(SCHEME) -configuration Release -showBuildSettings 2>/dev/null | grep -m1 '^\s*BUILT_PRODUCTS_DIR' | sed 's/.*= //') APP_BUNDLE = $(BUILD_DIR)/$(APP_NAME).app -TEAM_ID = ZX8SV23YKJ -KEYCHAIN_PROFILE = notary - -.PHONY: build notarize run clean +.PHONY: build run clean build: xcodebuild -project $(PROJECT) -scheme $(SCHEME) -configuration Release build -notarize: build - ditto -c -k --keepParent "$(APP_BUNDLE)" $(APP_NAME).zip - xcrun notarytool submit $(APP_NAME).zip --keychain-profile $(KEYCHAIN_PROFILE) --wait - xcrun stapler staple "$(APP_BUNDLE)" - rm $(APP_NAME).zip - run: build -pkill -x $(APP_NAME) sleep 0.5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..de85746 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# ActionBar + +Monitor your GitHub Actions workflow runs directly from the macOS menu bar. + +

+ ActionBar showing GitHub Actions workflow runs in the macOS menu bar +

+ +

+ Download · + Website +

+ +## Features + +- See workflow run statuses at a glance +- Get notifications when runs complete +- Select the repositories you want to monitor +- Minimal permissions — no access to your code +- Native macOS app — lightweight and fast +- Automatic updates via [Sparkle](https://sparkle-project.org) + +## How it works + +ActionBar signs in through a [GitHub App](https://github.com/apps/actionbar-macos) using the OAuth device flow. The app only has **Actions (read)** and **Metadata (read)** permissions, so it never has access to your code. It only calls the [workflow runs API](https://docs.github.com/en/rest/actions/workflow-runs) (`/repos/{owner}/{repo}/actions/runs`) for the repositories you select. + +Your access token is stored in the macOS Keychain. + +By default, ActionBar polls the API every 30 seconds; the interval is configurable, and you can refresh manually at any time. Each monitored repository requires a separate API call on every refresh, so monitoring a large number of repositories may hit GitHub's rate limits. + +## Privacy + +ActionBar does not collect, transmit, or share any personal data. The app only talks to GitHub: the official API (`api.github.com`) and, for update checks, this repository's releases. No analytics, telemetry, or third-party services are used. All stored data is deleted when you sign out. + +## Building from source + +Requirements: macOS 14+ and a recent version of Xcode. + +1. Clone the repository. +2. Copy `Config/Local.xcconfig.example` to `Config/Local.xcconfig` and set your Apple Developer Team ID and a bundle identifier registered to your team. +3. Open `ActionBar.xcodeproj` and run the `ActionBar` scheme, or use `make run`. + +The app authenticates through the official ActionBar GitHub App. If you're shipping your own fork, register your own [GitHub App](https://docs.github.com/en/apps/creating-github-apps) with device flow enabled and **Actions (read)** permission, then update `clientID` in `Sources/ActionBar/Services/OAuthService.swift` and the app's installation URL. You'll also need your own Sparkle key pair and feed URL in `Resources/Info.plist` (see below). + +## Releasing + +Releases are built, signed, notarized and published by the [Release workflow](.github/workflows/release.yml) when a version tag is pushed. First add a `## ` section to [CHANGELOG.md](CHANGELOG.md); it becomes the release notes on GitHub and in the update dialog. Then tag: + +```sh +git tag v2.0 +git push origin v2.0 +``` + +The workflow produces a notarized `ActionBar.dmg` and a Sparkle `appcast.xml`, and attaches both to a GitHub Release. The app checks `releases/latest/download/appcast.xml` for updates. The build number is the commit count, so it always increases. + +It needs these repository secrets: + +| Secret | Value | +| --- | --- | +| `DEVELOPER_ID_CERT_P12` | "Developer ID Application" certificate exported from Keychain Access as `.p12`, base64-encoded | +| `DEVELOPER_ID_CERT_PASSWORD` | Password for the `.p12` | +| `APPLE_ID` | Apple ID email used for notarization | +| `APPLE_APP_PASSWORD` | [App-specific password](https://support.apple.com/en-us/102654) for that Apple ID | +| `APPLE_TEAM_ID` | Apple Developer Team ID | +| `SPARKLE_PRIVATE_KEY` | Sparkle EdDSA private key (`generate_keys --account actionbar -x key.txt`) | + +## License + +The source code is released under the [MIT License](LICENSE). + +The ActionBar name and app icon are not covered by the license. If you distribute a fork, please use a different name and icon. + +ActionBar is an independent project and is not affiliated with GitHub. diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index 4007b10..0000000 --- a/RELEASE.md +++ /dev/null @@ -1,96 +0,0 @@ -# Distribute GitHub Actions Menu App - -## Prerequisites (manual, not code changes) - -1. **Apple Developer Program** — enroll at developer.apple.com ($99/year). Required for both direct download and Mac App Store. -2. **Certificates** — create in Xcode or Apple Developer portal: - - **Developer ID Application** — for direct download distribution - - **Mac App Distribution** + **Mac Installer Distribution** — for Mac App Store -3. **App ID** — register `com.menuapp.github-actions` (or a new bundle ID) in the Developer portal with capabilities: Keychain Sharing, Push Notifications. -4. **Provisioning Profile** — for Mac App Store distribution (not needed for Developer ID). - ---- - -## Code Changes - -### 1. Create `Resources/GitHubActionsMenu.entitlements` - -Production entitlements file (used for both distribution methods): - -```xml - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - keychain-access-groups - - $(AppIdentifierPrefix)com.menuapp.github-actions - - - -``` - -- `app-sandbox` — required for Mac App Store, recommended for Developer ID -- `network.client` — outbound HTTPS to `api.github.com` -- `keychain-access-groups` — for storing the GitHub PAT - -### 2. Update `Makefile` - -Add new targets for production signing, notarization, and DMG creation: - -- `release` target: build + bundle + sign with Developer ID + hardened runtime + entitlements -- `notarize` target: submit to Apple notary service via `xcrun notarytool` -- `dmg` target: create a distributable .dmg - -Key codesign change: -``` -codesign --force --sign "Developer ID Application: YOUR NAME (TEAM_ID)" \ - --options runtime \ - --entitlements Resources/GitHubActionsMenu.entitlements \ - $(APP_BUNDLE) -``` - -### 3. Update `Resources/Info.plist` - -Add `NSHumanReadableCopyright` key (shown in Finder "Get Info"): -```xml -NSHumanReadableCopyright -Copyright © 2025 Your Name. All rights reserved. -``` - ---- - -## Distribution: Direct Download - -After the code changes above: - -1. `make release` — builds, bundles, signs with Developer ID + hardened runtime -2. `make dmg` — packages into a .dmg -3. `make notarize` — submits to Apple notary, staples the ticket -4. Upload the .dmg to GitHub Releases - -Users can download, drag to Applications, and open without Gatekeeper warnings. - -## Distribution: Mac App Store - -Additional steps beyond code changes: - -1. Create the app in **App Store Connect** (appstoreconnect.apple.com) -2. Add screenshots, description, privacy policy URL, category -3. Sign with **Mac App Distribution** certificate instead of Developer ID -4. Upload via `xcrun altool --upload-app` or Transporter.app -5. Submit for App Review - ---- - -## Verification - -1. Run `make release` — should build and codesign with real certificate -2. Run `codesign -dvvv GitHubActionsMenu.app` — verify signing identity and entitlements -3. Run `spctl --assess --type execute GitHubActionsMenu.app` — should say "accepted" after notarization -4. Test on a separate Mac (or a fresh user account) to confirm Gatekeeper doesn't block it diff --git a/Resources/Assets.xcassets/AppIcon.icon/Assets/GitHub_Invertocat_Black.svg b/Resources/Assets.xcassets/AppIcon.icon/Assets/GitHub_Invertocat_Black.svg deleted file mode 100644 index 4f46bf3..0000000 --- a/Resources/Assets.xcassets/AppIcon.icon/Assets/GitHub_Invertocat_Black.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/Resources/Assets.xcassets/AppIcon.icon/Assets/SVG Image 2.svg b/Resources/Assets.xcassets/AppIcon.icon/Assets/SVG Image 2.svg deleted file mode 100644 index 01c86de..0000000 --- a/Resources/Assets.xcassets/AppIcon.icon/Assets/SVG Image 2.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/Resources/Assets.xcassets/AppIcon.icon/icon.json b/Resources/Assets.xcassets/AppIcon.icon/icon.json deleted file mode 100644 index b54fbbc..0000000 --- a/Resources/Assets.xcassets/AppIcon.icon/icon.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "fill" : { - "solid" : "srgb:0.26052,0.26052,0.26052,1.00000" - }, - "groups" : [ - { - "layers" : [ - { - "fill" : { - "solid" : "extended-srgb:0.15686,0.80392,0.25490,1.00000" - }, - "image-name" : "SVG Image 2.svg", - "name" : "SVG Image 2", - "position" : { - "scale" : 1.25, - "translation-in-points" : [ - 0, - 0 - ] - } - }, - { - "blend-mode" : "normal", - "fill" : "automatic", - "glass" : false, - "hidden" : false, - "image-name" : "GitHub_Invertocat_Black.svg", - "name" : "GitHub_Invertocat_Black", - "opacity" : 0.2, - "position" : { - "scale" : 8.4, - "translation-in-points" : [ - 1.8204059415438678e-06, - -9.0234375 - ] - } - } - ], - "lighting" : "individual", - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 - }, - "specular" : true, - "translucency" : { - "enabled" : false, - "value" : 0.5 - } - } - ], - "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" - } -} \ No newline at end of file diff --git a/Resources/Info.plist b/Resources/Info.plist new file mode 100644 index 0000000..86e68ad --- /dev/null +++ b/Resources/Info.plist @@ -0,0 +1,12 @@ + + + + + SUFeedURL + https://github.com/pch/actionbar/releases/latest/download/appcast.xml + SUPublicEDKey + 1jU4KQVuBsoP51EIKDxiQqfq6zRKhehlQDb5nXvblc8= + SUEnableAutomaticChecks + + + diff --git a/Sources/ActionBar/App/ActionBarApp.swift b/Sources/ActionBar/App/ActionBarApp.swift index 6421b68..cb0deb8 100644 --- a/Sources/ActionBar/App/ActionBarApp.swift +++ b/Sources/ActionBar/App/ActionBarApp.swift @@ -4,11 +4,13 @@ import SwiftUI struct ActionBarApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject private var appState = AppState() + @StateObject private var updater = UpdaterService() var body: some Scene { MenuBarExtra { MenuContentView() .environmentObject(appState) + .environmentObject(updater) } label: { MenuBarLabel(appState: appState) .onAppear { appState.start() } diff --git a/Sources/ActionBar/Services/UpdaterService.swift b/Sources/ActionBar/Services/UpdaterService.swift new file mode 100644 index 0000000..0e4862a --- /dev/null +++ b/Sources/ActionBar/Services/UpdaterService.swift @@ -0,0 +1,22 @@ +import Combine +import Sparkle + +@MainActor +final class UpdaterService: ObservableObject { + @Published private(set) var canCheckForUpdates = false + + private let controller = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: nil, + userDriverDelegate: nil + ) + + init() { + controller.updater.publisher(for: \.canCheckForUpdates) + .assign(to: &$canCheckForUpdates) + } + + func checkForUpdates() { + controller.checkForUpdates(nil) + } +} diff --git a/Sources/ActionBar/Views/AboutView.swift b/Sources/ActionBar/Views/AboutView.swift index 31800dc..21395aa 100644 --- a/Sources/ActionBar/Views/AboutView.swift +++ b/Sources/ActionBar/Views/AboutView.swift @@ -1,6 +1,8 @@ import SwiftUI struct AboutView: View { + @EnvironmentObject var updater: UpdaterService + private var appVersion: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" } @@ -21,6 +23,12 @@ struct AboutView: View { } .font(.caption) .buttonStyle(.link) + + Button("Check for Updates…") { + updater.checkForUpdates() + } + .font(.caption) + .disabled(!updater.canCheckForUpdates) } .padding(16) } @@ -28,4 +36,5 @@ struct AboutView: View { #Preview { AboutView() + .environmentObject(UpdaterService()) } diff --git a/Sources/ActionBar/Views/MenuContentView.swift b/Sources/ActionBar/Views/MenuContentView.swift index 2fed820..77d5c31 100644 --- a/Sources/ActionBar/Views/MenuContentView.swift +++ b/Sources/ActionBar/Views/MenuContentView.swift @@ -2,6 +2,7 @@ import SwiftUI struct MenuContentView: View { @EnvironmentObject var appState: AppState + @EnvironmentObject var updater: UpdaterService @State private var now = Date() @State private var showAbout = false @@ -83,6 +84,7 @@ struct MenuContentView: View { .foregroundColor(.secondary) .popover(isPresented: $showAbout) { AboutView() + .environmentObject(updater) } Button("Sign Out") { @@ -202,6 +204,7 @@ struct MenuContentView: View { ] return MenuContentView() .environmentObject(state) + .environmentObject(UpdaterService()) } #Preview("Empty") { @@ -210,6 +213,7 @@ struct MenuContentView: View { state.username = "octocat" return MenuContentView() .environmentObject(state) + .environmentObject(UpdaterService()) } #Preview("Loading") { @@ -219,4 +223,5 @@ struct MenuContentView: View { state.isLoading = true return MenuContentView() .environmentObject(state) + .environmentObject(UpdaterService()) } diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000..f484903 Binary files /dev/null and b/docs/screenshot.png differ