From 94c36223c95bf4de9a420efc3846f2b0a94a16d4 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 29 Jul 2026 00:21:08 +0900 Subject: [PATCH] fix(release): smoke test a copy so the shipped bundle stays sealed v0.2.1 notarized as Invalid on both arches with a single issue: path: OpenHWP.app/Contents/MacOS/laufey message: The signature of the binary is invalid. The signature was fine. On a successful start the app writes a 2-byte marker, Contents/MacOS/laufey.dylib.update-ok, into its own bundle. That adds a file the signature does not seal: codesign --verify --deep --strict a sealed resource is missing or invalid file added: .../Contents/MacOS/laufey.dylib.update-ok so the smoke test added in #20 broke the very artifact it was verifying, between signing and packaging. v0.2.0 notarized only because it never launched the app. Launch a byte-identical copy in $RUNNER_TEMP instead. The copy absorbs the marker and the shipped bundle is never executed. Reproduced and verified locally: the copy gets the marker, the shipped bundle does not and still reports "valid on disk / satisfies its Designated Requirement", and the smoke test still logs "Listening on". Also re-verify the bundle immediately before hdiutil. Any future mutation under Contents/ now fails in seconds with the offending path named, instead of coming back as an opaque notarization rejection ten minutes later. Confirmed to exit 1 with the marker present and 0 without it. --- .github/workflows/release.yml | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60a94d0..d4308f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,11 @@ name: Release # below rather than committed, so a contributor without the certificate can # still build. # -# The app is launched before release. Notarization only proves Gatekeeper will -# permit the app to run; v0.2.0 shipped notarized and crashed on launch because -# the Hardened Runtime denied V8 its JIT pages. +# A copy of the app is launched before release. Notarization only proves +# Gatekeeper will permit the app to run; v0.2.0 shipped notarized and crashed on +# launch because the Hardened Runtime denied V8 its JIT pages. The copy matters: +# the app writes a marker file into its own bundle on startup, which would break +# the signature seal of the bundle being shipped. on: release: @@ -237,11 +239,21 @@ jobs: # The executable name comes from the bundle, not a hardcoded guess. EXE="$(plutil -extract CFBundleExecutable raw "$APP/Contents/Info.plist")" + # Test a copy, never the bundle we ship. On a successful start the app + # writes Contents/MacOS/laufey.dylib.update-ok into its own bundle, + # which adds an unsealed file and breaks the signature — the notary + # service reports that, ten minutes later, as "the signature of the + # binary is invalid" on the main executable. The copy is byte-identical, + # so launching it proves the same bits run. + SMOKE="$RUNNER_TEMP/smoke-app" + rm -rf "$SMOKE" && mkdir -p "$SMOKE" + cp -R "$APP" "$SMOKE/OpenHWP.app" + # Notarization proves Gatekeeper will *permit* the app to run. It says # nothing about whether it does. Launch it: a Hardened Runtime that # denies V8 its JIT pages kills the runtime thread within seconds. LOG="$RUNNER_TEMP/smoke.log" - "$APP/Contents/MacOS/$EXE" > "$LOG" 2>&1 & + "$SMOKE/OpenHWP.app/Contents/MacOS/$EXE" > "$LOG" 2>&1 & PID=$! for _ in $(seq 1 15); do kill -0 "$PID" 2>/dev/null || break @@ -278,6 +290,13 @@ jobs: APP="dist/${{ matrix.arch }}/OpenHWP.app" DMG="dist/OpenHWP-${{ matrix.arch }}.dmg" + # The bundle must still be exactly what was signed. Anything that adds + # a file under Contents/ — a launch, a stray .DS_Store — breaks the + # seal, and the notary service only reports it as an opaque "signature + # of the binary is invalid" after a ten-minute round trip. Seconds here + # instead. + codesign --verify --deep --strict --verbose=2 "$APP" + # The .app inside the .dmg is now stale, so rebuild the container from # the re-signed bundle rather than shipping the original. STAGE="$RUNNER_TEMP/dmg-stage"