diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ee69404..5c99139f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,7 @@ jobs: # recognises the dev lane by this exact string # (FormFactor.devBundleIdentifier) and any other id would # resolve to the release fallback. - id="$(plutil -extract CFBundleIdentifier raw dist/OnetimePad.app/Contents/Info.plist)" + id="$(plutil -extract CFBundleIdentifier raw "dist/OnetimePad Debug.app/Contents/Info.plist")" case "$id" in dev.onetimesecret.pad.debug) echo "bundle id: $id" ;; *) echo "expected the dev bundle id dev.onetimesecret.pad.debug, got: $id" >&2; exit 1 ;; @@ -130,19 +130,19 @@ jobs: # Bundle.main under xctest is the test runner and carries none # of these keys. The reach no Swift test has is the packaging # script itself dropping or overriding the key on the way into - # dist/OnetimePad.app, and that is the regression this step + # the dist/ bundle, and that is the regression this step # exists for. Both halves are wanted; neither replaces the # other. - sudden="$(plutil -extract NSSupportsSuddenTermination raw dist/OnetimePad.app/Contents/Info.plist)" + sudden="$(plutil -extract NSSupportsSuddenTermination raw "dist/OnetimePad Debug.app/Contents/Info.plist")" test "$sudden" = "true" \ || { echo "expected NSSupportsSuddenTermination true, got: $sudden" >&2; exit 1; } # The pre-signature digest is the published artifact; an # empty file means the hash pipeline broke silently. - test -s dist/OnetimePad.presig.sha256 \ - || { echo "dist/OnetimePad.presig.sha256 is missing or empty" >&2; exit 1; } - cmp THIRD_PARTY_NOTICES.md dist/OnetimePad.app/Contents/Resources/THIRD_PARTY_NOTICES.md + test -s "dist/OnetimePad Debug.presig.sha256" \ + || { echo "dist/OnetimePad Debug.presig.sha256 is missing or empty" >&2; exit 1; } + cmp THIRD_PARTY_NOTICES.md "dist/OnetimePad Debug.app/Contents/Resources/THIRD_PARTY_NOTICES.md" cmp THIRD_PARTY_NOTICES.md bindings/CompanionCore.xcframework/THIRD_PARTY_NOTICES.md - cat dist/OnetimePad.presig.sha256 + cat "dist/OnetimePad Debug.presig.sha256" # The release lane, which exists for the checks that only the release # configuration reaches. package-app.sh refuses to ship a binary @@ -202,7 +202,7 @@ jobs: # cannot collide with the dev lane or an App Store build on # defaults, keychain items, or the state directory they # derive from it (ADR-0012, and ADR-0016 case 4). - id="$(plutil -extract CFBundleIdentifier raw dist/OnetimePad.app/Contents/Info.plist)" + id="$(plutil -extract CFBundleIdentifier raw "dist/OnetimePad Local.app/Contents/Info.plist")" case "$id" in dev.onetimesecret.pad) echo "bundle id: $id" ;; *) echo "expected the local bundle id dev.onetimesecret.pad, got: $id" >&2; exit 1 ;; @@ -210,15 +210,15 @@ jobs: # ADR-0016 case 2, on the plist that actually ships. The # debug job carries the note on why the packaged bundle, and # not only the source plist, is worth asserting. - sudden="$(plutil -extract NSSupportsSuddenTermination raw dist/OnetimePad.app/Contents/Info.plist)" + sudden="$(plutil -extract NSSupportsSuddenTermination raw "dist/OnetimePad Local.app/Contents/Info.plist")" test "$sudden" = "true" \ || { echo "expected NSSupportsSuddenTermination true, got: $sudden" >&2; exit 1; } - test -s dist/OnetimePad.presig.sha256 \ - || { echo "dist/OnetimePad.presig.sha256 is missing or empty" >&2; exit 1; } - cmp THIRD_PARTY_NOTICES.md dist/OnetimePad.app/Contents/Resources/THIRD_PARTY_NOTICES.md + test -s "dist/OnetimePad Local.presig.sha256" \ + || { echo "dist/OnetimePad Local.presig.sha256 is missing or empty" >&2; exit 1; } + cmp THIRD_PARTY_NOTICES.md "dist/OnetimePad Local.app/Contents/Resources/THIRD_PARTY_NOTICES.md" cmp THIRD_PARTY_NOTICES.md bindings/CompanionCore.xcframework/THIRD_PARTY_NOTICES.md test "$(cat bindings/CompanionCore.features)" = "release" - cat dist/OnetimePad.presig.sha256 + cat "dist/OnetimePad Local.presig.sha256" # The ADR conventions (docs/adr/README.md) are structural enough to # check: required metadata, the four sections every record carries, diff --git a/CHANGELOG.md b/CHANGELOG.md index 81a7cd6a..44348db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Development bundles no longer take the TestFlight copy's name.** + `scripts/dev.sh` builds `dist/OnetimePad Debug.app` and + `scripts/install.sh` installs `/Applications/OnetimePad Local.app`; + only `package-app.sh --app-store` still produces `OnetimePad.app`. + The names live in `scripts/build-lanes.sh` beside the bundle ids. + `install.sh` removes a local install it left at + `/Applications/OnetimePad.app` only when that bundle carries the local + id, so a TestFlight install there is untouched. + - **Settings offers two page layouts, Tabs and Timeline** (app 0.26.0). Tabs draws named pages along the bottom at full width; Timeline, still a prototype, groups live pages by day down the side. The separate diff --git a/SECURITY.md b/SECURITY.md index 821783fa..c36ea638 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,10 +20,11 @@ disabled). ## Verifying a build The packaging script hashes the app bundle it has just assembled, -before `codesign` runs, and writes the result to -`dist/OnetimePad.presig.sha256`. +before `codesign` runs, and writes the result beside it: +`dist/OnetimePad Local.presig.sha256` for the default local build, +`dist/OnetimePad.presig.sha256` for an App Store build. -The file holds one line, ` OnetimePad.app`. The digest is +The file holds one line, ` .app`. The digest is taken over every regular file in the bundle, by relative path and content: `find` the bundle, sort the paths under `LC_ALL=C` so the order is byte order rather than the caller's locale, hash each file, @@ -36,7 +37,7 @@ To reproduce it: git checkout # clean tree, see the note below scripts/build-core.sh scripts/package-app.sh -cat dist/OnetimePad.presig.sha256 +cat "dist/OnetimePad Local.presig.sha256" ``` Compare that line against the published one. The build script stamps diff --git a/docs/dogfood/DOGFOOD.md b/docs/dogfood/DOGFOOD.md index eb7fe0e8..c1b0e759 100644 --- a/docs/dogfood/DOGFOOD.md +++ b/docs/dogfood/DOGFOOD.md @@ -14,7 +14,10 @@ scripts/install.sh ``` Builds the core only if it is stale, builds the app, and installs it -to `/Applications` (override with `APP_DEST`). Running the installed +to `/Applications` as `OnetimePad Local.app` (override the directory +with `APP_DEST`). A TestFlight install keeps `OnetimePad.app`. A local +install left at that name by an older `scripts/install.sh` is removed, +and only when its bundle id is `dev.onetimesecret.pad`. Running the installed copy matters for two reasons: - Rebuilds in the repo never touch it. `swift build` re-signs whatever @@ -325,8 +328,8 @@ binary from a terminal still shows those lines as they happen, and is still the fastest loop while you are working on the core: ```bash -osascript -e 'tell application "/Applications/OnetimePad.app" to quit' -/Applications/OnetimePad.app/Contents/MacOS/OnetimePad +osascript -e 'tell application "/Applications/OnetimePad Local.app" to quit' +"/Applications/OnetimePad Local.app/Contents/MacOS/OnetimePad" ``` Reach for `log show` for the launch you cannot reproduce, and the diff --git a/docs/qa/verification-procedures/file-draft-lifecycle.md b/docs/qa/verification-procedures/file-draft-lifecycle.md index 131020f7..4897d1c1 100644 --- a/docs/qa/verification-procedures/file-draft-lifecycle.md +++ b/docs/qa/verification-procedures/file-draft-lifecycle.md @@ -80,7 +80,7 @@ as the state directory is. ## Case 1: a kill with a dirty file open -1. Launch `dist/OnetimePad.app`. Open `/tmp/qa-draft.txt` through File > +1. Launch `dist/OnetimePad Debug.app`. Open `/tmp/qa-draft.txt` through File > Open. Confirm the tab appears in the FILES group, that it carries no countdown gauge, and that the header reads saved. 2. Type a distinctive line into the file, for example @@ -90,7 +90,7 @@ as the state directory is. `drafts.sealed` exists and has a recent mtime. 4. Note the wall clock time. This is the draft's last edit time, and step 6 checks it. -5. `pkill -9 -f 'dist/OnetimePad\.app'`, then `ls -la "$STATE"` and +5. `pkill -9 -f 'dist/OnetimePad Debug\.app'`, then `ls -la "$STATE"` and record every file present, in particular anything matching `*.[0-9a-f]*.tmp`. 6. Launch the app. diff --git a/docs/qa/verification-procedures/pinned-over-fullscreen.md b/docs/qa/verification-procedures/pinned-over-fullscreen.md index a2983c9a..d70180b0 100644 --- a/docs/qa/verification-procedures/pinned-over-fullscreen.md +++ b/docs/qa/verification-procedures/pinned-over-fullscreen.md @@ -49,7 +49,7 @@ next. This procedure is what tells the three outcomes apart. Quit any running copy first (`scripts/quit-app.sh`; only the graceful path saves state), then `scripts/package-app.sh && open -dist/OnetimePad.app`. +"dist/OnetimePad Local.app"`. In a second terminal, watch the surface's own log, which now carries the gate: diff --git a/docs/qa/verification-procedures/raised-card-drag-tracking.md b/docs/qa/verification-procedures/raised-card-drag-tracking.md index c875b915..7a9e2164 100644 --- a/docs/qa/verification-procedures/raised-card-drag-tracking.md +++ b/docs/qa/verification-procedures/raised-card-drag-tracking.md @@ -41,7 +41,7 @@ not document and a unit test cannot reach, hence "verify on hardware". Quit any running instance first (`swift build` re-signs in place and SIGKILLs a live one), then -`scripts/package-app.sh && open dist/OnetimePad.app`. +`scripts/package-app.sh && open "dist/OnetimePad Local.app"`. - Raise with ⌃⌥Space, press the header, drag slowly across the screen. **Pass:** the card stays glued to the pointer for the whole sweep. diff --git a/docs/qa/verification-procedures/re-signed-bundle.md b/docs/qa/verification-procedures/re-signed-bundle.md index 4a2663a0..b7bc15f0 100644 --- a/docs/qa/verification-procedures/re-signed-bundle.md +++ b/docs/qa/verification-procedures/re-signed-bundle.md @@ -61,7 +61,7 @@ in `SUPERSEDED_MAGICS`, never on a key failure 2. Record the evidence that must not change: ```sh - codesign -dv --verbose=4 /Applications/OnetimePad.app 2>&1 | grep -E 'Identifier|Authority|TeamIdentifier' + codesign -dv --verbose=4 "/Applications/OnetimePad Local.app" 2>&1 | grep -E 'Identifier|Authority|TeamIdentifier' ls -la "$STATE" shasum -a 256 "$STATE/state.sealed" ``` @@ -70,9 +70,9 @@ in `SUPERSEDED_MAGICS`, never on a key failure the easiest different identity: ```sh - codesign --force --deep --sign - /Applications/OnetimePad.app - codesign --verify --strict /Applications/OnetimePad.app - codesign -dv --verbose=4 /Applications/OnetimePad.app 2>&1 | grep Authority + codesign --force --deep --sign - "/Applications/OnetimePad Local.app" + codesign --verify --strict "/Applications/OnetimePad Local.app" + codesign -dv --verbose=4 "/Applications/OnetimePad Local.app" 2>&1 | grep Authority ``` Signing with a second real certificate instead is equally valid and diff --git a/docs/qa/verification-procedures/reboot.md b/docs/qa/verification-procedures/reboot.md index 130248b3..1df2cc02 100644 --- a/docs/qa/verification-procedures/reboot.md +++ b/docs/qa/verification-procedures/reboot.md @@ -24,7 +24,7 @@ scripts/install.sh `scripts/install.sh` builds the core, packages the release bundle, asks a running installed copy to quit gracefully, and installs to -`/Applications/OnetimePad.app`. Pin `CODESIGN_IDENTITY` in the +`/Applications/OnetimePad Local.app`. Pin `CODESIGN_IDENTITY` in the local environment file (template: `environments/example/.env.example`) first, or every reinstall resets the Keychain confirmations this procedure depends on. diff --git a/docs/qa/verification-procedures/spaces-and-cmd-tab.md b/docs/qa/verification-procedures/spaces-and-cmd-tab.md index 74a5d84f..6afcc6dd 100644 --- a/docs/qa/verification-procedures/spaces-and-cmd-tab.md +++ b/docs/qa/verification-procedures/spaces-and-cmd-tab.md @@ -88,7 +88,7 @@ does not. Quit any running copy first (`scripts/quit-app.sh`; only the graceful path saves state), then `scripts/package-app.sh && open -dist/OnetimePad.app`. +"dist/OnetimePad Local.app"`. Have at least three desktops (Mission Control, add two), and close both Settings and About before starting, since an open ordinary window is the diff --git a/scripts/build-lanes.sh b/scripts/build-lanes.sh index b10b2857..45f2b61e 100755 --- a/scripts/build-lanes.sh +++ b/scripts/build-lanes.sh @@ -21,6 +21,12 @@ PRODUCTION_BUNDLE_ID="com.onetimesecret.pad" LOCAL_BUNDLE_ID="dev.onetimesecret.pad" DEV_BUNDLE_ID="dev.onetimesecret.pad.debug" +# Bundle file names. Only App Store builds take the plain name, so a local +# install never replaces /Applications/OnetimePad.app, the path a TestFlight +# install occupies, and a debug bundle never reads as either in Finder. +PRODUCTION_APP_NAME="OnetimePad" +LOCAL_APP_NAME="OnetimePad Local" +DEV_APP_NAME="OnetimePad Debug" ENVIRONMENTS_DIR="${ONETIMEPAD_ENVIRONMENTS_DIR:-$HOME/.local/appledev/CompanionApp/environments}" SIGNING_VARIABLES=(CODESIGN_IDENTITY INSTALLER_IDENTITY PROVISIONING_PROFILE) @@ -89,18 +95,21 @@ select_build_lane() { # dev) CONFIG="debug" BUILD_BUNDLE_ID="$DEV_BUNDLE_ID" + BUILD_APP_NAME="$DEV_APP_NAME" INSTALLER_IDENTITY="" PROFILE_CLASS="development" ;; local) CONFIG="release" BUILD_BUNDLE_ID="$LOCAL_BUNDLE_ID" + BUILD_APP_NAME="$LOCAL_APP_NAME" INSTALLER_IDENTITY="" PROFILE_CLASS="development" ;; app-store) CONFIG="release" BUILD_BUNDLE_ID="$PRODUCTION_BUNDLE_ID" + BUILD_APP_NAME="$PRODUCTION_APP_NAME" PROFILE_CLASS="app-store" ;; esac diff --git a/scripts/dev.sh b/scripts/dev.sh index 98f8aa7e..06f6b96d 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # The dev lane: rebuild whatever is stale, package the debug bundle, -# and launch it from dist/. The debug build takes its own bundle id +# and launch it from dist/ as "OnetimePad Debug.app". The debug build +# takes its own bundle id # (dev.onetimesecret.pad.debug), a "Dev" display name and the black app icon # (ADR-0012), so it runs # beside the installed copy without contending for the menu bar, @@ -35,6 +36,8 @@ set -euo pipefail cd "$(dirname "$0")/.." +source scripts/build-lanes.sh + NO_LAUNCH=0 # The dev lane defaults to allowing screen capture so scripted runs and # hand checks can screenshot the surface without a flag. The header @@ -85,7 +88,7 @@ running_from() { # pgrep -f "$(printf '%s' "$1" | sed 's/[][\.|$(){}?+*^]/\\&/g')" >/dev/null } -DIST_APP="$PWD/dist/OnetimePad.app" +DIST_APP="$PWD/dist/$DEV_APP_NAME.app" if running_from "$DIST_APP/Contents/MacOS"; then echo "==> Asking the copy running from dist/ to quit" quit_err="" diff --git a/scripts/install.sh b/scripts/install.sh index 5512a876..88975b4b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # The local lane: build the release bundle as dev.onetimesecret.pad, sign -# it, and install it to APP_DEST (default /Applications). The installed copy +# it, and install it to APP_DEST (default /Applications) as +# "OnetimePad Local.app", beside a TestFlight OnetimePad.app rather than +# over it. The installed copy # runs from /Applications rather than from .build/ or dist/, so # rebuilds in the repo never kill it. See # environments/example/.env.example for pinning a signing identity @@ -131,25 +133,34 @@ install_bundle() { # # OnetimePad in Launchpad and Spotlight, over state the new id cannot # read. It goes only after it has quit: quit_installed exits rather # than return when the app will not go, and we never remove a live app. -migrate_legacy_bundle() { # - local name="$1" +# +# The local lane later installed as OnetimePad.app, which is also the +# name a TestFlight install takes. That bundle goes only when it carries +# this lane's id, so a TestFlight copy at the same path is left alone. +migrate_legacy_bundle() { # [required bundle id] + local name="$1" id="${2:-}" local legacy="$APP_DEST/$name.app" [[ -d "$legacy" ]] || return 0 + if [[ -n "$id" ]] && + [[ "$(plutil -extract CFBundleIdentifier raw "$legacy/Contents/Info.plist" 2>/dev/null)" != "$id" ]]; then + return 0 + fi quit_installed "$name" - echo "==> Removing legacy $legacy (this app is now OnetimePad.app)" + echo "==> Removing legacy $legacy (this app is now $BUILD_APP_NAME.app)" rm -rf "$legacy" } -quit_installed OnetimePad -install_bundle OnetimePad +quit_installed "$BUILD_APP_NAME" +install_bundle "$BUILD_APP_NAME" migrate_legacy_bundle CompanionBackdrop +migrate_legacy_bundle "$PRODUCTION_APP_NAME" "$BUILD_BUNDLE_ID" if [[ "$NO_LAUNCH" == 0 ]]; then if ((ALLOW_CAPTURE)); then echo "==> Launching installed app (screen capture allowed)" - open --env COMPANION_ALLOW_CAPTURE=1 "$APP_DEST/OnetimePad.app" + open --env COMPANION_ALLOW_CAPTURE=1 "$APP_DEST/$BUILD_APP_NAME.app" else echo "==> Launching installed app" - open "$APP_DEST/OnetimePad.app" + open "$APP_DEST/$BUILD_APP_NAME.app" fi fi diff --git a/scripts/package-app.sh b/scripts/package-app.sh index 3504d69c..30326360 100755 --- a/scripts/package-app.sh +++ b/scripts/package-app.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash -# Package the app as a real .app bundle: dist/OnetimePad.app. +# Package the app as a real .app bundle under dist/, named per lane +# (scripts/build-lanes.sh): "OnetimePad Debug.app" for --debug, +# "OnetimePad Local.app" for the local lane, and OnetimePad.app only for +# --app-store, so no development bundle takes the TestFlight copy's name. # A bare `swift run` binary has no CFBundleIdentifier, so macOS cannot # address it: TCC grants don't stick, per-app pickers cannot list it, # and LaunchServices registers it as a nameless process. The bundle is @@ -21,8 +24,8 @@ # COMPANION_ALLOW_CAPTURE set, which also seeds it on. `open` does not # forward the caller's environment; pass the variable explicitly, and to # either configuration: -# open --env COMPANION_ALLOW_CAPTURE=1 dist/OnetimePad.app -# open --env COMPANION_ALLOW_CAPTURE=1 /Applications/OnetimePad.app +# open --env COMPANION_ALLOW_CAPTURE=1 "dist/OnetimePad Debug.app" +# open --env COMPANION_ALLOW_CAPTURE=1 "/Applications/OnetimePad Local.app" # Both entry points take --allow-capture, which is the same launch # without the incantation: scripts/dev.sh --allow-capture and # scripts/install.sh --allow-capture. @@ -43,8 +46,9 @@ # the documented login keychain fallback. # # Before signing, the assembled bundle is hashed into -# dist/OnetimePad.presig.sha256, the reproducible pre-signature -# artifact ADR-0012 publishes. +# dist/.presig.sha256 (dist/OnetimePad.presig.sha256 for +# --app-store), the reproducible pre-signature artifact ADR-0012 +# publishes. set -euo pipefail cd "$(dirname "$0")/.." @@ -315,7 +319,7 @@ if [[ "$CONFIG" == "release" ]]; then fi fi -APP=dist/OnetimePad.app +APP="dist/$BUILD_APP_NAME.app" echo "==> Assembling $APP (App $VERSION, FFI $FFI_VERSION, Core $CORE_VERSION, $CONFIG)" rm -rf "$APP" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" @@ -638,5 +642,5 @@ if ((APP_STORE_MODE)); then fi echo "Built $APP and $PKG (App Store build $APP_STORE_BUILD_NUMBER)." else - echo "Built $APP. Launch with: open $APP" + echo "Built $APP. Launch with: open \"$APP\"" fi diff --git a/scripts/tests/test_build_lanes.py b/scripts/tests/test_build_lanes.py index 0aa3794c..077549dc 100644 --- a/scripts/tests/test_build_lanes.py +++ b/scripts/tests/test_build_lanes.py @@ -15,8 +15,8 @@ ) PREFIXES = ("DEV_", "LOCAL_", "APP_STORE_") SELECTION = ( - 'printf "%s|%s|%s|%s|%s|%s\\n" "$CONFIG" "$BUILD_BUNDLE_ID" ' - '"$CODESIGN_IDENTITY" "$INSTALLER_IDENTITY" ' + 'printf "%s|%s|%s|%s|%s|%s|%s\\n" "$CONFIG" "$BUILD_BUNDLE_ID" ' + '"$BUILD_APP_NAME" "$CODESIGN_IDENTITY" "$INSTALLER_IDENTITY" ' '"$PROVISIONING_PROFILE" "$PROFILE_CLASS"' ) @@ -73,9 +73,9 @@ def test_each_lane_selects_its_metadata_and_its_own_signing_values(self): # App Store lane keeps one. self.write_every_environment() expected = { - "dev": "debug|dev.onetimesecret.pad.debug|dev-file||dev-file|development", - "local": "release|dev.onetimesecret.pad|local-file||local-file|development", - "app-store": "release|com.onetimesecret.pad|staging-file|staging-file|staging-file|app-store", + "dev": "debug|dev.onetimesecret.pad.debug|OnetimePad Debug|dev-file||dev-file|development", + "local": "release|dev.onetimesecret.pad|OnetimePad Local|local-file||local-file|development", + "app-store": "release|com.onetimesecret.pad|OnetimePad|staging-file|staging-file|staging-file|app-store", } for lane, values in expected.items(): with self.subTest(lane=lane):