From 3d1c5548bc07e1d1662ed1db2c20ec62d45c48e4 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:04:38 -0400 Subject: [PATCH] promotion-gate: use plistlib for CFBundleShortVersionString (handles binary plist) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second false-negative from promotion-gate.yml: first run 127'd on missing 7zz (#149 fixed that), second run extracted the DMG fine but reported empty version because: - Info.plist in a real DMG is a BINARY plist, not XML - plistutil (libplist-utils) was supposed to convert it to XML but silently failed (its '2>/dev/null' swallowed the error) - The fallback 'cp' put the binary blob at /tmp/plist.xml - grep matched (binary file matches) but sed on binary yielded '' - Version comparison: '' != '150.0.6' → gate quarantined a good release Use Python's plistlib instead. Handles binary AND XML formats natively, zero extra deps (Python 3 is on every ubuntu-latest runner). Meta-lesson (again): a two-step tool chain where the first step's failure is silenced becomes a two-step tool chain where you cannot tell which step failed. Both '2>/dev/null' fallbacks in this gate have now bitten us. Removing libplist-utils entirely closes the class. --- .github/workflows/promotion-gate.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/promotion-gate.yml b/.github/workflows/promotion-gate.yml index 6717441..89b4f84 100644 --- a/.github/workflows/promotion-gate.yml +++ b/.github/workflows/promotion-gate.yml @@ -79,24 +79,32 @@ jobs: DMG=$(ls release-assets/*macos*.dmg 2>/dev/null | head -1) [ -f "$DMG" ] || { echo "no macOS DMG in release; skipping plist gate"; exit 0; } sudo apt-get update -qq - sudo apt-get install -y -qq p7zip-full libplist-utils + sudo apt-get install -y -qq p7zip-full # Pick whichever 7-Zip CLI got installed. SEVENZ="" for cand in 7z 7zz 7za; do if command -v "$cand" >/dev/null 2>&1; then SEVENZ="$cand"; break; fi done - [ -n "$SEVENZ" ] || { echo "::error::no 7z-family binary found after installing p7zip-full"; exit 1; } + [ -n "$SEVENZ" ] || { echo "::error::no 7z-family binary found"; exit 1; } echo "using 7z CLI: $SEVENZ" mkdir -p release-assets/dmg-extract "$SEVENZ" x -o./release-assets/dmg-extract -y "$DMG" > /dev/null PLIST=$(find release-assets/dmg-extract -name Info.plist -path "*BearBrowser.app/Contents/*" | head -1) [ -f "$PLIST" ] || { echo "::error::no Info.plist in DMG"; find release-assets/dmg-extract -maxdepth 4 -type d | head -20; exit 1; } - # Convert to XML if binary (plistutil handles both formats). - plistutil -i "$PLIST" -o /tmp/plist.xml 2>/dev/null || cp "$PLIST" /tmp/plist.xml - VERSION=$(grep -A1 CFBundleShortVersionString /tmp/plist.xml | tail -1 | sed -E 's,.*(.*).*,\1,') + # Read the plist with Python's plistlib — handles BOTH binary and XML + # formats, always available. Prior attempt piped grep|sed against + # plistutil output; plistutil silently failed (its `2>/dev/null` + # swallowed the error) and grep matched the binary blob but sed + # extracted nothing. Second promotion-gate false-negative in a row. + VERSION=$(python3 -c " + import plistlib, sys + with open(sys.argv[1], 'rb') as f: d = plistlib.load(f) + print(d.get('CFBundleShortVersionString','')) + " "$PLIST") EXPECTED="${TAG#v}" - echo "plist CFBundleShortVersionString: $VERSION" - echo "release tag (without leading v): $EXPECTED" + echo "plist CFBundleShortVersionString: '$VERSION'" + echo "release tag (without leading v): '$EXPECTED'" + [ -n "$VERSION" ] || { echo "::error::plistlib returned empty version"; exit 1; } [ "$VERSION" = "$EXPECTED" ] || { echo "::error::plist version $VERSION != tag $EXPECTED"; exit 1; } - name: Update-check network hygiene smoke