diff --git a/.github/workflows/review-desktop-update-smoke.yml b/.github/workflows/review-desktop-update-smoke.yml new file mode 100644 index 000000000..cf0791e0f --- /dev/null +++ b/.github/workflows/review-desktop-update-smoke.yml @@ -0,0 +1,58 @@ +name: Review Desktop Update Smoke + +# Proves a published build updates in place on macOS: each job runs one +# starting build from a throwaway folder, lets Squirrel install the channel's +# current release, and checks the bundle kept its folder name. One job per +# update zip, so both a pre-rename (Review) and a renamed (Whiteboard) install +# are covered. Runners are separate machines, so their Squirrel state is too. + +on: + workflow_dispatch: + inputs: + quality: + description: "Channel whose current release the build should update to" + type: choice + options: + - preview + - stable + default: preview + from_version: + description: "Published version to start from, e.g. 0.0.34-preview.20260924.59 (must be older than the channel's current release)" + required: true + type: string + +permissions: + contents: read + +jobs: + update: + name: ${{ matrix.artifact }} zip → current ${{ inputs.quality }} + runs-on: macos-15 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + artifact: [Review, Whiteboard] + steps: + - name: Checkout + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + sparse-checkout: apps/review-desktop/scripts + - name: Update in place + run: | + chmod +x apps/review-desktop/scripts/update-smoke.sh + apps/review-desktop/scripts/update-smoke.sh "${{ inputs.quality }}" "${{ inputs.from_version }}" "${{ matrix.artifact }}" + - name: Keep the app and installer logs + if: always() + run: | + mkdir -p logs + cp "$RUNNER_TEMP"/update-smoke-*/app.stderr logs/ 2>/dev/null || true + cp "$HOME"/Library/Caches/dev.fast.review*.ShipIt/ShipIt_stderr.log logs/ 2>/dev/null || true + find "$RUNNER_TEMP"/update-smoke-*/state/user-data/logs -name main.log -exec cp {} logs/ \; 2>/dev/null || true + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: update-smoke-${{ inputs.quality }}-${{ matrix.artifact }} + path: logs + if-no-files-found: ignore + retention-days: 3 diff --git a/apps/review-desktop/scripts/update-smoke.sh b/apps/review-desktop/scripts/update-smoke.sh new file mode 100755 index 000000000..9065fb46c --- /dev/null +++ b/apps/review-desktop/scripts/update-smoke.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Prove a published build updates in place: run it from a throwaway folder, +# let Squirrel download and install the channel's current release, then check +# that the bundle folder kept its name and carries the new version. +# +# update-smoke.sh +# +# The third argument picks which update zip the starting build came from, so +# the folder is named like an install that predates or postdates the rename. +# Squirrel state is keyed by bundle id, so never run this beside a Desktop of +# the same channel. +set -euo pipefail + +QUALITY="$1"; FROM_VERSION="$2"; ARTIFACT="$3" +APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +WORK="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/update-smoke-$ARTIFACT" +UPDATE_URL="https://update.dev.fast" + +FOLDER="$(node "$APP_DIR/scripts/release-channel.mjs" "$QUALITY" | awk -F'\t' -v a="$ARTIFACT" '$2 == a { print $1 }')" +[[ -n "$FOLDER" ]] || { echo "no $ARTIFACT zip in the $QUALITY channel" >&2; exit 2; } +case "$QUALITY" in + stable) BUNDLE_ID="dev.fast.review" ;; + preview) BUNDLE_ID="dev.fast.review.preview" ;; +esac +SHIPIT="$HOME/Library/Caches/$BUNDLE_ID.ShipIt" +FEED="$UPDATE_URL/api/update/darwin-arm64/$QUALITY/0000000000000000000000000000000000000000" + +if pgrep -f "/Contents/MacOS/" | xargs -I{} ps -o command= -p {} 2>/dev/null | grep -q "$BUNDLE_ID\|$FOLDER.app"; then + echo "a $QUALITY Desktop is already running; Squirrel state is shared by bundle id" >&2 + exit 1 +fi + +rm -rf "$WORK"; mkdir -p "$WORK/app" "$WORK/state/user-data" "$WORK/state/extensions" "$WORK/home" +ZIP="$ARTIFACT-darwin-arm64-$FROM_VERSION.zip" +echo "Downloading $ZIP" +curl -fsSLo "$WORK/$ZIP" "$UPDATE_URL/releases/$FROM_VERSION/darwin-arm64/$ZIP" +ditto -x -k "$WORK/$ZIP" "$WORK/app" +APP="$WORK/app/$FOLDER.app" +[[ -d "$APP" ]] || { echo "$ZIP did not unpack to $FOLDER.app:"; ls "$WORK/app"; exit 1; } +EXECUTABLE="$(defaults read "$APP/Contents/Info.plist" CFBundleExecutable)" +BEFORE="$(node -p "require('$APP/Contents/Resources/app/product.json').reviewVersion")" + +EXPECTED="$(curl -fsS "$FEED?bundle=$(printf %s "$FOLDER" | sed 's/ /%20/g')" | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).productVersion")" +echo "Starting from $FOLDER.app ($BEFORE); the feed offers $EXPECTED" +[[ "$BEFORE" != "$EXPECTED" ]] || { echo "nothing to update to; publish a newer $QUALITY build first" >&2; exit 2; } + +mkdir -p "$SHIPIT"; touch "$SHIPIT/ShipIt_stderr.log" +LOG_START="$(wc -l < "$SHIPIT/ShipIt_stderr.log")" +TARGET_URL="file://$APP/" + +DEV_REVIEW_HOME="$WORK/home" DEV_REVIEW_IMPORT_FROM=none ELECTRON_ENABLE_LOGGING=1 \ + "$APP/Contents/MacOS/$EXECUTABLE" \ + "--user-data-dir=$WORK/state/user-data" "--extensions-dir=$WORK/state/extensions" \ + >"$WORK/app.stdout" 2>"$WORK/app.stderr" & +PID=$! + +# Squirrel writes its install request once the download is verified. The +# first check runs 30 s after launch; the zip is a few hundred megabytes. +for _ in $(seq 1 90); do + if plutil -p "$SHIPIT/ShipItState.plist" 2>/dev/null | grep -qF "$(printf %s "$TARGET_URL" | sed 's/ /%20/g')"; then + break + fi + kill -0 "$PID" 2>/dev/null || { echo "app exited early"; tail -20 "$WORK/app.stderr"; exit 1; } + sleep 5 +done +if ! plutil -p "$SHIPIT/ShipItState.plist" 2>/dev/null | grep -qF "$(printf %s "$TARGET_URL" | sed 's/ /%20/g')"; then + echo "Squirrel never staged an update for $APP" >&2 + grep -h -i "update" "$WORK"/state/user-data/logs/*/main.log 2>/dev/null | tail -30 >&2 + exit 1 +fi +echo "Squirrel staged:"; plutil -p "$SHIPIT/ShipItState.plist" | grep -E "BundleURL|useUpdateBundleName" | sed 's/^/ /' + +# Squirrel installs when the app exits, however it exits. +kill -TERM "$PID"; sleep 15; kill -KILL "$PID" 2>/dev/null || true; wait "$PID" 2>/dev/null || true +for _ in $(seq 1 60); do + tail -n +"$((LOG_START + 1))" "$SHIPIT/ShipIt_stderr.log" | grep -q -E "Installation completed|Installation error" && break + sleep 2 +done +echo "ShipIt:"; tail -n +"$((LOG_START + 1))" "$SHIPIT/ShipIt_stderr.log" | grep -E "Beginning|Installation|error" | sed 's/^/ /' + +BUNDLES="$(find "$WORK/app" -mindepth 1 -maxdepth 1)" +echo "On disk:"; echo "$BUNDLES" | sed 's/^/ /' +[[ -d "$APP" ]] || { echo "FAIL: $FOLDER.app is gone; the install renamed it" >&2; exit 1; } +[[ "$(echo "$BUNDLES" | wc -l)" -eq 1 ]] || { echo "FAIL: more than one bundle in the folder" >&2; exit 1; } +AFTER="$(node -p "require('$APP/Contents/Resources/app/product.json').reviewVersion")" +[[ "$AFTER" == "$EXPECTED" ]] || { echo "FAIL: $FOLDER.app is at $AFTER, expected $EXPECTED" >&2; exit 1; } +tail -n +"$((LOG_START + 1))" "$SHIPIT/ShipIt_stderr.log" | grep -q "Installation completed successfully" || { echo "FAIL: ShipIt did not report success" >&2; exit 1; } +echo "PASS: $FOLDER.app went from $BEFORE to $AFTER in place"