26 KiB
GitHub CI
The root workflow at .github/workflows/open-shorebird-ci.yml validates the
combined open Shorebird workspace rather than any single submodule in isolation.
Default CI
Push and pull request runs perform source-level checks and build distributable binaries:
source-checksruns the bootstrap test path withSKIP_GCLIENT_SYNC=1, including CLI, code push client, artifact proxy, updater library, open patch tooling, and server tests. This checks the Shorebird CLI focused tests, code push client open-server default tests, open patch tools, server tests, and the root shell scripts without downloading the full engine dependency graph. It also runsscripts/verify_ci_workflow.sh, which parses the workflow YAML, checks every workflowrun:block withbash -n, and verifies the expected open SDK/CLI/server artifact contract, including archive creation and checksum sidecar generation. It also checks.gitmodules,docs/REPOSITORIES.md, and generated.gclientfiles so a fresh GitHub checkout uses open HTTPS remotes instead of local paths, SSH remotes, or official closed Shorebird repositories. The verifier also scans runtime/build-sensitive CLI, updater, artifact-proxy, Flutter tool, Gradle, engine metadata, and web UI artifact-copy sources to reject hosted Shorebird endpoints and officialshorebirdtechGitHub dependencies. It also reads the forked engine BUILD files and checks that the GN targets and archive names referenced by the manual engine jobs still exist. Before uploading the workspace, runscripts/verify_upload_readiness.sh. It invokes the same workflow contract verifier in upload-ready mode, which requires every listed CI support file to be tracked in its owning checkout and requires the root plus required submodule checkouts to be clean.scripts/verify_sync_open_sources.shalso exercises the source-link helper in an isolated temporary workspace so clean generated engine checkouts are replaced by the workspace Dart/updater submodules and dirty targets are not overwritten.scripts/verify_open_infrastructure_defaults.shindependently scans build-sensitive Flutter, Shorebird CLI, artifact-proxy, updater,.gclient, and submodule metadata for closed Shorebird endpoints, officialshorebirdtechdependency remotes, and private prebuilt buckets, while requiring the open local server and artifact mirror defaults.scripts/verify_powershell_open_defaults.shchecks the Windows launcher and Flutter Dart SDK updater PowerShell scripts for open Flutter/artifact defaults and parses them withpwshwhen it is available.scripts/verify_write_sha256.shsmoke-tests the portable checksum helper used by every artifact job.scripts/verify_ios_interpreter_route_validator.shsmoke-tests the iOS App Store route gate with synthetic GN args and encrypted patch artifacts, covering a valid interpreter artifact and rejecting dynamic-module, non-iOS, malformed JSON, and Mach-O native patch inputs.- All artifact and runtime-smoke jobs declare
needs: source-checks, so no uploaded CLI, server, SDK, or engine artifact is produced before the source-level open-replacement contract passes. cli-artifactscompiles:shorebirdfor Linux x64, macOS x64, macOS arm64, and Windows x64open_aot_patch_toolsfor Linux x64, macOS x64, macOS arm64, and Windows x64artifact_proxyfor Linux x64, macOS x64, macOS arm64, and Windows x64- public updater
patchbinaries packaged as CLI-cache mirror artifacts:patch-linux-x64.zip,patch-darwin-x64.zip,patch-darwin-arm64.zip, andpatch-windows-x64.zip mirror-metadata, containingshorebird/<engine-revision>/artifacts_manifest.yamlThe job provisions stable Rust before building the updaterpatchbinary, so those mirror artifacts do not depend on preinstalled runner state. CI opens each generated patch mirror ZIP before upload and verifies it has exactly the cache-facing entry name (patchon Unix,patch.exeon Windows) with a non-empty payload. Each CLI upload is anopen-shorebird-cli-<os>-<arch>.tar.gzarchive withbin/shorebird,bin/open_aot_patch_tools,bin/artifact_proxy,bin/internalversion metadata,manifest.json, and a.sha256sidecar. The manifest records the Flutter revision and engine revision used by the bundled CLI cache metadata. The macOS CLI split is intentional: macOS x64 runs onmacos-15-intel, and macOS arm64 runs onmacos-14. Before upload, CI extracts the archive and runsshorebird --version,open_aot_patch_tools --help, andartifact_proxy --health-checkfrom the extracted layout. Theshorebird --versionsmoke must report the openhttps://git.tonycloud.org/flutter/shorebird.gitfork and rejects the old official Shorebird SSH remote.
server-artifactsrunsgo test ./...and cross-compilesshorebird-serverfor Linux x64/arm64, macOS x64/arm64, and Windows x64 targets. Each uploaded archive contains the server binary,web/dashboard assets,.env.example,README.md,openapi.yaml,manifest.json, and a.sha256sidecar. The manifest records both the root workflow commit and the server submodule commit. Keepweb/beside the executable when unpacking; the server looks for dashboard assets relative to its working directory and executable path. The Linux x64 archive is extracted and smoke-tested in CI by starting the packaged binary with SQLite/local storage, checking/health, fetching the dashboard HTML from/, and verifying/openapi.yaml.
The CLI/server artifacts are uploaded from each workflow run. CLI archives are
intended for installation or release attachments; patch mirror ZIPs are separate
because the Shorebird CLI cache expects the patch-*.zip names and a patch
or patch.exe entry inside each ZIP. The patch mirror ZIPs include .sha256
sidecars and can be copied under
/shorebird/<engine-revision>/ on the host referenced by
SHOREBIRD_ARTIFACT_BASE_URL. Each mirror-patch-*.zip GitHub artifact also
includes a publish-ready shorebird/<engine-revision>/patch-*.zip copy and
matching .sha256 sidecar, so mirror publication can copy the shorebird/
subtree directly. The mirror-metadata artifact provides the matching
artifacts_manifest.yaml for the artifact proxy.
Manual engine artifacts also include a mirror/ subtree whose contents are
already laid out under shorebird/flutter_infra_release/...; copy that subtree
to the same artifact mirror root to satisfy the generated
artifacts_manifest.yaml overrides.
On full_sdk_build=true runs, artifact-mirror downloads the mirror metadata,
all platform patch ZIPs, and the Linux/Android/web/iOS/macOS engine artifacts,
then uploads open-shorebird-artifact-mirror. It also downloads the CLI,
server, and custom Dart SDK artifacts and uploads
open-shorebird-release-manifest, a JSON manifest that verifies every archived
release artifact has a matching checksum sidecar and records its SHA-256 digest,
size, relative path, downloaded artifact group, file name, and workflow commit.
Zero-byte release artifacts are rejected even when their sidecars match.
The manifest job also requires every
expected CLI, server, custom Dart SDK, engine, patch-tool, metadata, and mirror
artifact family to be present. The mirror archive is the checked publish-ready
mirror root for SHOREBIRD_ARTIFACT_BASE_URL; the release manifest is the
provenance index for the full SDK/CLI/server/engine output set.
The artifact-mirror job validates the generated manifest with
scripts/validate_release_manifest.py before upload. After downloading workflow
artifacts, the same validator can audit the manifest against the downloaded
artifact directory and will reject missing files, duplicate or unsafe paths,
checksum/size mismatches, sidecar mismatches, orphan sidecars, mismatched
artifact group or file-name provenance fields, and artifacts that are present on
disk but missing from the manifest.
Use the wrapper below after downloading a completed full_sdk_build=true run:
gh run download <run-id> --dir downloaded-artifacts
./scripts/verify_downloaded_release_artifacts.sh \
--github-sha <run-head-sha> \
downloaded-artifacts
It verifies the release manifest sidecar, validates the manifest against every
downloaded artifact, optionally requires the manifest commit to match the
expected run SHA, verifies the mirror archive sidecar, safely extracts
open-shorebird-artifact-mirror.tar.gz after rejecting unsafe archive members,
and validates the extracted mirror root before anything is published.
After the workspace is uploaded to GitHub, the end-to-end proof can be driven from a local checkout with:
./scripts/verify_hosted_full_sdk_build.sh \
--repo owner/repo \
--ref main
That helper dispatches open-shorebird-ci.yml with full_sdk_build=true,
waits for the hosted workflow run to succeed, downloads all artifacts, and runs
scripts/verify_downloaded_release_artifacts.sh against the downloaded output
with the workflow run's headSha.
It uses GitHub CLI when gh is installed; otherwise it uses the GitHub REST API
with GITHUB_TOKEN or GH_TOKEN plus curl, jq, and unzip.
Use --linux-heavy-runner, --macos-heavy-runner, and the disk-threshold flags
when the repository uses custom larger/self-hosted runner labels.
For local release assembly or reassembly after downloading workflow artifacts,
run:
./scripts/assemble_artifact_mirror.sh downloaded-artifacts public-mirror
The assembler scans direct artifact contents plus *.tar.gz engine archives,
validates archive member paths, rejects links/devices before extraction, copies
every shorebird/ mirror subtree into public-mirror, rejects conflicting
files with different bytes, validates that each
artifacts_manifest.yaml override resolves to a copied non-empty file, verifies
every platform patch-*.zip contains exactly the cache-facing patch or
patch.exe entry, and writes missing .sha256 sidecars.
scripts/validate_artifact_mirror.py public-mirror can be run independently
after safely extracting open-shorebird-artifact-mirror.tar.gz; the
artifact-mirror job safe-extracts and validates the assembled archive before
upload.
scripts/verify_assemble_artifact_mirror.sh keeps the low-level assembler
covered in default source checks. scripts/verify_artifact_mirror_workflow_assembly.sh
also dry-runs the full artifact-mirror aggregation flow with fake downloaded
artifacts, including the mirror-only input subset, extracted mirror validation,
and release manifest requirements. This catches path-layout regressions before
the hosted full_sdk_build=true run.
CI writes checksum sidecars through scripts/write_sha256.sh so the artifact
jobs do not depend on platform-specific checksum tools.
The workflow defaults all run steps to Bash, including Windows matrix jobs,
because the workspace scripts and packaging commands use Bash syntax.
On June 25, 2026, the Darwin arm64 CLI artifact path was smoke-tested locally:
the workflow-equivalent commands compiled shorebird, open_aot_patch_tools,
and artifact_proxy; built the Rust updater patch binary; produced
open-shorebird-cli-macos-arm64.tar.gz and patch-darwin-arm64.zip; wrote both
checksum sidecars; extracted the CLI archive; and verified the compiled tools
could run from the extracted layout.
The patch mirror ZIP contained a single patch entry, matching the CLI cache
contract.
The Darwin arm64 server artifact path was smoke-tested locally the same day:
the workflow-equivalent packaging command produced
shorebird-server-darwin-arm64.tar.gz, wrote a .sha256 sidecar, extracted
the archive, started the packaged server with SQLite and local storage, and
verified /health returned status=ok while / served the packaged dashboard
HTML from the sibling web/ directory. The packaged server also serves the
checked OpenAPI contract at /openapi.yaml.
Open Artifact Hosts
The CLI can be used with a self-hosted server and open artifact mirror without
changing source code. When no project base_url, user config, or environment
override is present, the open CLI defaults API/auth traffic to the local
self-hosted server at http://localhost:8080 instead of Shorebird's hosted
service:
export SHOREBIRD_HOSTED_URL=https://updates.example.com
export SHOREBIRD_ARTIFACT_BASE_URL=https://artifacts.example.com/open
export SHOREBIRD_FLUTTER_STORAGE_BASE_URL=https://artifacts.example.com/flutter
export SHOREBIRD_FLUTTER_GIT_URL=https://github.com/example/open-flutter.git
SHOREBIRD_HOSTED_URL points API calls at the self-hosted server and is also
written into new shorebird.yaml files as base_url. The bundled updater
library also defaults to http://localhost:8080 when a legacy app omits
base_url. The self-hosted server publishes its checked OpenAPI contract at
http://localhost:8080/openapi.yaml. The artifact mirror keeps the same path
layout as Shorebird's default storage bucket:
$SHOREBIRD_ARTIFACT_BASE_URL/shorebird/<engine-revision>/patch-linux-x64.zip
$SHOREBIRD_ARTIFACT_BASE_URL/shorebird/<engine-revision>/patch-darwin-x64.zip
$SHOREBIRD_ARTIFACT_BASE_URL/shorebird/<engine-revision>/patch-darwin-arm64.zip
$SHOREBIRD_ARTIFACT_BASE_URL/shorebird/<engine-revision>/patch-windows-x64.zip
$SHOREBIRD_ARTIFACT_BASE_URL/shorebird/<engine-revision>/artifacts_manifest.yaml
When SHOREBIRD_ARTIFACT_BASE_URL is not set, the open CLI defaults CLI-managed
artifact downloads to http://localhost:8080/artifacts. Use that local mirror
root for development, or set SHOREBIRD_ARTIFACT_BASE_URL to the public mirror
where the CI-produced patch-*.zip artifacts are hosted.
If the mirror is a bucket-style host, SHOREBIRD_STORAGE_BASE_URL and
SHOREBIRD_STORAGE_BUCKET can be used instead of
SHOREBIRD_ARTIFACT_BASE_URL. SHOREBIRD_FLUTTER_STORAGE_BASE_URL controls the
FLUTTER_STORAGE_BASE_URL passed to vended Flutter commands; when unset it
defaults to http://localhost:8080/download.flutter.io.
SHOREBIRD_FLUTTER_GIT_URL controls where the CLI clones vended Flutter
revisions during cache installation. When unset, the open CLI defaults to this
workspace's open Flutter fork instead of github.com/shorebirdtech/flutter.git.
The Flutter fork also defaults its own engine downloads, Dart SDK refresh
scripts, doctor network check, Android Gradle Maven host, Android host-app
integration fixtures, docs artifact scripts, and Shorebird integration tests to
http://localhost:8080/download.flutter.io when FLUTTER_STORAGE_BASE_URL is
unset, so direct Flutter use does not fall back to Shorebird's closed artifact
host.
The open CI currently does not publish aot-tools.dill. That file is a
different Shorebird linker artifact used by the legacy native-AOT iOS linker
path. The open App Store-safe iOS path uses encrypted Dart bytecode interpreter
artifacts instead, and Android/Linux/macOS/Windows patch creation uses the
public updater patch binaries above.
The CLI cache does not download aot-tools.dill by default. Set
SHOREBIRD_ENABLE_LEGACY_AOT_TOOLS=1 only when deliberately validating the
development-only legacy native-AOT linker path.
Runtime smoke scripts that need external targets are kept out of default CI:
scripts/android_runtime_patch_smoke.sh requires an attached Android
device/emulator with app-private storage access, and
scripts/linux_runtime_patch_smoke.sh requires a Linux desktop runtime or
xvfb-run. Use them on suitable self-hosted runners when extending runtime
coverage beyond the artifact build matrix.
Use workflow_dispatch with run_runtime_smokes=true to run those external
runtime smokes from CI. The runtime jobs are intentionally separate from
full_sdk_build=true: they are for provisioned runners that already have the
matching local engine build outputs in the checkout workspace. Runtime checkout
uses clean: false so self-hosted runners can preserve those out/ directories
between preparation and smoke runs.
| Input | Default | Used by |
|---|---|---|
run_runtime_smokes |
false |
Enables linux-runtime-smoke and android-runtime-smoke |
linux_runtime_runner |
self-hosted |
Runner label for scripts/linux_runtime_patch_smoke.sh |
android_runtime_runner |
self-hosted |
Runner label for scripts/android_runtime_patch_smoke.sh |
android_serial |
empty | Optional adb serial passed as ANDROID_SERIAL |
The Linux runtime runner must provide
flutter/engine/src/out/linux_release_x64. The Android runtime runner must provide
flutter/engine/src/out/android_release_arm64 and
flutter/engine/src/out/host_release_arm64, plus an Android target where either
adb shell run-as works for the test package or adb root works. It must also
provide Java on PATH because the smoke builds APKs before seeding the patch.
Heavy SDK Builds
Default push and pull request runs build the custom SDK and engine artifacts.
Manual workflow_dispatch runs also build them by default because
full_sdk_build=true is the default input value; set full_sdk_build=false
only for source/CLI/server-only manual runs.
The heavy jobs install Chromium's depot_tools into the workflow workspace
before running gclient, so they can bootstrap from a clean runner. The
workflow defaults heavy jobs to managed GitHub-hosted runners:
ubuntu-latestfor Linux SDK, Linux engine, Android engine, and web SDK buildsmacos-latestfor macOS Dart SDK, iOS engine, and macOS engine builds
Override linux_heavy_runner / macos_heavy_runner when a repository wants
larger or self-hosted runners. Every SDK/engine job also runs
scripts/check_ci_capacity.sh before gclient sync or ninja; SDK-only jobs
and engine jobs require at least 8 GiB free by default. Raise the dispatch
thresholds for runner images where a larger preflight budget should be enforced.
The hosted Android engine job also provisions Temurin Java 17 before building
Android JAR/APK-related engine artifacts. Every SDK/engine job verifies
python3, gclient, and ninja before generating build files, so
PATH/toolchain problems fail before a long build starts. CI sets
DEPOT_TOOLS_UPDATE=0, and the local bootstrap scripts default the same way,
so depot_tools uses the pinned submodule revision unless explicitly overridden.
Manual dispatch accepts these workflow inputs:
| Input | Default | Used by |
|---|---|---|
linux_heavy_runner |
ubuntu-latest |
custom-dart-sdk, linux-engine, android-engine, web-sdk |
macos_heavy_runner |
macos-latest |
custom-dart-sdk-macos, ios-engine / Apple engine artifacts |
sdk_min_free_disk_gb |
8 |
Minimum free disk GiB for custom-dart-sdk and custom-dart-sdk-macos |
engine_min_free_disk_gb |
8 |
Minimum free disk GiB for Linux, Android, web, iOS, and macOS engine builds |
base_flutter_engine_revision |
empty | Optional upstream Flutter engine revision recorded in artifacts_manifest.yaml for non-overridden artifact proxy fallbacks |
Use these inputs to move the SDK/engine jobs onto different larger or
self-hosted runners without editing the workflow file. The Linux heavy jobs also run
scripts/free_ci_disk_linux.sh, which removes unrelated preinstalled toolchain
caches only when GITHUB_ACTIONS=true and RUNNER_ENVIRONMENT=github-hosted.
Self-hosted runners skip that cleanup by default. Set CI_FREE_DISK_SPACE=0 to
skip cleanup anywhere, or CI_FREE_DISK_SPACE_FORCE=1 to opt in on a
self-hosted runner.
When base_flutter_engine_revision is empty, mirror-metadata records the
same revision as flutter/bin/internal/engine.version. Set it when the open
Shorebird engine revision is a custom fork revision and unchanged artifacts
should be proxied back to a different upstream Flutter engine revision.
The engine jobs perform two syncs when run_gclient_sync=true: the root
workspace sync for the Dart SDK checkout, then a second sync from flutter/ for
Flutter engine dependencies written by scripts/write_gclient.sh. Linux engine
jobs use INCLUDE_ENGINE_DEPS=1 to include Flutter engine dependencies,
Android dependencies, and emsdk; the Apple engine job uses the same flag to
include iOS, Android, and emsdk
dependencies on macOS.
Before building, scripts/sync_open_sources.sh links
flutter/engine/src/flutter/third_party/dart to the dart-sdk submodule
and flutter/engine/src/flutter/third_party/updater to the updater
submodule. It does not clone the official shorebirdtech/updater repository by
default; set UPDATER_URL explicitly only when testing a different updater
fork. The helper rejects known unpatched upstream Dart SDK remotes and official
Shorebird updater remotes so CI does not accidentally build from the wrong
component checkout.
The Flutter fork's DEPS file also points its Dart SDK and updater dependency
URLs at the open mirrors and does not reference Shorebird's private
shorebird-dart-sdk-prebuilt bucket; custom SDK jobs build and archive the
open Dart SDK fork directly.
custom-dart-sdk and custom-dart-sdk-macos build the Dart SDK fork with:
dart_dynamic_modules = false
dart_enable_aot_patching = true
dart_enable_shorebird_interpreter = true
They run scripts/verify_dart_sdk_args.sh, then the focused VM patch API
tests. The Linux job uploads custom-dart-sdk-linux-x64; the macOS job uploads
custom-dart-sdk-macos-arm64. Each archive contains the Dart SDK,
gen_snapshot, dartaotruntime, args.gn, and a manifest.json that records
the root workflow commit, Dart SDK source commit, and patch-related build
flags. The workflow also uploads .sha256 sidecars for the SDK archives.
Before writing the checksum, each SDK job extracts the archive, verifies
manifest.json, args.gn, gen_snapshot, dartaotruntime, and
dart-sdk/bin/dart, then runs the extracted dart --version.
ios-engine builds:
host_release_arm64with--shorebird-interpreter,dart_dynamic_modules=false,dart_enable_aot_patching=true, anddart_enable_shorebird_interpreter=trueios_releasewith--shorebird-interpretermacos_release_arm64withdart_dynamic_modules=false,dart_enable_aot_patching=true,shorebird_enable_aot_patching=true, andshorebird_use_interpreter=false
It runs scripts/verify_ios_interpreter_route.sh before uploading the
ios-interpreter-engine artifact, so the archived build proves
DART_DYNAMIC_MODULES is off and the iOS route uses the bytecode interpreter
instead of native AOT patch loading. The archive includes Flutter.framework,
Flutter.xcframework, a mirror-ready ios-release/artifacts.zip containing
the xcframework plus gen_snapshot_arm64, analyze_snapshot_arm64, and the
code-sign configuration files Flutter expects, the
host_release_arm64/gen_snapshot binary built with --shorebird-interpreter,
the iOS and host args.gn files, and a manifest.json that records
dart_dynamic_modules=false,
dart_enable_aot_patching=true, dart_enable_shorebird_interpreter=true,
shorebird_enable_aot_patching=false, shorebird_use_interpreter=true, and
the Flutter engine revision; the workflow also uploads a .sha256 sidecar for
the engine archive.
Before checksum upload, CI extracts the iOS engine archive and verifies
Flutter.framework, Flutter.xcframework, ios-release/artifacts.zip, the
mirror copy under mirror/shorebird/flutter_infra_release/flutter/<engine>,
both args files, manifest.json, and the executable host gen_snapshot.
When an app or IPA is supplied to the gate, strict mode also rejects bundled
patch payloads, executable-memory entitlements, and raw aot_patch_key_hex
material in shorebird.yaml.
The same Apple job uploads a separate macos-engine-arm64 artifact containing
FlutterMacOS.framework.zip, flutter_patched_sdk_product.zip, the macOS
args.gn, a mirror-ready copy of the macOS framework override, a manifest with
the Flutter engine revision, and a .sha256 sidecar. The shared
flutter_patched_sdk_product.zip mirror override is published only by the Linux
engine job to avoid duplicate producers for the same mirror path. The macOS
build is checked with
scripts/verify_engine_args.sh so CI fails if dart_dynamic_modules=true
appears or if the native AOT patch runtime flags are missing from the generated
args. CI extracts the macOS engine archive before upload and verifies the
framework zip, patched SDK zip, args file, manifest, and framework mirror
subtree.
linux-engine builds linux_release_x64 on Ubuntu with
dart_dynamic_modules=false, dart_enable_aot_patching=true,
shorebird_enable_aot_patching=true, and shorebird_use_interpreter=false. It
uploads linux-x64-flutter-gtk.zip, flutter_patched_sdk_product.zip,
artifacts.zip, mirror-ready copies of those engine override files, args.gn,
a manifest with the Flutter engine revision, and a .sha256 sidecar. CI
extracts the archive before upload and verifies the GTK zip, patched SDK zip,
artifacts zip, args file, manifest, and mirror subtree.
android-engine builds android_release_arm64 on Ubuntu with
dart_dynamic_modules=false, dart_enable_aot_patching=true,
shorebird_enable_aot_patching=true, and shorebird_use_interpreter=false. It
uploads artifacts.zip, symbols.zip, flutter.jar, libflutter.so, host
gen_snapshot_arm64, analyze_snapshot_arm64, mirror-ready copies of the
Android engine override files, args.gn, a manifest with the Flutter engine
revision, and a .sha256 sidecar. CI extracts the archive before upload and
verifies the Android artifacts/symbols zips, flutter.jar, libflutter.so,
host snapshot/analyzer tools, args file, manifest, and mirror subtree.
web-sdk builds the Flutter web SDK archive from wasm_release with
dart_dynamic_modules=false and flutter_prebuilt_dart_sdk=true, then uploads
flutter-web-sdk.zip, a mirror-ready copy of that SDK archive, args.gn, a
manifest with the Flutter engine revision, and a .sha256 sidecar. The web job
does not download Flutter's private prebuilt Dart SDK; it links
dart-sdk/tools/sdks/dart-sdk into Flutter's expected prebuilt path before
running GN. Native engine jobs use --no-prebuilt-dart-sdk because the open
workspace builds those SDK artifacts from the linked Dart checkout instead. Web
is still not a Shorebird CodePush release platform in this CLI/protocol; this
job exists to keep the open Flutter SDK/web artifacts buildable from the
workspace. CI extracts the web SDK archive before upload and verifies the SDK
zip, args file, manifest, and mirror subtree.
Set run_gclient_sync=false only for debugging a runner image that already has
all gclient-managed dependencies restored.