From 29fba826bad9b24c1da8bee95dd6b3b5f976161e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Sun, 17 May 2026 05:00:56 +0100 Subject: [PATCH 1/2] ci(mac-release): release-contract asserts scheme + bundle-id + SUPublicEDKey The "Verify release contract" step previously only checked that the Xcode project + Info.plist + Sparkle script paths exist. A rename inside the owletto submodule (scheme Lobu->Owletto, bundle id ai.lobu.mac->ai.owletto.mac, Sparkle key removal) would pass those checks and fail late in xcodebuild, or worse, silently break auto-update for installed users. Add four assertions in the same shell block: - xcodebuild -list must expose a scheme named exactly 'Lobu' (the workflow uses `-scheme Lobu` everywhere). - Info.plist CFBundleName and CFBundleDisplayName must be 'Owletto'. - project.pbxproj must still set PRODUCT_BUNDLE_IDENTIFIER = ai.lobu.mac (CFBundleIdentifier in Info.plist resolves to that var at build time, so the real id lives in the pbxproj build settings). - Info.plist must contain SUPublicEDKey so installed apps can verify Sparkle update signatures. Backward-compatible: only adds checks. Closes #794. --- .github/workflows/mac-release.yml | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/.github/workflows/mac-release.yml b/.github/workflows/mac-release.yml index c0f383685..85a0eef32 100644 --- a/.github/workflows/mac-release.yml +++ b/.github/workflows/mac-release.yml @@ -72,6 +72,12 @@ jobs: # assumes must actually exist at the pinned SHA. Otherwise the # `lobster-mark.svg` → `owletto-mark.svg` style of silent rename on # the owletto side would only surface inside `xcodebuild`. + # 4-7. The scheme name, app display name, bundle identifier, and + # Sparkle public key must match what the rest of the workflow and + # installed apps depend on. A rename inside owletto (e.g. scheme + # `Lobu` → `Owletto`, bundle id `ai.lobu.mac` → `ai.owletto.mac`) + # would pass the path checks above but break the build or, worse, + # silently break auto-update for installed users. - name: Verify release contract (submodule + Mac source layout) shell: bash run: | @@ -99,6 +105,52 @@ jobs: fi done + # 4. The Xcode scheme must still be named 'Lobu'. The rest of this + # workflow passes `-scheme Lobu` to xcodebuild; if owletto renames + # the scheme (e.g. to 'Owletto') the path checks above still pass + # and we'd only find out late inside xcodebuild. + if ! xcodebuild -project packages/web/apps/mac/Lobu.xcodeproj -list 2>/dev/null \ + | grep -qE '^[[:space:]]+Lobu[[:space:]]*$'; then + echo "::error::Expected scheme 'Lobu' missing from Lobu.xcodeproj" + exit 1 + fi + + # 5. CFBundleName + CFBundleDisplayName must stay 'Owletto'. These + # are what users see in Finder / the menu bar / "About"; a silent + # rename here would ship a DMG that installs as a different app + # next to existing installs. + EXPECTED_BUNDLE_NAME="Owletto" + for key in CFBundleName CFBundleDisplayName; do + actual=$(/usr/libexec/PlistBuddy -c "Print :$key" \ + packages/web/apps/mac/Lobu/Info.plist 2>/dev/null || true) + if [ "$actual" != "$EXPECTED_BUNDLE_NAME" ]; then + echo "::error::Info.plist:$key is '$actual', expected '$EXPECTED_BUNDLE_NAME'" + exit 1 + fi + done + + # 6. PRODUCT_BUNDLE_IDENTIFIER in project.pbxproj must stay + # ai.lobu.mac. CFBundleIdentifier in Info.plist resolves to + # $(PRODUCT_BUNDLE_IDENTIFIER) at build time, so the real value + # lives in the pbxproj build settings. The URL scheme, Keychain + # service name, and Sparkle update path are all keyed off this — + # a change here breaks the upgrade contract for installed apps. + EXPECTED_BUNDLE_ID="ai.lobu.mac" + if ! grep -q "PRODUCT_BUNDLE_IDENTIFIER = ${EXPECTED_BUNDLE_ID};" \ + packages/web/apps/mac/Lobu.xcodeproj/project.pbxproj; then + echo "::error::PRODUCT_BUNDLE_IDENTIFIER in project.pbxproj is not '${EXPECTED_BUNDLE_ID}'. Bundle id drift breaks installed-app upgrades." + exit 1 + fi + + # 7. Sparkle public key must still be in Info.plist. Installed apps + # verify update signatures against SUPublicEDKey; if the key is + # removed or rotated server-side without matching a key the + # installed app trusts, auto-update breaks silently. + if ! grep -q "SUPublicEDKey" packages/web/apps/mac/Lobu/Info.plist; then + echo "::error::SUPublicEDKey missing from Info.plist — installed apps cannot verify updates" + exit 1 + fi + - name: Resolve version id: v run: echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT" From 393f31a750aefc368818e63ae4c25f527501faef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Sun, 17 May 2026 05:09:06 +0100 Subject: [PATCH 2/2] ci(mac-release): tighten release-contract assertions (pi follow-up) Pi review on PR #807 flagged three checks that could pass while the release contract was actually broken: 1. Scheme check matched against `xcodebuild -list` plain output, which contains both a `Targets:` and a `Schemes:` section. A target named 'Lobu' would satisfy the grep even if the scheme had been renamed to 'Owletto'. Switch to `-list -json` and parse the schemes array, so only an actual scheme named 'Lobu' counts. 2. PRODUCT_BUNDLE_IDENTIFIER grep proved "some occurrence equals ai.lobu.mac". If one build config drifted while another stale occurrence stayed correct, the grep would still pass. Assert the distinct set of values across project.pbxproj equals exactly {ai.lobu.mac}. 3. SUPublicEDKey grep could match the literal string inside an XML comment. Replace with PlistBuddy `Print :SUPublicEDKey`, which only succeeds when it's an actual plist key. Sanity-checked all three against the currently pinned submodule SHA (b01682708) locally: scheme list = "Lobu", distinct bundle ids = "ai.lobu.mac", PlistBuddy returns the key. No behavior change beyond catching cases the old checks would silently miss. --- .github/workflows/mac-release.yml | 43 ++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/.github/workflows/mac-release.yml b/.github/workflows/mac-release.yml index 85a0eef32..2b9e1be3a 100644 --- a/.github/workflows/mac-release.yml +++ b/.github/workflows/mac-release.yml @@ -109,9 +109,15 @@ jobs: # workflow passes `-scheme Lobu` to xcodebuild; if owletto renames # the scheme (e.g. to 'Owletto') the path checks above still pass # and we'd only find out late inside xcodebuild. - if ! xcodebuild -project packages/web/apps/mac/Lobu.xcodeproj -list 2>/dev/null \ - | grep -qE '^[[:space:]]+Lobu[[:space:]]*$'; then - echo "::error::Expected scheme 'Lobu' missing from Lobu.xcodeproj" + # + # Scope the check to the schemes list via `-list -json`. The plain + # `-list` text output has both `Targets:` and `Schemes:` sections, + # so a `grep '^ Lobu$'` could match a target named 'Lobu' even + # after the scheme was renamed to 'Owletto'. + SCHEMES=$(xcodebuild -project packages/web/apps/mac/Lobu.xcodeproj -list -json 2>/dev/null \ + | python3 -c "import json,sys; print('\n'.join(json.load(sys.stdin)['project']['schemes']))") + if ! echo "$SCHEMES" | grep -qE '^Lobu$'; then + echo "::error::Expected scheme 'Lobu' missing from project schemes (got: $SCHEMES)" exit 1 fi @@ -130,15 +136,24 @@ jobs: done # 6. PRODUCT_BUNDLE_IDENTIFIER in project.pbxproj must stay - # ai.lobu.mac. CFBundleIdentifier in Info.plist resolves to - # $(PRODUCT_BUNDLE_IDENTIFIER) at build time, so the real value - # lives in the pbxproj build settings. The URL scheme, Keychain - # service name, and Sparkle update path are all keyed off this — - # a change here breaks the upgrade contract for installed apps. + # ai.lobu.mac across ALL build configurations. CFBundleIdentifier + # in Info.plist resolves to $(PRODUCT_BUNDLE_IDENTIFIER) at build + # time, so the real value lives in the pbxproj build settings. + # The URL scheme, Keychain service name, and Sparkle update path + # are all keyed off this — a change here breaks the upgrade + # contract for installed apps. + # + # Assert the set of distinct values is exactly {ai.lobu.mac}, not + # "some occurrence equals ai.lobu.mac". A simple grep would still + # pass if one config drifted while another stale occurrence stayed + # correct. EXPECTED_BUNDLE_ID="ai.lobu.mac" - if ! grep -q "PRODUCT_BUNDLE_IDENTIFIER = ${EXPECTED_BUNDLE_ID};" \ - packages/web/apps/mac/Lobu.xcodeproj/project.pbxproj; then - echo "::error::PRODUCT_BUNDLE_IDENTIFIER in project.pbxproj is not '${EXPECTED_BUNDLE_ID}'. Bundle id drift breaks installed-app upgrades." + distinct_ids=$(grep -E 'PRODUCT_BUNDLE_IDENTIFIER = ' \ + packages/web/apps/mac/Lobu.xcodeproj/project.pbxproj \ + | sed -E 's/.*PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);.*/\1/' \ + | sort -u) + if [ "$distinct_ids" != "$EXPECTED_BUNDLE_ID" ]; then + echo "::error::PRODUCT_BUNDLE_IDENTIFIER must be exactly '${EXPECTED_BUNDLE_ID}' across all configs (got: $distinct_ids). Bundle id drift breaks installed-app upgrades." exit 1 fi @@ -146,7 +161,11 @@ jobs: # verify update signatures against SUPublicEDKey; if the key is # removed or rotated server-side without matching a key the # installed app trusts, auto-update breaks silently. - if ! grep -q "SUPublicEDKey" packages/web/apps/mac/Lobu/Info.plist; then + # + # Use PlistBuddy so the check matches an actual plist key, not + # text inside an XML comment. + if ! /usr/libexec/PlistBuddy -c "Print :SUPublicEDKey" \ + packages/web/apps/mac/Lobu/Info.plist >/dev/null 2>&1; then echo "::error::SUPublicEDKey missing from Info.plist — installed apps cannot verify updates" exit 1 fi