2026-08-04 12:15:07 -08:00
#!/usr/bin/env bash
# Sync phase: clone/update depot_tools, clone the chromium-fork checkout,
# gclient sync. Run via `hetzner-runner exec -script sync.sh` against a box
# already provisioned by a separate `up` step -- see ../../.gitea/workflows
# in chromium-fork for the phase breakdown this belongs to.
set -euo pipefail
: " ${ GITEA_TOKEN : ?GITEA_TOKEN must be set } "
: " ${ GITEA_REPO : =Cold-Air-Networks/chromium-fork } "
: " ${ GITEA_REF : =main } "
: " ${ MIRROR_READ_TOKEN : = $GITEA_TOKEN } "
: " ${ CHROMIUM_ROOT : =/root/chromium-ci } "
retry( ) {
local desc = " $1 " ; shift
local attempt = 1
local max = 5
until " $@ " ; do
if [ " $attempt " -ge " $max " ] ; then
echo " $desc failed after $attempt attempts, giving up "
return 1
fi
echo " $desc failed (attempt $attempt / $max ); retrying in 5s "
attempt = $(( attempt + 1 ))
sleep 5
done
}
redact( ) {
sed -E 's#x-access-token:[^@]*@#x-access-token:REDACTED@#g'
}
2026-08-05 02:45:59 -08:00
echo "===== base packages (file, strace) ====="
# `file` is a hard requirement: install-build-deps.py's dev_list() shells
# out to it (via `file -L <path>`) to detect ELF architecture, and this
# minimal Ubuntu cloud image doesn't ship it by default (confirmed: ci run
# #61, 2026-08-05, FileNotFoundError: 'file'). strace is a diagnostic
# nicety for stall-watch.py, not a build requirement -- best-effort only.
if ! command -v file >/dev/null 2>& 1 || ! command -v strace >/dev/null 2>& 1; then
apt-get update -qq
Add stall-watch.py diagnostic wrapper; fix PTY hang in file upload
stall-watch.py wraps a command and, if it produces no output for
--stall-timeout seconds, dumps the process tree, network connections,
per-process /proc/*/wchan, and a 5s live strace sample of the stalled
process before killing it. Wired around gclient sync in sync.sh: ci run
#56 (2026-08-04) stalled there for ~3h with zero output and no diagnostic
trail (last thing logged: devtools-frontend's sync_rollup_libs.py hook).
Allocates its own PTY for the wrapped child so it doesn't reintroduce the
block-buffering the SSH PTY fix was added to avoid.
Needed a general upload capability to get stall-watch.py onto a box that
otherwise only ever receives whatever's piped to `bash -s` -- added
`exec -upload local=remote` (cat > remote over a plain SSH session).
That surfaced a real bug: sshSession() requested a PTY unconditionally,
which uploadFile inherited for its `cat > file` transfer. PTY canonical
mode needs a line-oriented EOF signal a plain channel-close doesn't
reliably deliver, so the very first real upload test hung indefinitely
(confirmed: had to manually kill the process and delete the orphaned
server). Fixed by making PTY allocation opt-in per session (still on for
sshExec/sshExecScript, off for uploadFile) and rewriting uploadFile to
use Start()+StdinPipe()+explicit Close() instead of Run()'s implicit
Stdin-reader-copying, for precise control over when EOF is signaled.
Re-verified end-to-end on a real box: checksum-clean upload, and a
simulated stall correctly captured a real process tree/strace/wchan
showing the stuck `sleep` and killed it with no orphan left behind.
2026-08-04 19:27:17 -08:00
fi
2026-08-05 02:45:59 -08:00
command -v file >/dev/null 2>& 1 || apt-get install -y file
command -v strace >/dev/null 2>& 1 || apt-get install -y strace \
|| echo "strace install failed, continuing without it"
Add stall-watch.py diagnostic wrapper; fix PTY hang in file upload
stall-watch.py wraps a command and, if it produces no output for
--stall-timeout seconds, dumps the process tree, network connections,
per-process /proc/*/wchan, and a 5s live strace sample of the stalled
process before killing it. Wired around gclient sync in sync.sh: ci run
#56 (2026-08-04) stalled there for ~3h with zero output and no diagnostic
trail (last thing logged: devtools-frontend's sync_rollup_libs.py hook).
Allocates its own PTY for the wrapped child so it doesn't reintroduce the
block-buffering the SSH PTY fix was added to avoid.
Needed a general upload capability to get stall-watch.py onto a box that
otherwise only ever receives whatever's piped to `bash -s` -- added
`exec -upload local=remote` (cat > remote over a plain SSH session).
That surfaced a real bug: sshSession() requested a PTY unconditionally,
which uploadFile inherited for its `cat > file` transfer. PTY canonical
mode needs a line-oriented EOF signal a plain channel-close doesn't
reliably deliver, so the very first real upload test hung indefinitely
(confirmed: had to manually kill the process and delete the orphaned
server). Fixed by making PTY allocation opt-in per session (still on for
sshExec/sshExecScript, off for uploadFile) and rewriting uploadFile to
use Start()+StdinPipe()+explicit Close() instead of Run()'s implicit
Stdin-reader-copying, for precise control over when EOF is signaled.
Re-verified end-to-end on a real box: checksum-clean upload, and a
simulated stall correctly captured a real process tree/strace/wchan
showing the stuck `sleep` and killed it with no orphan left behind.
2026-08-04 19:27:17 -08:00
2026-08-04 12:15:07 -08:00
echo "===== depot_tools ====="
if [ ! -d " $HOME /depot_tools " ] ; then
retry "clone depot_tools" git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git " $HOME /depot_tools "
fi
export PATH = " $HOME /depot_tools: $PATH "
2026-08-04 12:40:29 -08:00
# Explicit bootstrap before anything else touches depot_tools' `python3`
# wrapper. That wrapper hard-requires python3_bin_reldir.txt (see
# depot_tools/python3), which is normally created by `update_depot_tools`
# -- but update_depot_tools itself refuses to do anything as root
# ("Running depot tools as root is sad", see depot_tools/update_depot_tools)
# and we're root on this box. gclient sync still works without this
# (confirmed: ci run #54, 2026-08-04, sync completed fully) because it goes
# through vpython3, a separately-bootstrapped wrapper -- but any later
# direct python3 invocation, e.g. install-build-deps.py's shebang, fails
# with exactly this error otherwise. cipd_bin_setup and bootstrap_python3
# are the two functions update_depot_tools calls after its root check;
# calling them straight sidesteps that check entirely.
#
# set +u around this block: these are third-party scripts (depot_tools'
# own cipd/cipd_bin_setup.sh), not written to be safe under nounset --
# confirmed cipd itself references an unset var with no default and dies
# under set -u. They're sourced (not exec'd), so they inherit our shell's
# flags; restore -u immediately after.
set +u
source " $HOME /depot_tools/cipd_bin_setup.sh "
cipd_bin_setup
source " $HOME /depot_tools/bootstrap_python3 "
bootstrap_python3
set -u
2026-08-04 12:15:07 -08:00
mkdir -p " $CHROMIUM_ROOT "
cd " $CHROMIUM_ROOT "
REPO_URL = " https://x-access-token: ${ GITEA_TOKEN } @forge.quinefoundation.com/ ${ GITEA_REPO } .git "
# VK-GL-CTS against chromium.googlesource.com directly was the single
# biggest source of sync flakiness in chromium-fork's original CI workflow
# (corrupted packs, silent stalls). Redirect to the org's own mirror.
VK_GL_CTS_UPSTREAM = "https://chromium.googlesource.com/external/github.com/KhronosGroup/VK-GL-CTS"
git config --global \
url." https://x-access-token: ${ MIRROR_READ_TOKEN } @forge.quinefoundation.com/Cold-Air-Networks/mirror-vk-gl-cts.git " .insteadOf \
" $VK_GL_CTS_UPSTREAM "
echo "===== registered url.insteadOf mappings ====="
git config --global --get-regexp '^url\.' | redact
echo "===== end registered url.insteadOf mappings ====="
cat > .gclient <<EOF
solutions = [
{
"name" : "src" ,
"url" : " $REPO_URL " ,
"custom_deps" : { } ,
"custom_vars" : { } ,
} ,
]
EOF
echo " ===== fetching $GITEA_REPO @ $GITEA_REF ===== "
if [ ! -d src/.git ] ; then
retry "clone src" git clone " $REPO_URL " src
fi
cd src
git remote set-url origin " $REPO_URL "
retry " fetch $GITEA_REF " git fetch origin " $GITEA_REF "
git checkout --force FETCH_HEAD
# --shallow is deliberately absent: chromium-fork's original CI workflow
# traced every VK-GL-CTS stall to --shallow itself (git_cache.py turns it
# into --depth 10000, a much harder reachability problem across VK-GL-CTS's
# 150+ tags/branches than an unlimited-depth fetch of the same refs) --
# removing it is what got that workflow's sync to actually succeed.
echo "===== gclient sync ====="
Add stall-watch.py diagnostic wrapper; fix PTY hang in file upload
stall-watch.py wraps a command and, if it produces no output for
--stall-timeout seconds, dumps the process tree, network connections,
per-process /proc/*/wchan, and a 5s live strace sample of the stalled
process before killing it. Wired around gclient sync in sync.sh: ci run
#56 (2026-08-04) stalled there for ~3h with zero output and no diagnostic
trail (last thing logged: devtools-frontend's sync_rollup_libs.py hook).
Allocates its own PTY for the wrapped child so it doesn't reintroduce the
block-buffering the SSH PTY fix was added to avoid.
Needed a general upload capability to get stall-watch.py onto a box that
otherwise only ever receives whatever's piped to `bash -s` -- added
`exec -upload local=remote` (cat > remote over a plain SSH session).
That surfaced a real bug: sshSession() requested a PTY unconditionally,
which uploadFile inherited for its `cat > file` transfer. PTY canonical
mode needs a line-oriented EOF signal a plain channel-close doesn't
reliably deliver, so the very first real upload test hung indefinitely
(confirmed: had to manually kill the process and delete the orphaned
server). Fixed by making PTY allocation opt-in per session (still on for
sshExec/sshExecScript, off for uploadFile) and rewriting uploadFile to
use Start()+StdinPipe()+explicit Close() instead of Run()'s implicit
Stdin-reader-copying, for precise control over when EOF is signaled.
Re-verified end-to-end on a real box: checksum-clean upload, and a
simulated stall correctly captured a real process tree/strace/wchan
showing the stuck `sleep` and killed it with no orphan left behind.
2026-08-04 19:27:17 -08:00
# Wrapped in stall-watch.py: ci run #56 (2026-08-04) stalled here for ~3h
# with zero output (last thing seen: devtools-frontend's
2026-08-04 21:25:42 -08:00
# sync_rollup_libs.py hook) and left no diagnostic trail. 5 minutes of true
# silence is well outside anything a legitimate hook/clone/cipd-deploy
Add stall-watch.py diagnostic wrapper; fix PTY hang in file upload
stall-watch.py wraps a command and, if it produces no output for
--stall-timeout seconds, dumps the process tree, network connections,
per-process /proc/*/wchan, and a 5s live strace sample of the stalled
process before killing it. Wired around gclient sync in sync.sh: ci run
#56 (2026-08-04) stalled there for ~3h with zero output and no diagnostic
trail (last thing logged: devtools-frontend's sync_rollup_libs.py hook).
Allocates its own PTY for the wrapped child so it doesn't reintroduce the
block-buffering the SSH PTY fix was added to avoid.
Needed a general upload capability to get stall-watch.py onto a box that
otherwise only ever receives whatever's piped to `bash -s` -- added
`exec -upload local=remote` (cat > remote over a plain SSH session).
That surfaced a real bug: sshSession() requested a PTY unconditionally,
which uploadFile inherited for its `cat > file` transfer. PTY canonical
mode needs a line-oriented EOF signal a plain channel-close doesn't
reliably deliver, so the very first real upload test hung indefinitely
(confirmed: had to manually kill the process and delete the orphaned
server). Fixed by making PTY allocation opt-in per session (still on for
sshExec/sshExecScript, off for uploadFile) and rewriting uploadFile to
use Start()+StdinPipe()+explicit Close() instead of Run()'s implicit
Stdin-reader-copying, for precise control over when EOF is signaled.
Re-verified end-to-end on a real box: checksum-clean upload, and a
simulated stall correctly captured a real process tree/strace/wchan
showing the stuck `sleep` and killed it with no orphan left behind.
2026-08-04 19:27:17 -08:00
# should ever go without printing *something* -- if it happens again this
# dumps the process tree, network state, and a live strace sample of
# whatever's actually stuck, before killing it.
2026-08-04 21:25:42 -08:00
#
# ci run #57 (2026-08-05) then spent 90+ minutes cycling through this loop
# with no clear signal from the outside about which attempt it was on or
# whether stall-watch had even fired -- Gitea's log capture visibly lags
# real execution by a lot on long steps (confirmed on both runs), so
# "no new log lines" doesn't mean "no progress". Two fixes for that,
# neither of which depends on the log catching up in real time:
# - explicit timestamped start/end/duration per attempt, so whatever log
# *does* eventually land is unambiguous about where the time went.
# - a hard wall-clock budget across the whole retry loop (not just per
# attempt) -- run #57 never got a chance to hit this because it was
# killed manually; now the loop itself will give up and report
# instead of running indefinitely regardless of individual attempts
# each staying under the stall threshold.
SYNC_LOOP_START = $( date +%s)
SYNC_LOOP_BUDGET = 2400 # 40 minutes total across all attempts
2026-08-04 12:15:07 -08:00
ATTEMPT = 1
MAX_ATTEMPTS = 5
while true; do
2026-08-04 21:25:42 -08:00
ATTEMPT_START = $( date +%s)
echo " ----- gclient sync attempt $ATTEMPT / $MAX_ATTEMPTS starting at $( date -u +%FT%TZ) ----- "
if python3 " $HOME /stall-watch.py " --stall-timeout 300 -- \
Add stall-watch.py diagnostic wrapper; fix PTY hang in file upload
stall-watch.py wraps a command and, if it produces no output for
--stall-timeout seconds, dumps the process tree, network connections,
per-process /proc/*/wchan, and a 5s live strace sample of the stalled
process before killing it. Wired around gclient sync in sync.sh: ci run
#56 (2026-08-04) stalled there for ~3h with zero output and no diagnostic
trail (last thing logged: devtools-frontend's sync_rollup_libs.py hook).
Allocates its own PTY for the wrapped child so it doesn't reintroduce the
block-buffering the SSH PTY fix was added to avoid.
Needed a general upload capability to get stall-watch.py onto a box that
otherwise only ever receives whatever's piped to `bash -s` -- added
`exec -upload local=remote` (cat > remote over a plain SSH session).
That surfaced a real bug: sshSession() requested a PTY unconditionally,
which uploadFile inherited for its `cat > file` transfer. PTY canonical
mode needs a line-oriented EOF signal a plain channel-close doesn't
reliably deliver, so the very first real upload test hung indefinitely
(confirmed: had to manually kill the process and delete the orphaned
server). Fixed by making PTY allocation opt-in per session (still on for
sshExec/sshExecScript, off for uploadFile) and rewriting uploadFile to
use Start()+StdinPipe()+explicit Close() instead of Run()'s implicit
Stdin-reader-copying, for precise control over when EOF is signaled.
Re-verified end-to-end on a real box: checksum-clean upload, and a
simulated stall correctly captured a real process tree/strace/wchan
showing the stuck `sleep` and killed it with no orphan left behind.
2026-08-04 19:27:17 -08:00
gclient sync --with_branch_heads --with_tags -j" $( nproc) " --verbose --force 2>& 1 | redact; then
2026-08-04 21:25:42 -08:00
ATTEMPT_ELAPSED = $(( $( date +%s) - ATTEMPT_START ))
echo " ----- gclient sync attempt $ATTEMPT / $MAX_ATTEMPTS succeeded after ${ ATTEMPT_ELAPSED } s ----- "
2026-08-04 12:15:07 -08:00
break
fi
2026-08-04 21:25:42 -08:00
ATTEMPT_ELAPSED = $(( $( date +%s) - ATTEMPT_START ))
LOOP_ELAPSED = $(( $( date +%s) - SYNC_LOOP_START ))
echo " ----- gclient sync attempt $ATTEMPT / $MAX_ATTEMPTS failed after ${ ATTEMPT_ELAPSED } s ( ${ LOOP_ELAPSED } s total so far) ----- "
2026-08-04 12:15:07 -08:00
if [ " $ATTEMPT " -ge " $MAX_ATTEMPTS " ] ; then
echo " gclient sync failed after $ATTEMPT attempts, giving up "
exit 1
fi
2026-08-04 21:25:42 -08:00
if [ " $LOOP_ELAPSED " -ge " $SYNC_LOOP_BUDGET " ] ; then
echo " gclient sync retry loop exceeded its ${ SYNC_LOOP_BUDGET } s wall-clock budget after attempt $ATTEMPT , giving up rather than continuing indefinitely "
exit 1
fi
echo " retrying ( $(( ATTEMPT + 1 )) / $MAX_ATTEMPTS ) "
2026-08-04 12:15:07 -08:00
ATTEMPT = $(( ATTEMPT + 1 ))
done