Files
shorebird-workspace/scripts/verify_hosted_full_sdk_build.sh
T
2026-06-25 20:19:40 +08:00

382 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'EOF'
usage: verify_hosted_full_sdk_build.sh --repo owner/name [options]
Dispatches the Open Shorebird full SDK build on GitHub Actions, waits for the
workflow run to finish, downloads all artifacts, and verifies the release
manifest plus assembled artifact mirror.
Uses GitHub CLI when `gh` is available. Otherwise uses the GitHub REST API with
`GITHUB_TOKEN` or `GH_TOKEN`; that fallback also requires `curl`, `jq`, and
`unzip`.
Options:
--repo owner/name GitHub repository to run against.
--ref branch-or-sha Ref to dispatch. Defaults to current branch.
--workflow file Workflow file. Defaults to open-shorebird-ci.yml.
--download-dir path Artifact download directory. Defaults to hosted-full-sdk-artifacts.
--timeout-minutes minutes Maximum wait time. Defaults to 720.
--poll-seconds seconds Poll interval. Defaults to 30.
--linux-heavy-runner label Override linux_heavy_runner.
--macos-heavy-runner label Override macos_heavy_runner.
--sdk-min-free-disk-gb value Override sdk_min_free_disk_gb.
--engine-min-free-disk-gb value Override engine_min_free_disk_gb.
--base-flutter-engine-revision v Override base_flutter_engine_revision.
--skip-gclient-sync Dispatch with run_gclient_sync=false.
EOF
}
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO=""
REF=""
WORKFLOW="open-shorebird-ci.yml"
DOWNLOAD_DIR="hosted-full-sdk-artifacts"
TIMEOUT_MINUTES=720
POLL_SECONDS=30
LINUX_HEAVY_RUNNER=""
MACOS_HEAVY_RUNNER=""
SDK_MIN_FREE_DISK_GB=""
ENGINE_MIN_FREE_DISK_GB=""
BASE_FLUTTER_ENGINE_REVISION=""
RUN_GCLIENT_SYNC=true
GITHUB_API_URL="${GITHUB_API_URL:-https://api.github.com}"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--repo)
REPO="${2:-}"
shift 2
;;
--ref)
REF="${2:-}"
shift 2
;;
--workflow)
WORKFLOW="${2:-}"
shift 2
;;
--download-dir)
DOWNLOAD_DIR="${2:-}"
shift 2
;;
--timeout-minutes)
TIMEOUT_MINUTES="${2:-}"
shift 2
;;
--poll-seconds)
POLL_SECONDS="${2:-}"
shift 2
;;
--linux-heavy-runner)
LINUX_HEAVY_RUNNER="${2:-}"
shift 2
;;
--macos-heavy-runner)
MACOS_HEAVY_RUNNER="${2:-}"
shift 2
;;
--sdk-min-free-disk-gb)
SDK_MIN_FREE_DISK_GB="${2:-}"
shift 2
;;
--engine-min-free-disk-gb)
ENGINE_MIN_FREE_DISK_GB="${2:-}"
shift 2
;;
--base-flutter-engine-revision)
BASE_FLUTTER_ENGINE_REVISION="${2:-}"
shift 2
;;
--skip-gclient-sync)
RUN_GCLIENT_SYNC=false
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
usage
exit 64
;;
esac
done
if [[ -z "$REPO" ]]; then
echo "--repo owner/name is required" >&2
usage
exit 64
fi
if [[ -z "$REF" ]]; then
REF="$(git -C "$ROOT" branch --show-current 2>/dev/null || true)"
fi
if [[ -z "$REF" ]]; then
echo "--ref is required when the current checkout is detached" >&2
exit 64
fi
run_fields=(
-f full_sdk_build=true
-f run_gclient_sync="$RUN_GCLIENT_SYNC"
-f run_runtime_smokes=false
)
[[ -z "$LINUX_HEAVY_RUNNER" ]] || run_fields+=(-f linux_heavy_runner="$LINUX_HEAVY_RUNNER")
[[ -z "$MACOS_HEAVY_RUNNER" ]] || run_fields+=(-f macos_heavy_runner="$MACOS_HEAVY_RUNNER")
[[ -z "$SDK_MIN_FREE_DISK_GB" ]] || run_fields+=(-f sdk_min_free_disk_gb="$SDK_MIN_FREE_DISK_GB")
[[ -z "$ENGINE_MIN_FREE_DISK_GB" ]] || run_fields+=(-f engine_min_free_disk_gb="$ENGINE_MIN_FREE_DISK_GB")
[[ -z "$BASE_FLUTTER_ENGINE_REVISION" ]] || run_fields+=(-f base_flutter_engine_revision="$BASE_FLUTTER_ENGINE_REVISION")
api_token() {
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
printf '%s' "$GITHUB_TOKEN"
elif [[ -n "${GH_TOKEN:-}" ]]; then
printf '%s' "$GH_TOKEN"
else
echo "GITHUB_TOKEN or GH_TOKEN is required when gh is not installed" >&2
exit 69
fi
}
api_require_tool() {
local tool="$1"
if ! command -v "$tool" >/dev/null 2>&1; then
echo "$tool is required when gh is not installed" >&2
exit 69
fi
}
api_request() {
local method="$1"
local path="$2"
local data="${3:-}"
local token
token="$(api_token)"
local curl_args=(
-fsSL
-X "$method"
-H "Accept: application/vnd.github+json"
-H "Authorization: Bearer $token"
-H "X-GitHub-Api-Version: 2022-11-28"
)
if [[ -n "$data" ]]; then
curl_args+=(-H "Content-Type: application/json" -d "$data")
fi
curl "${curl_args[@]}" "$GITHUB_API_URL/repos/$REPO$path"
}
api_dispatch_payload() {
local inputs_filter
inputs_filter='{full_sdk_build: "true", run_gclient_sync: $run_gclient_sync, run_runtime_smokes: "false"}'
local jq_args=(
--arg ref "$REF"
--arg run_gclient_sync "$RUN_GCLIENT_SYNC"
)
if [[ -n "$LINUX_HEAVY_RUNNER" ]]; then
inputs_filter="$inputs_filter + {linux_heavy_runner: \$linux_heavy_runner}"
jq_args+=(--arg linux_heavy_runner "$LINUX_HEAVY_RUNNER")
fi
if [[ -n "$MACOS_HEAVY_RUNNER" ]]; then
inputs_filter="$inputs_filter + {macos_heavy_runner: \$macos_heavy_runner}"
jq_args+=(--arg macos_heavy_runner "$MACOS_HEAVY_RUNNER")
fi
if [[ -n "$SDK_MIN_FREE_DISK_GB" ]]; then
inputs_filter="$inputs_filter + {sdk_min_free_disk_gb: \$sdk_min_free_disk_gb}"
jq_args+=(--arg sdk_min_free_disk_gb "$SDK_MIN_FREE_DISK_GB")
fi
if [[ -n "$ENGINE_MIN_FREE_DISK_GB" ]]; then
inputs_filter="$inputs_filter + {engine_min_free_disk_gb: \$engine_min_free_disk_gb}"
jq_args+=(--arg engine_min_free_disk_gb "$ENGINE_MIN_FREE_DISK_GB")
fi
if [[ -n "$BASE_FLUTTER_ENGINE_REVISION" ]]; then
inputs_filter="$inputs_filter + {base_flutter_engine_revision: \$base_flutter_engine_revision}"
jq_args+=(--arg base_flutter_engine_revision "$BASE_FLUTTER_ENGINE_REVISION")
fi
jq -n "${jq_args[@]}" "{ref: \$ref, inputs: ($inputs_filter)}"
}
api_download_artifacts() {
rm -rf "$DOWNLOAD_DIR"
mkdir -p "$DOWNLOAD_DIR"
local page=1
local downloaded_count=0
while true; do
local response
response="$(api_request GET "/actions/runs/$run_id/artifacts?per_page=100&page=$page")"
local artifact_count
artifact_count="$(jq '.artifacts | length' <<<"$response")"
[[ "$artifact_count" == "0" ]] && break
while IFS= read -r artifact; do
local name
local url
name="$(jq -r '.name' <<<"$artifact")"
url="$(jq -r '.archive_download_url' <<<"$artifact")"
case "$name" in
""|*/*|*..*)
echo "unsafe GitHub artifact name: $name" >&2
exit 70
;;
esac
if [[ -z "$url" || "$url" == "null" ]]; then
echo "missing archive_download_url for artifact: $name" >&2
exit 70
fi
local artifact_dir
local zip_path
artifact_dir="$DOWNLOAD_DIR/$name"
zip_path="$(mktemp "${TMPDIR:-/tmp}/github-artifact.XXXXXX")"
curl \
-fsSL \
-L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $(api_token)" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-o "$zip_path" \
"$url"
rm -rf "$artifact_dir"
mkdir -p "$artifact_dir"
unzip -q "$zip_path" -d "$artifact_dir"
rm -f "$zip_path"
downloaded_count=$((downloaded_count + 1))
done < <(jq -c '.artifacts[] | select(.expired | not)' <<<"$response")
page=$((page + 1))
done
if [[ "$downloaded_count" -eq 0 ]]; then
echo "no non-expired artifacts were available for workflow run $run_id" >&2
exit 70
fi
}
use_gh=0
if command -v gh >/dev/null 2>&1; then
use_gh=1
else
api_require_tool curl
api_require_tool jq
api_require_tool unzip
api_token >/dev/null
fi
echo "Dispatching $WORKFLOW on $REPO@$REF with full_sdk_build=true"
start_epoch="$(date +%s)"
start_iso="$(date -u -r "$start_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "@$start_epoch" +"%Y-%m-%dT%H:%M:%SZ")"
if [[ "$use_gh" == "1" ]]; then
gh workflow run "$WORKFLOW" \
--repo "$REPO" \
--ref "$REF" \
"${run_fields[@]}"
else
api_request POST "/actions/workflows/$WORKFLOW/dispatches" "$(api_dispatch_payload)" >/dev/null
fi
run_id=""
for _ in {1..40}; do
if [[ "$use_gh" == "1" ]]; then
run_list_args=(
--repo "$REPO"
--workflow "$WORKFLOW"
--event workflow_dispatch
--json databaseId,createdAt
--limit 20
)
if [[ "$REF" =~ ^[0-9a-fA-F]{40}$ ]]; then
run_list_args+=(--commit "$REF")
else
run_list_args+=(--branch "$REF")
fi
run_id="$(
gh run list \
"${run_list_args[@]}" \
--jq ".[] | select(.createdAt >= \"$start_iso\") | .databaseId" \
|
head -n 1
)"
else
runs_response="$(api_request GET "/actions/workflows/$WORKFLOW/runs?event=workflow_dispatch&per_page=20")"
run_id="$(
jq -r \
--arg ref "$REF" \
--arg start_iso "$start_iso" \
'
.workflow_runs
| map(select(.created_at >= $start_iso))
| map(select(.head_branch == $ref or .head_sha == $ref))
| sort_by(.created_at)
| reverse
| .[0].id // ""
' \
<<<"$runs_response"
)"
fi
[[ -z "$run_id" ]] || break
sleep 3
done
if [[ -z "$run_id" ]]; then
echo "unable to find dispatched workflow run for $WORKFLOW on $REF" >&2
exit 70
fi
echo "Waiting for hosted full SDK run: $run_id"
deadline=$((start_epoch + TIMEOUT_MINUTES * 60))
run_head_sha=""
while true; do
if [[ "$use_gh" == "1" ]]; then
IFS=$'\t' read -r status conclusion url run_head_sha < <(
gh run view "$run_id" \
--repo "$REPO" \
--json status,conclusion,url,headSha \
--jq '[.status, (.conclusion // ""), .url, (.headSha // "")] | @tsv'
)
else
run_response="$(api_request GET "/actions/runs/$run_id")"
IFS=$'\t' read -r status conclusion url run_head_sha < <(
jq -r '[.status, (.conclusion // ""), .html_url, (.head_sha // "")] | @tsv' \
<<<"$run_response"
)
fi
echo "run $run_id status=$status conclusion=${conclusion:-null} url=$url"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" != "success" ]]; then
echo "hosted full SDK build failed: conclusion=$conclusion" >&2
exit 70
fi
break
fi
if [[ "$(date +%s)" -ge "$deadline" ]]; then
echo "timed out waiting for hosted full SDK build after $TIMEOUT_MINUTES minutes" >&2
exit 70
fi
sleep "$POLL_SECONDS"
done
if [[ "$use_gh" == "1" ]]; then
rm -rf "$DOWNLOAD_DIR"
mkdir -p "$DOWNLOAD_DIR"
gh run download "$run_id" --repo "$REPO" --dir "$DOWNLOAD_DIR"
else
api_download_artifacts
fi
if [[ -z "$run_head_sha" ]]; then
echo "unable to read headSha for workflow run $run_id" >&2
exit 70
fi
"$ROOT/scripts/verify_downloaded_release_artifacts.sh" \
--github-sha "$run_head_sha" \
"$DOWNLOAD_DIR"
echo "hosted full SDK build verified: $run_id"