# 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: 1. `source-checks` runs the bootstrap test path with `SKIP_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 runs `scripts/verify_ci_workflow.sh`, which parses the workflow YAML, checks every workflow `run:` block with `bash -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 `.gclient` files 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 official `shorebirdtech` GitHub 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, run `scripts/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.sh` also 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.sh` independently scans build-sensitive Flutter, Shorebird CLI, artifact-proxy, updater, `.gclient`, and submodule metadata for closed Shorebird endpoints, official `shorebirdtech` dependency remotes, and private prebuilt buckets, while requiring the open local server and artifact mirror defaults. `scripts/verify_powershell_open_defaults.sh` checks the Windows launcher and Flutter Dart SDK updater PowerShell scripts for open Flutter/artifact defaults and parses them with `pwsh` when it is available. `scripts/verify_write_sha256.sh` smoke-tests the portable checksum helper used by every artifact job. `scripts/verify_ios_interpreter_route_validator.sh` smoke-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. 2. 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. 3. `cli-artifacts` compiles: - `shorebird` for Linux x64, macOS x64, macOS arm64, and Windows x64 - `open_aot_patch_tools` for Linux x64, macOS x64, macOS arm64, and Windows x64 - `artifact_proxy` for Linux x64, macOS x64, macOS arm64, and Windows x64 - public updater `patch` binaries packaged as CLI-cache mirror artifacts: `patch-linux-x64.zip`, `patch-darwin-x64.zip`, `patch-darwin-arm64.zip`, and `patch-windows-x64.zip` - `mirror-metadata`, containing `shorebird//artifacts_manifest.yaml` The job provisions stable Rust before building the updater `patch` binary, 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 (`patch` on Unix, `patch.exe` on Windows) with a non-empty payload. Each CLI upload is an `open-shorebird-cli--.tar.gz` archive with `bin/shorebird`, `bin/open_aot_patch_tools`, `bin/artifact_proxy`, `bin/internal` version metadata, `manifest.json`, and a `.sha256` sidecar. 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 on `macos-15-intel`, and macOS arm64 runs on `macos-14`. Before upload, CI extracts the archive and runs `shorebird --version`, `open_aot_patch_tools --help`, and `artifact_proxy --health-check` from the extracted layout. The `shorebird --version` smoke must report the open `https://git.tonycloud.org/flutter/shorebird.git` fork and rejects the old official Shorebird SSH remote. 4. `server-artifacts` runs `go test ./...` and cross-compiles `shorebird-server` for 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 `.sha256` sidecar. The manifest records both the root workflow commit and the server submodule commit. Keep `web/` 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//` on the host referenced by `SHOREBIRD_ARTIFACT_BASE_URL`. Each `mirror-patch-*.zip` GitHub artifact also includes a publish-ready `shorebird//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: ```sh gh run download --dir downloaded-artifacts ./scripts/verify_downloaded_release_artifacts.sh \ --github-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: ```sh ./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: ```sh ./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: ```sh 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: ```text $SHOREBIRD_ARTIFACT_BASE_URL/shorebird//patch-linux-x64.zip $SHOREBIRD_ARTIFACT_BASE_URL/shorebird//patch-darwin-x64.zip $SHOREBIRD_ARTIFACT_BASE_URL/shorebird//patch-darwin-arm64.zip $SHOREBIRD_ARTIFACT_BASE_URL/shorebird//patch-windows-x64.zip $SHOREBIRD_ARTIFACT_BASE_URL/shorebird//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-latest` for Linux SDK, Linux engine, Android engine, and web SDK builds - `macos-latest` for 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: ```gn 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_arm64` with `--shorebird-interpreter`, `dart_dynamic_modules=false`, `dart_enable_aot_patching=true`, and `dart_enable_shorebird_interpreter=true` - `ios_release` with `--shorebird-interpreter` - `macos_release_arm64` with `dart_dynamic_modules=false`, `dart_enable_aot_patching=true`, `shorebird_enable_aot_patching=true`, and `shorebird_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/`, 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.