Skip to content

Commit 8f01baa

Browse files
authored
fix(release): repair macOS Chromium clone base (#2457)
1 parent 207e509 commit 8f01baa

4 files changed

Lines changed: 90 additions & 48 deletions

File tree

.github/scripts/macos-chromium-workspace.sh

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,31 +248,54 @@ check_no_browseros_outputs() {
248248
\( -name 'Default_browseros_*' -o -name 'Default_browserclaw_*' \) \
249249
-print -quit
250250
)"
251-
[ -z "$found" ] || die "Persistent Chromium base contains BrowserOS output state: $found; remove BrowserOS out dirs from the base before rerunning"
251+
[ -z "$found" ] || die "Could not remove BrowserOS output state from the CI-owned Chromium base: $found"
252252
}
253253

254254
check_git_repo_clean() {
255255
local repo="$1"
256256
local status
257257

258258
status="$(git -C "$repo" status --porcelain=v1 --untracked-files=all)"
259-
[ -z "$status" ] || die "Persistent Chromium base has tracked or untracked changes in $repo; reset the warm base checkout before rerunning"
259+
[ -z "$status" ] || die "Could not restore the CI-owned Chromium base repository to a clean state: $repo"
260260
}
261261

262-
check_nested_git_repos_clean() {
262+
repair_git_repo() {
263+
local repo="$1"
264+
local ref="$2"
265+
266+
git -C "$repo" reset --hard "$ref"
267+
git -C "$repo" clean -fd
268+
check_git_repo_clean "$repo"
269+
}
270+
271+
repair_nested_git_repos() {
263272
local git_meta repo
264273

265274
while IFS= read -r git_meta; do
266275
repo="$(resolve_existing_dir "$(dirname "$git_meta")")" || continue
267276
[ "$repo" != "$base_src" ] || continue
268-
check_git_repo_clean "$repo"
277+
repair_git_repo "$repo" HEAD
269278
done < <(
270279
find "$base_root" \
271280
\( -path "$base_src/out" -o -path "$base_src/out/*" \) -prune -o \
272281
-name .git -print -prune
273282
)
274283
}
275284

285+
remove_browseros_outputs() {
286+
local out_dir="$1/out"
287+
local output
288+
289+
[ -d "$out_dir" ] || return 0
290+
for output in \
291+
"$out_dir"/Default_browseros_* \
292+
"$out_dir"/Default_browserclaw_*; do
293+
[ -d "$output" ] || continue
294+
rm -rf "$output"
295+
done
296+
check_no_browseros_outputs "$1"
297+
}
298+
276299
verify_cow_clone_support() {
277300
local parent="$1"
278301
local probe_dir="$parent/.browseros-ci-apfs-probe-$(run_tag)-$$"
@@ -293,25 +316,29 @@ verify_cow_clone_support() {
293316
rm -rf "$probe_dir"
294317
}
295318

296-
verify_base() {
319+
prepare_base() {
297320
local version_file="$1"
298-
local pin_head
321+
local current_head pin_head
299322

300323
[ -f "$base_root/.gclient" ] || die "Chromium base root is missing .gclient: $base_root"
301324
[ -e "$base_src/.git" ] || die "Chromium base src is missing .git: $base_src"
302325
[ -f "$version_file" ] || die "Chromium version file not found: $version_file"
303326

304327
chromium_version="$(read_chromium_version "$version_file")" \
305328
|| die "Could not parse Chromium version file: $version_file"
306-
base_head="$(git -C "$base_src" rev-parse HEAD)"
329+
current_head="$(git -C "$base_src" rev-parse HEAD)"
307330
pin_head="$(git -C "$base_src" rev-parse "refs/tags/$chromium_version^{commit}")" \
308331
|| die "Chromium base is missing pinned tag $chromium_version; refresh the warm base checkout before rerunning"
309-
[ "$base_head" = "$pin_head" ] \
310-
|| die "Chromium base HEAD $base_head does not match pinned $chromium_version ($pin_head); refresh the warm base checkout before rerunning"
311-
312-
check_git_repo_clean "$base_src"
313-
check_nested_git_repos_clean
314-
check_no_browseros_outputs "$base_src"
332+
[ "$current_head" = "$pin_head" ] \
333+
|| die "Chromium base HEAD $current_head does not match pinned $chromium_version ($pin_head); refresh the warm base checkout before rerunning"
334+
335+
# BROWSEROS_CHROMIUM_SRC is infrastructure-owned clone input, never a
336+
# developer workspace. Resetting it is intentionally destructive: all build
337+
# mutations belong in the disposable APFS clone created after this seam.
338+
repair_git_repo "$base_src" "$pin_head"
339+
repair_nested_git_repos
340+
remove_browseros_outputs "$base_src"
341+
base_head="$(git -C "$base_src" rev-parse HEAD)"
315342
}
316343

317344
cleanup_after_setup_error() {
@@ -359,7 +386,7 @@ setup_workspace() {
359386
tag="$(run_tag)"
360387
reap_stale_workspaces "$workspace_parent" "$tag" "$base_root"
361388
version_file="${version_file:-$(default_version_file)}"
362-
verify_base "$version_file"
389+
prepare_base "$version_file"
363390

364391
workspace_root="$workspace_parent/$workspace_prefix$tag"
365392
workspace_src="$workspace_root/src"

packages/browseros/bos_build/ci_workflow_test.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,7 @@ def setUp(self):
16411641
self.github_env = self.root / "github_env"
16421642
self.github_output = self.root / "github_output"
16431643
self.git_log = self.root / "git.log"
1644+
self.git_clean_log = self.root / "git-clean.log"
16441645
self.cp_log = self.root / "cp.log"
16451646
self.version_file = self.root / "CHROMIUM_VERSION"
16461647
self.version_file.write_text(
@@ -1701,6 +1702,14 @@ def _write_fake_git(self):
17011702
fi
17021703
cmd="${1:-}"
17031704
shift || true
1705+
repo="${repo:-.}"
1706+
repo_was_cleaned() {
1707+
[ -f "$GIT_CLEAN_LOG" ] || return 1
1708+
while IFS= read -r cleaned_repo; do
1709+
[ "$cleaned_repo" = "$repo" ] && return 0
1710+
done < "$GIT_CLEAN_LOG"
1711+
return 1
1712+
}
17041713
case "$cmd" in
17051714
rev-parse)
17061715
target="${1:-}"
@@ -1717,6 +1726,7 @@ def _write_fake_git(self):
17171726
esac
17181727
;;
17191728
status)
1729+
repo_was_cleaned && exit 0
17201730
if [ -n "${GIT_DIRTY_REPO:-}" ]; then
17211731
if [ "$repo" = "$GIT_DIRTY_REPO" ]; then
17221732
printf '%b' "${GIT_DIRTY_STATUS:- M nested-change\\n}"
@@ -1725,6 +1735,9 @@ def _write_fake_git(self):
17251735
printf '%b' "${GIT_STATUS:-}"
17261736
fi
17271737
;;
1738+
clean)
1739+
printf '%s\\n' "$repo" >> "$GIT_CLEAN_LOG"
1740+
;;
17281741
esac
17291742
"""
17301743
)
@@ -1761,6 +1774,7 @@ def _env(self, **overrides):
17611774
"BROWSEROS_CHROMIUM_VERSION_FILE": str(self.version_file),
17621775
"CP_LOG": str(self.cp_log),
17631776
"GIT_HEAD": self.head,
1777+
"GIT_CLEAN_LOG": str(self.git_clean_log),
17641778
"GIT_LOG": str(self.git_log),
17651779
"GITHUB_ENV": str(self.github_env),
17661780
"GITHUB_OUTPUT": str(self.github_output),
@@ -1874,7 +1888,7 @@ def test_setup_reaps_only_marked_stale_owned_workspaces(self):
18741888
self.assertTrue(Path(self._outputs()["workspace_root"]).exists())
18751889
self.assertTrue(self.base_root.exists())
18761890

1877-
def test_setup_reaps_stale_workspaces_before_dirty_base_failure(self):
1891+
def test_setup_reaps_stale_workspaces_before_repairing_dirty_base(self):
18781892
parent = self._workspace_parent()
18791893
parent.mkdir()
18801894
stale = self._workspace_root("old-1")
@@ -1888,10 +1902,9 @@ def test_setup_reaps_stale_workspaces_before_dirty_base_failure(self):
18881902
GIT_STATUS=" M chrome/app/generated_resources.grd\n",
18891903
)
18901904

1891-
self.assertNotEqual(result.returncode, 0)
1892-
self.assertIn("tracked or untracked changes", result.stderr + result.stdout)
1905+
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
18931906
self.assertFalse(stale.exists())
1894-
self.assertFalse(self._workspace_root().exists())
1907+
self.assertTrue(self._workspace_root().exists())
18951908
self.assertTrue(self.base_root.exists())
18961909

18971910
def test_cleanup_ignores_unsafe_state_target(self):
@@ -1969,29 +1982,27 @@ def test_setup_fails_when_base_is_not_at_pinned_chromium_tag(self):
19691982
self.assertIn("does not match pinned", result.stderr + result.stdout)
19701983
self.assertFalse(self._workspace_root().exists())
19711984

1972-
def test_setup_fails_when_base_has_tracked_changes(self):
1985+
def test_setup_repairs_base_with_tracked_changes(self):
19731986
result = self._run_helper(
19741987
"setup",
19751988
self.base_src,
19761989
GIT_STATUS=" M chrome/app/generated_resources.grd\n",
19771990
)
19781991

1979-
self.assertNotEqual(result.returncode, 0)
1980-
self.assertIn("tracked or untracked changes", result.stderr + result.stdout)
1981-
self.assertFalse(self._workspace_root().exists())
1992+
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
1993+
self.assertTrue(self._workspace_root().exists())
19821994

1983-
def test_setup_fails_when_base_has_untracked_changes(self):
1995+
def test_setup_repairs_base_with_untracked_changes(self):
19841996
result = self._run_helper(
19851997
"setup",
19861998
self.base_src,
19871999
GIT_STATUS="?? chrome/browser/browseros/generated_resources.grd\n",
19882000
)
19892001

1990-
self.assertNotEqual(result.returncode, 0)
1991-
self.assertIn("tracked or untracked changes", result.stderr + result.stdout)
1992-
self.assertFalse(self._workspace_root().exists())
2002+
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
2003+
self.assertTrue(self._workspace_root().exists())
19932004

1994-
def test_setup_fails_when_nested_gclient_repo_has_changes(self):
2005+
def test_setup_repairs_nested_gclient_repo_with_changes(self):
19952006
nested_repo = self.base_src / "third_party" / "v8"
19962007
nested_repo.mkdir(parents=True)
19972008
(nested_repo / ".git").mkdir()
@@ -2003,20 +2014,18 @@ def test_setup_fails_when_nested_gclient_repo_has_changes(self):
20032014
GIT_DIRTY_STATUS=" M src/builtins/generated.cc\n",
20042015
)
20052016

2006-
self.assertNotEqual(result.returncode, 0)
2007-
self.assertIn(str(nested_repo.resolve()), result.stderr + result.stdout)
2008-
self.assertIn("tracked or untracked changes", result.stderr + result.stdout)
2009-
self.assertFalse(self._workspace_root().exists())
2017+
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
2018+
self.assertTrue(self._workspace_root().exists())
20102019

2011-
def test_setup_fails_when_base_has_browseros_output_dirs(self):
2020+
def test_setup_removes_browseros_output_dirs_from_base(self):
20122021
out_dir = self.base_src / "out" / "Default_browseros_arm64"
20132022
out_dir.mkdir(parents=True)
20142023

20152024
result = self._run_helper("setup", self.base_src)
20162025

2017-
self.assertNotEqual(result.returncode, 0)
2018-
self.assertIn("BrowserOS output state", result.stderr + result.stdout)
2019-
self.assertFalse(self._workspace_root().exists())
2026+
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
2027+
self.assertFalse(out_dir.exists())
2028+
self.assertTrue(self._workspace_root().exists())
20202029

20212030

20222031
@unittest.skipIf(os.name == "nt", "macOS signing helper shell tests run on POSIX")

packages/browseros/bos_build/docs/nightly-macos-ci.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@ commonly fail with `User interaction not allowed`.
140140
The machine needs:
141141

142142
- A persistent BrowserOS checkout.
143-
- A persistent pristine Chromium `src` checkout at the repository pin, used
144-
only as the APFS clone base.
143+
- A persistent Chromium `src` checkout at the repository pin, dedicated to CI
144+
as the APFS clone base. Its local changes and BrowserOS outputs are
145+
disposable.
145146
- `uv`, `gh`, depot_tools, Xcode tools, and Chrome.
146147
- The macOS signing identity and notarization credentials.
147148
- Enough disk for Chromium outputs and DMGs.
@@ -151,19 +152,22 @@ Set these repository variables:
151152
| Variable | Meaning |
152153
| --- | --- |
153154
| `BROWSEROS_REPO_PATH` | Absolute path to the persistent BrowserOS checkout |
154-
| `BROWSEROS_CHROMIUM_SRC` | Absolute path to the warm pristine Chromium base `src` |
155+
| `BROWSEROS_CHROMIUM_SRC` | Absolute path to the warm, CI-owned Chromium clone-base `src` |
155156

156157
Component, R2, signing, and notarization credentials come from GitHub Actions
157158
secrets. `SLACK_WEBHOOK_URL` is optional and receives failures after the Mac job
158159
has started.
159160

160161
Before each signed browser build, `.github/scripts/macos-chromium-workspace.sh`
161-
validates the base checkout and creates a run/attempt-specific APFS
162-
copy-on-write clone of the whole gclient root under
162+
proves that the base is at the configured Chromium tag, destructively resets
163+
tracked and untracked state in the base and nested gclient repositories, and
164+
removes BrowserOS-owned output directories. It then creates a
165+
run/attempt-specific APFS copy-on-write clone of the whole gclient root under
163166
`../browseros-ci-apfs-workspaces/`. `bos_build` receives the clone's `src`, so
164167
cleaning, patching, compiling, signing, packaging, and universal merge outputs
165168
stay inside the disposable workspace. Cleanup runs with `if: always()`, and the
166-
next setup reaps abandoned owned workspaces from killed jobs.
169+
next setup reaps abandoned owned workspaces from killed jobs. Never point
170+
`BROWSEROS_CHROMIUM_SRC` at a developer checkout.
167171

168172
## Troubleshooting
169173

@@ -182,8 +186,9 @@ verify `MACOS_KEYCHAIN_PASSWORD` and the signing identity.
182186

183187
APFS workspace setup fails: confirm the base checkout is on APFS, the helper can
184188
create a same-volume `browseros-ci-apfs-workspaces` sibling directory, the base
185-
`src` is at `packages/browseros/CHROMIUM_VERSION`, and the base has no
186-
BrowserOS output directories or tracked patch/resource changes.
189+
`src` is checked out at `packages/browseros/CHROMIUM_VERSION`, and that pinned
190+
tag exists locally. Ordinary tracked changes, untracked files, nested-repository
191+
changes, and BrowserOS output directories are repaired automatically.
187192

188193
No browser version commit: inspect the hosted `Reserve new browser version on
189194
main` job. It requires `contents: write` and `pull-requests: write`, plus branch

packages/browseros/bos_build/docs/release-ci.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ matching signing key and build-time secrets.
186186
Windows signing needs the eSigner secrets and `SPARKLE_PRIVATE_KEY`. macOS uses
187187
repository variables `BROWSEROS_REPO_PATH` and `BROWSEROS_CHROMIUM_SRC` plus
188188
the signing and notarization secrets on the persistent builder.
189-
`BROWSEROS_CHROMIUM_SRC` is the pristine APFS clone base; the release build
190-
runs against a disposable copy-on-write workspace and cleans it under
191-
`if: always()`. Runner labels, cache behavior, and queue recovery are
192-
documented in `warpbuild-ci.md`; persistent macOS setup is in
193-
`nightly-macos-ci.md`.
189+
`BROWSEROS_CHROMIUM_SRC` is a dedicated, CI-owned APFS clone base. Setup keeps
190+
its pinned Chromium identity strict but repairs local Git changes and
191+
BrowserOS-owned output directories before the release runs against a disposable
192+
copy-on-write workspace. The workspace is cleaned under `if: always()`. Runner
193+
labels, cache behavior, and queue recovery are documented in `warpbuild-ci.md`;
194+
persistent macOS setup is in `nightly-macos-ci.md`.

0 commit comments

Comments
 (0)