Skills › gstack › gstack-upgrade › v1.65.0.0.sh
v1.65.0.0.sh
152 lines · gstack-upgrade/migrations/v1.65.0.0.sh at 3f79d81
1#!/usr/bin/env bash
2# Migration: v1.65.0.0 — repair Chrome-for-Testing bundles poisoned by the
3# old in-place rebrand (#2242).
4#
5# Why a migration: pre-v1.64 launchHeaded() rewrote the Chromium .app's
6# Info.plist ("Google Chrome for Testing" → "GStack Browser") and overwrote
7# its Resources/*.icns — inside the SHARED Playwright cache. That broke the
8# codesign seal (GPU process exit_code=5; headed mode dead on macOS 26) and
9# poisoned the cache for the user's OTHER Playwright projects too. Deleting
10# the rebrand code fixes fresh installs only; every existing macOS install
11# still has the mutated bundle on disk. This migration removes poisoned
12# bundles and re-fetches a clean one so the upgrade doesn't leave the user
13# with zero working browser (the browse launch path also self-heals, as the
14# belt to this suspenders, for installs that never run migrations).
15#
16# Removal scope: `playwright install chromium` treats the revision dir's
17# INSTALLATION_COMPLETE marker as "is already downloaded" — removing only
18# the .app strands the user with a marker, no browser, and a re-fetch that
19# no-ops. So when the poisoned .app sits in the standard cache layout
20# (chromium-<rev>/chrome-mac/<name>.app) the WHOLE revision dir goes;
21# otherwise the .app plus its sibling INSTALLATION_COMPLETE /
22# DEPENDENCIES_VALIDATED markers go. A revision dir already stranded in
23# exactly that state (markers present, .app missing) is detected and
24# removed too, so the re-fetch actually downloads.
25#
26# Affected: macOS installs that ever ran headed mode before v1.64.
27#
28# Idempotent: detection is content-based (plist contains "GStack Browser");
29# a clean cache is a no-op, and the .done touchfile gates re-runs. After a
30# removal, .done is only written once the end state is VERIFIED (a real
31# Chromium executable exists in the cache) — a removal followed by a failed
32# re-fetch (e.g. offline) leaves the migration pending, with a needs-refetch
33# sentinel so the next run retries the download.
35set -u
37_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}"
38. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
39gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root"
40MIGRATION_DIR="${GSTACK_HOME}/.migrations"
41DONE="${MIGRATION_DIR}/v1.65.0.0.done"
42# Written when a removal happened but the verified end state (a Chromium
43# executable in the cache) wasn't reached — e.g. the re-fetch failed
44# offline. Its presence re-triggers the re-fetch on the next run even when
45# the scans below find nothing left to remove.
46NEEDS_REFETCH="${MIGRATION_DIR}/v1.65.0.0.needs-refetch"
47mkdir -p "${MIGRATION_DIR}" 2>/dev/null || true
48[ -f "${DONE}" ] && exit 0
50# macOS only: the mutation targeted .app bundle plists.
51if [ "$(uname -s 2>/dev/null)" != "Darwin" ]; then
52 touch "${DONE}"
53 exit 0
54fi
56PW_CACHE="${PLAYWRIGHT_BROWSERS_PATH:-${HOME}/Library/Caches/ms-playwright}"
57REMOVED=0
58[ -f "${NEEDS_REFETCH}" ] && REMOVED=1
60is_revision_dir() {
61 # Standard Playwright cache revision dir name: chromium-<digits>.
62 printf '%s' "$(basename "$1")" | grep -Eq '^chromium-[0-9]+$'
63}
65if [ -d "${PW_CACHE}" ]; then
66 # 1. Content-based poison scan: every Chrome-for-Testing bundle in the
67 # cache (one per pinned chromium build) whose plist carries the rebrand.
68 while IFS= read -r plist; do
69 if grep -q "GStack Browser" "${plist}" 2>/dev/null; then
70 app_dir="$(dirname "$(dirname "${plist}")")"
71 case "${app_dir}" in
72 "${PW_CACHE}"/*.app|"${PW_CACHE}"/*/*.app|"${PW_CACHE}"/*/*/*.app)
73 rev_dir="$(dirname "$(dirname "${app_dir}")")"
74 if is_revision_dir "${rev_dir}"; then
75 # Remove the WHOLE revision dir: Playwright's
76 # INSTALLATION_COMPLETE marker lives beside chrome-mac/, and
77 # `playwright install chromium` treats its presence as "already
78 # downloaded" — removing only the .app would make the re-fetch
79 # below a no-op and leave the user with NO browser.
80 echo " [v1.65.0.0] removing rebrand-poisoned revision dir (incl. install markers): ${rev_dir}" >&2
81 rm -rf "${rev_dir}"
82 else
83 echo " [v1.65.0.0] removing rebrand-poisoned bundle: ${app_dir}" >&2
84 rm -rf "${app_dir}"
85 rm -f "$(dirname "${app_dir}")/INSTALLATION_COMPLETE" \
86 "$(dirname "${app_dir}")/DEPENDENCIES_VALIDATED" 2>/dev/null || true
87 fi
88 REMOVED=1
89 ;;
90 *)
91 echo " [v1.65.0.0] WARNING: poisoned plist outside the Playwright cache shape, skipping: ${plist}" >&2
92 ;;
93 esac
94 fi
95 done < <(find "${PW_CACHE}" -maxdepth 5 -name "Info.plist" -path "*.app/Contents/Info.plist" 2>/dev/null)
97 # 2. Stranded-state scan: an earlier version of this migration removed
98 # only the poisoned .app, leaving the revision dir with its
99 # INSTALLATION_COMPLETE marker — the exact state that makes
100 # `playwright install chromium` no-op while the user has NO browser.
101 # A chromium revision dir without any .app inside is that strand;
102 # remove it whole so the re-fetch actually downloads.
103 for rev_dir in "${PW_CACHE}"/chromium-*; do
104 [ -d "${rev_dir}" ] || continue
105 is_revision_dir "${rev_dir}" || continue
106 if [ -z "$(find "${rev_dir}" -maxdepth 2 -name '*.app' -print 2>/dev/null | head -1)" ]; then
107 echo " [v1.65.0.0] removing stranded revision dir (install markers without a browser): ${rev_dir}" >&2
108 rm -rf "${rev_dir}"
109 REMOVED=1
110 fi
111 done
112fi
114if [ "${REMOVED}" = "1" ]; then
115 # Re-fetch immediately: migrations run AFTER ./setup, so without this the
116 # user finishes the upgrade with no working browser at all (headless AND
117 # headed use the same bundle). Run from the gstack install root so bunx
118 # resolves the repo-pinned playwright version — an arbitrary migration-
119 # runner cwd could resolve a different playwright and populate a revision
120 # the pinned one never launches (same subshell-cd pattern as ./setup's
121 # Chromium install block).
122 SCRIPT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
123 echo " [v1.65.0.0] re-fetching a clean Chromium (bunx playwright install chromium)..." >&2
124 if command -v bunx >/dev/null 2>&1 && (cd "${SCRIPT_DIR}" && bunx playwright install chromium >&2); then
125 echo " [v1.65.0.0] playwright install finished." >&2
126 else
127 echo " [v1.65.0.0] WARNING: automatic re-fetch failed." >&2
128 fi
130 # Gate .done on the VERIFIED end state, not the installer's exit code:
131 # `playwright install` exits 0 even when it skips the download, and a
132 # successful removal followed by a failed/offline fetch must not be
133 # recorded as done — that would strand the user with no browser and a
134 # success message.
135 CHROME_EXE="$(find "${PW_CACHE}" -maxdepth 6 -type f -perm -u+x -path "*/chromium-*/*.app/Contents/MacOS/*" 2>/dev/null | head -1)"
136 if [ -n "${CHROME_EXE}" ]; then
137 echo " [v1.65.0.0] verified working Chromium at: ${CHROME_EXE}" >&2
138 rm -f "${NEEDS_REFETCH}" 2>/dev/null || true
139 else
140 touch "${NEEDS_REFETCH}" 2>/dev/null || true
141 echo " [v1.65.0.0] WARNING: no Chromium executable present after removing the poisoned bundle." >&2
142 echo " [v1.65.0.0] Headless AND headed browsing are unavailable until it is re-fetched. Run:" >&2
143 echo " [v1.65.0.0] cd ${SCRIPT_DIR} && bunx playwright install chromium" >&2
144 echo " [v1.65.0.0] Leaving this migration pending — it retries on the next run." >&2
145 exit 0
146 fi
147else
148 echo " [v1.65.0.0] no rebrand-poisoned bundles found — no-op." >&2
149fi
151touch "${DONE}"
152exit 0