From e29a154dac1886fc65ca0ebdbf4bdece31f68752 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Sat, 8 Aug 2026 16:59:48 +0300 Subject: [PATCH] fix(ci): ship the DMG's signed staging bundle as the Sparkle enclosure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related #957 The enclosure steps zipped SWIFT_APP_PATH — the raw swift-build output, which has no core binary, no certs, and not the final Developer ID deep signature (create-dmg.sh stages and signs a separate copy for the DMG). Notarization of that zip failed ('Invalid') on the first RC, and a Sparkle update built from it would have installed an app without a core. create-dmg.sh now exports its staged, signed bundle to signed-app-/mcpproxy.app after signing, and both workflows' enclosure steps consume that export (hard-failing if absent). Caught by the Stage 1 RC dress rehearsal (v0.54.0-rc.1), exactly as the rollout plan intended. --- .github/workflows/prerelease.yml | 23 +++++++++++++++-------- .github/workflows/release.yml | 25 ++++++++++++++++--------- scripts/create-dmg.sh | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 16ceaea22..f3a3f422a 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -727,8 +727,15 @@ jobs: exit 0 fi - if [ -z "${SWIFT_APP_PATH:-}" ] || [ ! -d "${SWIFT_APP_PATH}" ]; then - echo "❌ SWIFT_APP_PATH is not a bundle: '${SWIFT_APP_PATH:-}'" + # The enclosure must ship the SAME bundle users run: the DMG staging + # copy that create-dmg.sh signed (core + certs + Developer ID deep + # signature) and exported. SWIFT_APP_PATH is the raw swift-build + # output — no core, not notarization-signed — and must never be + # zipped as an update (it failed notarization and would install a + # coreless app). + APP_SRC="signed-app-${{ matrix.goarch }}/mcpproxy.app" + if [ ! -d "${APP_SRC}" ]; then + echo "❌ signed bundle export missing: ${APP_SRC} (create-dmg.sh exports it)" exit 1 fi @@ -736,7 +743,7 @@ jobs: ENCLOSURE="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.app.zip" NOTARIZE_ZIP="sparkle-notarize-${{ matrix.goarch }}.zip" - ditto -c -k --sequesterRsrc --keepParent "${SWIFT_APP_PATH}" "${NOTARIZE_ZIP}" + ditto -c -k --sequesterRsrc --keepParent "${APP_SRC}" "${NOTARIZE_ZIP}" SUBMISSION_OUTPUT=$(xcrun notarytool submit "${NOTARIZE_ZIP}" \ --apple-id "${{ secrets.APPLE_ID_USERNAME }}" \ @@ -751,16 +758,16 @@ jobs: exit 1 fi - xcrun stapler staple "${SWIFT_APP_PATH}" - xcrun stapler validate "${SWIFT_APP_PATH}" + xcrun stapler staple "${APP_SRC}" + xcrun stapler validate "${APP_SRC}" - ditto -c -k --sequesterRsrc --keepParent "${SWIFT_APP_PATH}" "sparkle-enclosure/${ENCLOSURE}" + ditto -c -k --sequesterRsrc --keepParent "${APP_SRC}" "sparkle-enclosure/${ENCLOSURE}" rm -f "${NOTARIZE_ZIP}" VERIFY_DIR=$(mktemp -d) ditto -x -k "sparkle-enclosure/${ENCLOSURE}" "${VERIFY_DIR}" - codesign --verify --deep --strict --verbose=2 "${VERIFY_DIR}/MCPProxy.app" - xcrun stapler validate "${VERIFY_DIR}/MCPProxy.app" + codesign --verify --deep --strict --verbose=2 "${VERIFY_DIR}/mcpproxy.app" + xcrun stapler validate "${VERIFY_DIR}/mcpproxy.app" rm -rf "${VERIFY_DIR}" echo "✅ Sparkle enclosure ready: ${ENCLOSURE}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cc8fed5f..18b70e21a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1028,8 +1028,15 @@ jobs: exit 0 fi - if [ -z "${SWIFT_APP_PATH:-}" ] || [ ! -d "${SWIFT_APP_PATH}" ]; then - echo "❌ SWIFT_APP_PATH is not a bundle: '${SWIFT_APP_PATH:-}'" + # The enclosure must ship the SAME bundle users run: the DMG staging + # copy that create-dmg.sh signed (core + certs + Developer ID deep + # signature) and exported. SWIFT_APP_PATH is the raw swift-build + # output — no core, not notarization-signed — and must never be + # zipped as an update (it failed notarization and would install a + # coreless app). + APP_SRC="signed-app-${{ matrix.goarch }}/mcpproxy.app" + if [ ! -d "${APP_SRC}" ]; then + echo "❌ signed bundle export missing: ${APP_SRC} (create-dmg.sh exports it)" exit 1 fi @@ -1037,8 +1044,8 @@ jobs: ENCLOSURE="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.app.zip" NOTARIZE_ZIP="sparkle-notarize-${{ matrix.goarch }}.zip" - echo "=== Archiving ${SWIFT_APP_PATH} for notarization ===" - ditto -c -k --sequesterRsrc --keepParent "${SWIFT_APP_PATH}" "${NOTARIZE_ZIP}" + echo "=== Archiving ${APP_SRC} for notarization ===" + ditto -c -k --sequesterRsrc --keepParent "${APP_SRC}" "${NOTARIZE_ZIP}" echo "=== Submitting the app bundle for notarization ===" SUBMISSION_OUTPUT=$(xcrun notarytool submit "${NOTARIZE_ZIP}" \ @@ -1057,11 +1064,11 @@ jobs: # Staple the BUNDLE, then re-archive it: the ticket must travel inside # the .app the user ends up running. - xcrun stapler staple "${SWIFT_APP_PATH}" - xcrun stapler validate "${SWIFT_APP_PATH}" + xcrun stapler staple "${APP_SRC}" + xcrun stapler validate "${APP_SRC}" echo "=== Creating the stapled enclosure ===" - ditto -c -k --sequesterRsrc --keepParent "${SWIFT_APP_PATH}" "sparkle-enclosure/${ENCLOSURE}" + ditto -c -k --sequesterRsrc --keepParent "${APP_SRC}" "sparkle-enclosure/${ENCLOSURE}" rm -f "${NOTARIZE_ZIP}" # Prove the archive round-trips with its signature intact before it @@ -1069,8 +1076,8 @@ jobs: # the user's machine otherwise. VERIFY_DIR=$(mktemp -d) ditto -x -k "sparkle-enclosure/${ENCLOSURE}" "${VERIFY_DIR}" - codesign --verify --deep --strict --verbose=2 "${VERIFY_DIR}/MCPProxy.app" - xcrun stapler validate "${VERIFY_DIR}/MCPProxy.app" + codesign --verify --deep --strict --verbose=2 "${VERIFY_DIR}/mcpproxy.app" + xcrun stapler validate "${VERIFY_DIR}/mcpproxy.app" spctl -a -t exec -vv "${VERIFY_DIR}/MCPProxy.app" rm -rf "${VERIFY_DIR}" diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh index 761804252..86c98b871 100755 --- a/scripts/create-dmg.sh +++ b/scripts/create-dmg.sh @@ -247,6 +247,20 @@ else codesign --force --deep --sign - --identifier "$BUNDLE_ID" "$TEMP_DIR/$APP_BUNDLE" fi +# Export the fully staged, fully signed bundle for the Sparkle enclosure +# (Spec 092). SWIFT_APP_PATH is only the raw swift-build output: it has no +# core binary, no certs, and not this final Developer ID signature — zipping +# it produced an enclosure that failed notarization AND would have installed +# an app without a core. The DMG staging copy here is the ONLY bundle in the +# pipeline that is byte-for-byte what users run, so it is what updates must +# ship. Symlink-free ditto copy preserves the signature. +SIGNED_APP_EXPORT="signed-app-${ARCH}" +rm -rf "${SIGNED_APP_EXPORT}" +mkdir -p "${SIGNED_APP_EXPORT}" +ditto "$TEMP_DIR/$APP_BUNDLE" "${SIGNED_APP_EXPORT}/${APP_BUNDLE}" +codesign --verify --deep --strict "${SIGNED_APP_EXPORT}/${APP_BUNDLE}" +echo "✅ Signed bundle exported for Sparkle enclosure: ${SIGNED_APP_EXPORT}/${APP_BUNDLE}" + # Create Applications symlink ln -s /Applications "$TEMP_DIR/Applications"