diff --git a/Docs/RELEASING.md b/Docs/RELEASING.md new file mode 100644 index 0000000..e80d5be --- /dev/null +++ b/Docs/RELEASING.md @@ -0,0 +1,58 @@ +# Cue 업데이트 배포 + +Cue의 자동 업데이트는 두 서명을 함께 사용합니다. + +- `Cue Local Signing`: 모든 배포 앱의 코드 서명을 동일하게 유지합니다. +- Sparkle 계정 `cue`: 내려받는 업데이트 파일이 Cue에서 만든 파일인지 + EdDSA로 확인합니다. + +둘 중 하나를 잃어버리거나 새로 만들면 기존 설치본이 다음 업데이트를 +받지 못할 수 있습니다. 로그인 키체인과 Sparkle 개인 키는 별도의 안전한 +저장소에 백업해 주세요. 개인 키 파일은 저장소에 올리지 않습니다. + +```sh +.build/artifacts/sparkle/Sparkle/bin/generate_keys \ + --account cue \ + -x /안전한/외부/경로/cue-sparkle.key +``` + +## 새 버전 배포 + +1. `Info.plist`의 표시 버전과 빌드 번호를 올립니다. +2. 업데이트 앱과 appcast를 만듭니다. + +```sh +./Scripts/build-update.sh +``` + +3. 코드 변경과 새 `appcast.xml`을 `main`에 반영합니다. +4. 같은 버전의 GitHub Release를 만들고 생성된 ZIP을 올립니다. + +```sh +CUE_RELEASE_VERSION=0.8.7 +gh release create "v$CUE_RELEASE_VERSION" \ + "dist/Cue-$CUE_RELEASE_VERSION-macos.zip" \ + --title "Cue $CUE_RELEASE_VERSION" \ + --notes-file /path/to/release-notes.md +``` + +`appcast.xml`의 다운로드 주소와 GitHub Release의 태그·파일명이 정확히 +같아야 합니다. `build-update.sh`는 앱 코드 서명, ZIP 무결성, EdDSA 서명을 +확인한 뒤 파일을 만듭니다. + +## 배포 후 확인 + +```sh +CUE_RELEASE_VERSION=0.8.7 +curl -fsSL \ + https://raw.githubusercontent.com/exemchs/cue/main/appcast.xml + +curl -I -L \ + "https://github.com/exemchs/cue/releases/download/v$CUE_RELEASE_VERSION/Cue-$CUE_RELEASE_VERSION-macos.zip" +``` + +릴리스 ZIP을 다시 내려받아 압축을 풀고 코드 서명도 확인합니다. + +```sh +codesign --verify --deep --strict /path/to/Cue.app +``` diff --git a/Docs/TEAM_DISTRIBUTION.md b/Docs/TEAM_DISTRIBUTION.md index 26a65fd..52f7078 100644 --- a/Docs/TEAM_DISTRIBUTION.md +++ b/Docs/TEAM_DISTRIBUTION.md @@ -1,25 +1,15 @@ # 팀원에게 Cue 공유하기 -Cue는 GitHub 저장소를 공유하고, 각자의 Mac에서 직접 빌드해 설치합니다. - -한 Mac에서 로컬 서명한 `Cue.app`을 그대로 다른 사람에게 보내면 macOS가 -다른 앱으로 인식하거나 권한을 다시 요청할 수 있습니다. 완성된 앱 파일보다 -저장소 주소와 설치 명령을 전달하는 편이 안정적입니다. +Cue는 GitHub Release에 올린 동일한 앱을 팀원이 받아 사용합니다. 이후 +버전도 같은 서명으로 배포하기 때문에 전체 디스크 접근 권한을 매번 다시 +허용하지 않아도 됩니다. ## 처음 설치할 때 -```sh -git clone https://github.com/exemchs/cue.git -cd cue -./Scripts/setup-signing-identity.sh -./Scripts/allow-local-signing.sh -./Scripts/build-app.sh -ditto dist/Cue.app /Applications/Cue.app -open /Applications/Cue.app -``` - -`allow-local-signing.sh`는 로그인 키체인 암호를 직접 입력받기 때문에 -Terminal에서 실행해야 합니다. 입력한 암호는 저장하지 않습니다. +1. [최신 릴리스](https://github.com/exemchs/cue/releases/latest)에서 + `Cue-버전-macos.zip`을 받습니다. +2. 압축을 풀고 `Cue.app`을 `/Applications`로 옮깁니다. +3. 처음 한 번은 `Cue.app`을 Control-클릭하고 **열기**를 선택합니다. 앱을 실행한 뒤에는 다음 항목만 설정합니다. @@ -33,19 +23,16 @@ Terminal에서 실행해야 합니다. 입력한 암호는 저장하지 않습 Mail 같은 소스를 추가할 때 앱마다 별도 권한을 허용할 필요는 없습니다. 아이콘도 설치된 앱에서 자동으로 가져옵니다. -## 업데이트할 때 - -기존 저장소에서 최신 코드를 받은 뒤 같은 명령으로 다시 빌드합니다. +## 자동 업데이트 -```sh -git pull -./Scripts/build-app.sh -ditto dist/Cue.app /Applications/Cue.app -open /Applications/Cue.app -``` +Cue는 6시간마다 GitHub에서 새 버전을 확인합니다. 업데이트 파일은 EdDSA +서명을 통과한 경우에만 사용하고, Cue를 다음에 종료하거나 다시 실행할 때 +자동으로 설치합니다. Cue를 하루 이상 계속 켜두면 설치 준비가 끝났다는 +안내가 나타날 수 있습니다. -권한 요청이 반복되지 않도록 처음 만든 로컬 서명 인증서와 -`/Applications/Cue.app` 경로를 계속 사용합니다. +0.8.6 이하에는 업데이터가 없으므로 최신 버전을 한 번 직접 설치해야 합니다. +기존에 각자 빌드한 Cue를 사용했다면 공유 빌드로 바뀌는 첫 설치에서만 전체 +디스크 접근 권한을 다시 켜야 할 수 있습니다. ## 회사에서 관리하는 Mac diff --git a/Info.plist b/Info.plist index 2fbc048..038c23c 100644 --- a/Info.plist +++ b/Info.plist @@ -15,14 +15,32 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.8.6 + 0.8.7 CFBundleVersion - 42 + 43 LSMinimumSystemVersion 14.0 LSUIElement NSHighResolutionCapable + SUAllowsAutomaticUpdates + + SUAutomaticallyUpdate + + SUEnableAutomaticChecks + + SUFeedURL + https://raw.githubusercontent.com/exemchs/cue/main/appcast.xml + SUPublicEDKey + /HhZoRAFWV8E0I80JwexpiZBvqE31OeMGCXwM4BHNmw= + SUScheduledCheckInterval + 21600 + SUScheduledImpatientCheckInterval + 86400 + SUShowReleaseNotes + + SUVerifyUpdateBeforeExtraction + diff --git a/PRIVACY.md b/PRIVACY.md index 2cc4088..65638e5 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -36,9 +36,15 @@ Mattermost 연결 파일에는 알림을 구분하기 위한 SHA-256 값, 확인 ## 네트워크 연결 -Cue는 OpenAI, Mattermost 또는 Cue 서버에 접속하지 않습니다. Mattermost -연결은 현재 Mac의 `127.0.0.1:49371` 안에서만 동작합니다. Mattermost 앱 -자체는 원래 사용하던 네트워크 연결을 그대로 사용합니다. +Cue는 OpenAI, Mattermost 또는 별도 Cue 서버에 작업과 알림 데이터를 +보내지 않습니다. Mattermost 연결은 현재 Mac의 `127.0.0.1:49371` +안에서만 동작합니다. Mattermost 앱 자체는 원래 사용하던 네트워크 연결을 +그대로 사용합니다. + +자동 업데이트를 확인하고 받을 때만 `raw.githubusercontent.com`과 +`github.com`에 연결합니다. 이 요청에는 Cue 버전과 macOS가 일반적인 +네트워크 요청에 포함하는 정보만 전달되며, 작업 제목, 메시지, 소스 설정은 +포함하지 않습니다. 내려받은 앱은 EdDSA 서명을 확인한 뒤 설치합니다. ## 로컬 데이터 지우기 diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..1566f0c --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "1f360802317f2a0908dc908830d9afe475e894286b80960c85cd78772be19ea7", + "pins" : [ + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "6276ba2b404829d139c45ff98427cf90e2efc59b", + "version" : "2.9.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 7eb72d6..6e9b44a 100644 --- a/Package.swift +++ b/Package.swift @@ -10,14 +10,29 @@ let package = Package( products: [ .executable(name: "Cue", targets: ["Cue"]) ], + dependencies: [ + .package( + url: "https://github.com/sparkle-project/Sparkle", + exact: "2.9.2" + ) + ], targets: [ .executableTarget( name: "Cue", + dependencies: [ + .product(name: "Sparkle", package: "Sparkle") + ], resources: [ .process("Resources") ], linkerSettings: [ - .linkedLibrary("sqlite3") + .linkedLibrary("sqlite3"), + .unsafeFlags([ + "-Xlinker", + "-rpath", + "-Xlinker", + "@executable_path/../Frameworks" + ]) ] ) ] diff --git a/README.md b/README.md index c543f71..b1c3309 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ Cue는 진행 중인 작업과 확인이 필요한 알림을
> [!IMPORTANT] -> Cue는 아직 직접 빌드해 사용하는 초기 버전입니다. 일부 기능은 공식 API -> 대신 각 앱이 Mac에 저장한 로컬 데이터를 읽습니다. macOS나 앱이 -> 업데이트되면 일부 연결이 동작하지 않을 수 있습니다. 권한을 허용하기 전 +> Cue의 일부 기능은 공식 API 대신 각 앱이 Mac에 저장한 로컬 데이터를 +> 읽습니다. macOS나 앱이 업데이트되면 일부 연결이 동작하지 않을 수 +> 있습니다. 권한을 허용하기 전 > [개인정보 처리 방식](PRIVACY.md)을 확인해 주세요. ## 왜 만들었나요 @@ -89,39 +89,18 @@ macOS 알림 센터에 저장된 알림 제목과 내용을 보여줍니다. 앱 ## 설치 -현재는 각자의 Mac에서 직접 빌드해 설치합니다. Pretendard Light, Regular, -Medium은 Cue에 포함되어 있어 따로 설치하지 않아도 됩니다. +1. [최신 릴리스](https://github.com/exemchs/cue/releases/latest)에서 + `Cue-버전-macos.zip`을 받습니다. +2. 압축을 풀고 `Cue.app`을 `/Applications`로 옮깁니다. +3. 처음 한 번은 `Cue.app`을 Control-클릭하고 **열기**를 선택합니다. ### 필요한 환경 - 노치가 있는 MacBook - macOS 14 이상 -- Swift 6을 포함한 Xcode Command Line Tools - 상태나 알림을 확인할 ChatGPT, Mattermost 또는 다른 앱 - Homebrew Node.js (Mattermost 알림을 원래 대화로 연결할 때만 필요) -Command Line Tools가 없다면 먼저 설치합니다. - -```sh -xcode-select --install -``` - -Cue를 내려받아 빌드하고 `/Applications`에 설치합니다. - -```sh -git clone https://github.com/exemchs/cue.git -cd cue -./Scripts/setup-signing-identity.sh -./Scripts/allow-local-signing.sh -./Scripts/build-app.sh -ditto dist/Cue.app /Applications/Cue.app -open /Applications/Cue.app -``` - -`allow-local-signing.sh`는 로그인 키체인 암호를 직접 입력받기 때문에 -Terminal에서 실행해야 합니다. 입력한 암호는 화면에 표시되거나 저장되지 -않습니다. 만들어진 로컬 인증서도 각 Mac에만 남습니다. - 설치 후에는 다음 항목만 연결하면 됩니다. 1. 노치의 Cue 영역을 오른쪽 클릭한 뒤 **앱 추가**를 누릅니다. @@ -130,8 +109,10 @@ Terminal에서 실행해야 합니다. 입력한 암호는 화면에 표시되 보안 → 전체 디스크 접근 권한**에서 `/Applications/Cue.app`을 켭니다. 4. macOS가 요청하는 경우 Cue를 한 번 다시 실행합니다. -권한을 반복해서 요청하지 않도록 Cue는 `/Applications`에 두고, 업데이트할 -때도 같은 로컬 서명 인증서를 사용합니다. +Cue는 6시간마다 새 버전을 확인하고, 서명을 확인한 업데이트만 백그라운드로 +받습니다. 받은 업데이트는 Cue를 다음에 종료하거나 다시 실행할 때 +설치됩니다. 0.8.6 이하를 사용 중이라면 위 설치 과정을 한 번만 다시 +진행해야 하며, 그다음부터 자동으로 업데이트됩니다. ## 데이터는 어떻게 처리하나요 @@ -140,6 +121,7 @@ Terminal에서 실행해야 합니다. 입력한 암호는 화면에 표시되 - 분석 도구를 사용하지 않습니다. - 작업과 알림 데이터를 외부 서버로 보내지 않습니다. - 설정과 확인 기록은 현재 Mac에만 저장합니다. +- 새 버전 확인을 위해 GitHub에만 연결합니다. Cue가 어떤 파일을 읽고 무엇을 저장하는지는 [PRIVACY.md](PRIVACY.md)에 정리해두었습니다. @@ -165,17 +147,26 @@ swift run Cue --preview-regular-app --preview-many-platforms \ ./Scripts/build-app.sh ``` +업데이트 파일과 appcast 만들기(배포 관리자): + +```sh +./Scripts/build-update.sh +``` + 주요 구성: - `CueModel`: 소스 설정, 상태 정리, 로컬 저장 - `LocalActivityMonitor`: Codex와 macOS 알림 센터 읽기 - `NotificationDatabaseWatcher`: 새 macOS 알림을 감지해 목록 갱신 - `MattermostBridgeSupervisor`: Mattermost의 원래 대화로 이동하기 위한 로컬 연결 +- `Sparkle`: GitHub 릴리스 확인, 업데이트 검증과 설치 - `CueNotchView`: 노치, 활동 목록, 소스 관리, 순서 변경 - `NotchPanelController`: 노치 위치와 멀티 디스플레이 동작 팀원에게 공유하는 방법은 [팀원 공유 문서](Docs/TEAM_DISTRIBUTION.md)에 정리해두었습니다. +업데이트를 배포하는 순서는 +[배포 문서](Docs/RELEASING.md)에서 확인할 수 있습니다. ## 함께 만들기 @@ -191,7 +182,8 @@ Cue는 [MIT License](LICENSE)로 배포합니다. 앱에 포함된 Pretendard 1.3.9는 SIL Open Font License 1.1을 따릅니다. 라이선스 원문은 `Sources/Cue/Resources/Licenses/Pretendard-OFL-1.1.txt`에 포함되어 -있습니다. +있습니다. 자동 업데이트에 사용하는 Sparkle은 MIT License를 따르며, +배포 앱 안에 라이선스 원문을 함께 포함합니다. ChatGPT, OpenAI, Mattermost, macOS와 각 앱의 이름 및 아이콘은 해당 권리자에게 속합니다. Cue는 해당 회사의 공식 제품이 아닙니다. diff --git a/Scripts/build-app.sh b/Scripts/build-app.sh index a38493e..94e9a45 100755 --- a/Scripts/build-app.sh +++ b/Scripts/build-app.sh @@ -48,9 +48,15 @@ swift build -c release STAGING_DIR="$(mktemp -d "$DIST_DIR/.cue-package.XXXXXX")" STAGED_APP="$STAGING_DIR/Cue.app" -mkdir -p "$STAGED_APP/Contents/MacOS" "$STAGED_APP/Contents/Resources" +mkdir -p \ + "$STAGED_APP/Contents/MacOS" \ + "$STAGED_APP/Contents/Resources" \ + "$STAGED_APP/Contents/Frameworks" cp "$BUILD_DIR/release/Cue" "$STAGED_APP/Contents/MacOS/Cue" +ditto "$BUILD_DIR/release/Sparkle.framework" \ + "$STAGED_APP/Contents/Frameworks/Sparkle.framework" + cp "$PROJECT_DIR/Sources/Cue/Resources/"*.mjs \ "$STAGED_APP/Contents/Resources/" @@ -58,6 +64,8 @@ ditto "$PROJECT_DIR/Sources/Cue/Resources/Fonts" \ "$STAGED_APP/Contents/Resources/Fonts" ditto "$PROJECT_DIR/Sources/Cue/Resources/Licenses" \ "$STAGED_APP/Contents/Resources/Licenses" +cp "$BUILD_DIR/checkouts/Sparkle/LICENSE" \ + "$STAGED_APP/Contents/Resources/Licenses/Sparkle-LICENSE" cp "$PROJECT_DIR/Info.plist" "$STAGED_APP/Contents/Info.plist" plutil -replace CFBundleIdentifier \ diff --git a/Scripts/build-update.sh b/Scripts/build-update.sh new file mode 100755 index 0000000..8d60e75 --- /dev/null +++ b/Scripts/build-update.sh @@ -0,0 +1,90 @@ +#!/bin/zsh +set -euo pipefail + +PROJECT_DIR="${0:A:h:h}" +BUILD_DIR="$PROJECT_DIR/.build" +DIST_DIR="$PROJECT_DIR/dist" +APP_DIR="$DIST_DIR/Cue.app" +SPARKLE_BIN="$BUILD_DIR/artifacts/sparkle/Sparkle/bin" +SPARKLE_ACCOUNT="${CUE_SPARKLE_ACCOUNT:-cue}" +STAGING_DIR="" + +cleanup() { + local exit_code=$? + if [[ -n "$STAGING_DIR" && -d "$STAGING_DIR" ]]; then + find "$STAGING_DIR" -depth -delete + fi + return $exit_code +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +"$PROJECT_DIR/Scripts/build-app.sh" + +if [[ ! -x "$SPARKLE_BIN/generate_keys" \ + || ! -x "$SPARKLE_BIN/generate_appcast" ]]; then + print -u2 "Sparkle update tools were not found in $SPARKLE_BIN." + exit 1 +fi + +VERSION="$(/usr/libexec/PlistBuddy \ + -c "Print :CFBundleShortVersionString" \ + "$APP_DIR/Contents/Info.plist")" +BUILD_NUMBER="$(/usr/libexec/PlistBuddy \ + -c "Print :CFBundleVersion" \ + "$APP_DIR/Contents/Info.plist")" +EMBEDDED_PUBLIC_KEY="$(/usr/libexec/PlistBuddy \ + -c "Print :SUPublicEDKey" \ + "$APP_DIR/Contents/Info.plist")" +KEYCHAIN_PUBLIC_KEY="$("$SPARKLE_BIN/generate_keys" \ + --account "$SPARKLE_ACCOUNT" \ + -p)" + +if [[ "$EMBEDDED_PUBLIC_KEY" != "$KEYCHAIN_PUBLIC_KEY" ]]; then + print -u2 "The Sparkle key in Info.plist does not match account:" + print -u2 "$SPARKLE_ACCOUNT" + exit 1 +fi + +STAGING_DIR="$(mktemp -d "$DIST_DIR/.cue-update.XXXXXX")" +ARCHIVE_NAME="Cue-$VERSION-macos.zip" +STAGED_ARCHIVE="$STAGING_DIR/$ARCHIVE_NAME" +FINAL_ARCHIVE="$DIST_DIR/$ARCHIVE_NAME" +DOWNLOAD_PREFIX="https://github.com/exemchs/cue/releases/download/v$VERSION/" + +ditto -c -k --sequesterRsrc --keepParent \ + "$APP_DIR" \ + "$STAGED_ARCHIVE" + +"$SPARKLE_BIN/generate_appcast" \ + --account "$SPARKLE_ACCOUNT" \ + --download-url-prefix "$DOWNLOAD_PREFIX" \ + --link "https://github.com/exemchs/cue" \ + --maximum-versions 1 \ + --maximum-deltas 0 \ + -o "$STAGING_DIR/appcast.xml" \ + "$STAGING_DIR" + +UPDATE_SIGNATURE="$(sed -n \ + 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p' \ + "$STAGING_DIR/appcast.xml")" +if [[ -z "$UPDATE_SIGNATURE" ]]; then + print -u2 "The generated appcast does not contain an update signature." + exit 1 +fi +"$SPARKLE_BIN/sign_update" \ + --account "$SPARKLE_ACCOUNT" \ + --verify \ + "$STAGED_ARCHIVE" \ + "$UPDATE_SIGNATURE" + +cp "$STAGING_DIR/appcast.xml" "$PROJECT_DIR/appcast.xml" +mv -f "$STAGED_ARCHIVE" "$FINAL_ARCHIVE" + +codesign --verify --deep --strict "$APP_DIR" +unzip -tq "$FINAL_ARCHIVE" >/dev/null + +echo "Cue $VERSION ($BUILD_NUMBER)" +echo "$FINAL_ARCHIVE" +echo "$PROJECT_DIR/appcast.xml" diff --git a/Sources/Cue/CueAppDelegate.swift b/Sources/Cue/CueAppDelegate.swift index 3847449..b32eae6 100644 --- a/Sources/Cue/CueAppDelegate.swift +++ b/Sources/Cue/CueAppDelegate.swift @@ -1,12 +1,19 @@ import AppKit +import Sparkle @MainActor final class CueAppDelegate: NSObject, NSApplicationDelegate { private var panelController: NotchPanelController? private var model: CueModel? + private lazy var updaterController = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: nil, + userDriverDelegate: nil + ) func applicationDidFinishLaunching(_ notification: Notification) { CueFontRegistry.registerBundledFonts() + _ = updaterController let model = CueModel() let controller = NotchPanelController(model: model) diff --git a/appcast.xml b/appcast.xml new file mode 100644 index 0000000..bf16f83 --- /dev/null +++ b/appcast.xml @@ -0,0 +1,16 @@ + + + + Cue + + 0.8.7 + Mon, 27 Jul 2026 17:20:20 +0900 + https://github.com/exemchs/cue + 43 + 0.8.7 + 14.0 + arm64 + + + + \ No newline at end of file