Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/review-desktop-update-smoke.yml
Original file line number Diff line number Diff line change
@@ -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
88 changes: 88 additions & 0 deletions apps/review-desktop/scripts/update-smoke.sh
Original file line number Diff line number Diff line change
@@ -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 <stable|preview> <from-version> <Review|Whiteboard>
#
# 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"
Loading