diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67ffd48..bf3494c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,3 +26,37 @@ jobs: - name: Run clippy run: cargo clippy --all-targets -- -D warnings + + macos-app: + name: macOS app package + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust components + run: rustup component add rustfmt clippy + + - name: Check formatting + run: cargo fmt --check + + - name: Run tests + run: cargo test + + - name: Run clippy + run: cargo clippy --all-targets -- -D warnings + + - name: Build and smoke-test menu bar app + run: scripts/test-macos-app.sh + + - name: Package menu bar app + run: scripts/package-macos-app.sh + + - name: Upload app package + uses: actions/upload-artifact@v4 + with: + name: plainlink-macos-app + path: | + dist/packages/*.zip + dist/packages/*.sha256 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79ef483..a665269 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ For macOS menu bar app changes: ```sh scripts/test-macos-app.sh +scripts/package-macos-app.sh ``` ## Rule Contributions diff --git a/README.md b/README.md index 50b52c2..be0d909 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,14 @@ scripts/test-macos-app.sh This creates `dist/PlainLink.app`. +To build a local unsigned release zip: + +```sh +scripts/package-macos-app.sh +``` + +This creates `dist/packages/PlainLink--macos-.zip` and a `.sha256` checksum. + ## Project Layout ```text @@ -94,6 +102,7 @@ docs/ scripts/ build-macos-app.sh Build dist/PlainLink.app test-macos-app.sh Build and smoke-test the app bundle + package-macos-app.sh Create an unsigned zip and checksum ``` ## Contributing diff --git a/docs/MACOS.md b/docs/MACOS.md index 739a16d..0620ce6 100644 --- a/docs/MACOS.md +++ b/docs/MACOS.md @@ -16,6 +16,18 @@ The generated app lives at: dist/PlainLink.app ``` +Create an unsigned zip and checksum for testing: + +```sh +scripts/package-macos-app.sh +``` + +Packages are written to: + +```text +dist/packages/ +``` + The app is built with Swift/AppKit and Apple Command Line Tools. It embeds the release Rust CLI, then uses that CLI for all product actions: ```mermaid diff --git a/docs/MENUBAR.md b/docs/MENUBAR.md index fab2f49..be0e4d4 100644 --- a/docs/MENUBAR.md +++ b/docs/MENUBAR.md @@ -32,6 +32,28 @@ scripts/test-macos-app.sh This builds the bundle, validates `Info.plist`, confirms both executables exist, runs the menu app smoke test, and checks the embedded CLI version command. +## Package + +```sh +scripts/package-macos-app.sh +``` + +The package script rebuilds and smoke-tests the app, then writes: + +```text +dist/packages/PlainLink--macos-.zip +dist/packages/PlainLink--macos-.zip.sha256 +``` + +The zip is unsigned and not notarized. It is meant for MVP testing and GitHub Actions artifacts. + +Verify a downloaded package: + +```sh +cd dist/packages +shasum -a 256 -c PlainLink--macos-.zip.sha256 +``` + ## Runtime Flow ```mermaid diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7f8e450..e5270da 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -9,6 +9,7 @@ - Restore last original URL. - User LaunchAgent install/status/restart/uninstall. - Stable user install/uninstall and doctor checks. +- Unsigned macOS release packaging. - Human-readable default rules. - Fixture-backed rule QA. - Contributor docs with Mermaid diagrams. @@ -16,7 +17,7 @@ ## Next - Add signed release builds. -- Add a downloadable app artifact. +- Publish downloadable app artifacts from GitHub Actions. - Add clearer first-run onboarding inside the menu bar app. - Add optional remote ruleset updates with clear privacy controls. diff --git a/scripts/package-macos-app.sh b/scripts/package-macos-app.sh new file mode 100755 index 0000000..8474c62 --- /dev/null +++ b/scripts/package-macos-app.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +APP_NAME="PlainLink" +APP_DIR="$ROOT_DIR/dist/$APP_NAME.app" +PACKAGE_DIR="$ROOT_DIR/dist/packages" +VERSION=$(awk -F '=' '$1 ~ /^[[:space:]]*version[[:space:]]*$/ { gsub(/[ "]/, "", $2); print $2; exit }' "$ROOT_DIR/Cargo.toml") +ARCH=$(uname -m) +ARTIFACT_NAME="$APP_NAME-$VERSION-macos-$ARCH.zip" +ZIP_PATH="$PACKAGE_DIR/$ARTIFACT_NAME" +SHA_PATH="$ZIP_PATH.sha256" + +if [ "$(uname -s)" != "Darwin" ]; then + echo "PlainLink.app packages can only be built on macOS." >&2 + exit 1 +fi + +if [ -z "$VERSION" ]; then + echo "could not read package version from Cargo.toml" >&2 + exit 1 +fi + +command -v ditto >/dev/null 2>&1 || { + echo "ditto is required to package PlainLink.app." >&2 + exit 1 +} + +command -v shasum >/dev/null 2>&1 || { + echo "shasum is required to create package checksums." >&2 + exit 1 +} + +"$ROOT_DIR/scripts/test-macos-app.sh" + +test -d "$APP_DIR" || { + echo "missing app bundle: $APP_DIR" >&2 + exit 1 +} + +mkdir -p "$PACKAGE_DIR" +rm -f "$ZIP_PATH" "$SHA_PATH" + +cd "$ROOT_DIR/dist" +ditto -c -k --sequesterRsrc --keepParent "$APP_NAME.app" "$ZIP_PATH" + +cd "$PACKAGE_DIR" +shasum -a 256 "$ARTIFACT_NAME" > "$ARTIFACT_NAME.sha256" + +echo "Packaged $ZIP_PATH" +echo "Checksum $SHA_PATH" diff --git a/tests/macos_app_bundle.rs b/tests/macos_app_bundle.rs index 16e28f9..dd9f1b9 100644 --- a/tests/macos_app_bundle.rs +++ b/tests/macos_app_bundle.rs @@ -10,6 +10,8 @@ fn macos_menu_app_assets_are_wired() { fs::read_to_string("scripts/build-macos-app.sh").expect("build script should exist"); let test_script = fs::read_to_string("scripts/test-macos-app.sh").expect("test script should exist"); + let package_script = + fs::read_to_string("scripts/package-macos-app.sh").expect("package script should exist"); for command in [ "install", @@ -36,4 +38,9 @@ fn macos_menu_app_assets_are_wired() { assert!(build_script.contains("-module-cache-path")); assert!(test_script.contains("--smoke-test")); assert!(test_script.contains("plutil -lint")); + assert!(package_script.contains("scripts/test-macos-app.sh")); + assert!(package_script.contains("ditto -c -k --sequesterRsrc --keepParent")); + assert!(package_script.contains("shasum -a 256")); + assert!(package_script.contains("ARTIFACT_NAME=")); + assert!(package_script.contains("macos-$ARCH.zip")); }