diff --git a/scripts/migrate-gcs-arch.sh b/scripts/migrate-gcs-arch.sh new file mode 100755 index 000000000..533d33953 --- /dev/null +++ b/scripts/migrate-gcs-arch.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Copies flat-path firecracker binaries into amd64/ subdirectories in GCS. +# +# Before ARM64 support was added, builds were uploaded as: +# {version_name}/firecracker +# +# The arch-aware layout expects: +# {version_name}/amd64/firecracker +# +# This script finds flat-path binaries and copies them into the amd64/ subdir +# so the orchestrator's arch-based path resolution can find them. +# +# Usage: +# ./scripts/migrate-gcs-arch.sh # dry-run: show what would be copied +# ./scripts/migrate-gcs-arch.sh --apply # copy flat -> amd64/ +# +# Examples: +# ./scripts/migrate-gcs-arch.sh gs://e2b-staging-fc-versions +# ./scripts/migrate-gcs-arch.sh gs://e2b-prod-public-builds/firecrackers --apply + +set -euo pipefail + +GCS_PATH="${1:?Usage: $0 [--apply]}" +shift + +APPLY=false +for arg in "$@"; do + case "$arg" in + --apply) APPLY=true ;; + *) echo "Unknown flag: $arg"; exit 1 ;; + esac +done + +# Normalize: strip trailing slash +GCS_PATH="${GCS_PATH%/}" + +echo "Scanning ${GCS_PATH} for flat-path firecracker binaries..." +echo "" + +# Match only flat-path binaries: {version}/firecracker +# The single * does NOT match path separators, so this excludes +# {version}/{arch}/firecracker. +objects=$(gsutil ls "${GCS_PATH}/*/firecracker" 2>/dev/null || true) + +if [[ -z "$objects" ]]; then + echo "No flat-path firecracker binaries found in ${GCS_PATH}" + exit 0 +fi + +copied=0 +skipped=0 +while IFS= read -r src; do + [[ -z "$src" ]] && continue + + # Insert /amd64 before the filename: + # .../v1.10.1/firecracker -> .../v1.10.1/amd64/firecracker + dst="${src%/firecracker}/amd64/firecracker" + + # Skip if destination already exists + if gsutil -q stat "$dst" 2>/dev/null; then + echo " SKIP $dst (already exists)" + ((skipped++)) || true + continue + fi + + if [[ "$APPLY" == true ]]; then + echo " COPY $src" + echo " -> $dst" + gsutil cp "$src" "$dst" + else + echo " [dry-run] $src" + echo " -> $dst" + fi + ((copied++)) || true +done <<< "$objects" + +echo "" +echo "Total: $copied to copy, $skipped skipped (already exist)" +if [[ "$APPLY" != true && "$copied" -gt 0 ]]; then + echo "" + echo "This was a dry run. Add --apply to actually copy." +fi diff --git a/scripts/upload-release-to-gcs.sh b/scripts/upload-release-to-gcs.sh new file mode 100755 index 000000000..0c7c8d5f5 --- /dev/null +++ b/scripts/upload-release-to-gcs.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Uploads firecracker-{amd64,arm64} assets from a fc-versions GitHub release +# to GCS at: +# ///firecracker +# //firecracker (legacy amd64 copy) +# +# Existing objects are never overwritten. +# +# Usage: +# ./scripts/upload-release-to-gcs.sh --tag --bucket [--dry-run] [--repo ] +# +# Options: +# --tag Release tag / version name (e.g. v1.14.1_af9c995). +# --bucket Target bucket (with optional path prefix), e.g. +# my-bucket +# my-bucket/firecrackers +# gs://my-bucket/firecrackers +# --repo GitHub repo (default: e2b-dev/fc-versions). +# --dry-run Print what would be uploaded without writing. +# -h, --help Show this help. + +set -euo pipefail + +REPO="e2b-dev/fc-versions" +TAG="" +BUCKET="" +DRY_RUN=false + +usage() { sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-0}"; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --tag) TAG="${2:?--tag needs a value}"; shift 2 ;; + --bucket) BUCKET="${2:?--bucket needs a value}"; shift 2 ;; + --repo) REPO="${2:?--repo needs a value}"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + -h|--help) usage 0 ;; + *) echo "Unknown argument: $1" >&2; usage 1 ;; + esac +done + +[[ -n "$TAG" ]] || { echo "ERROR: --tag is required" >&2; usage 1; } +[[ -n "$BUCKET" ]] || { echo "ERROR: --bucket is required" >&2; usage 1; } + +command -v gh >/dev/null || { echo "ERROR: gh CLI not found" >&2; exit 1; } +command -v gcloud >/dev/null || { echo "ERROR: gcloud CLI not found" >&2; exit 1; } + +BUCKET="${BUCKET#gs://}" +BUCKET="${BUCKET%/}" +BUCKET_URI="gs://${BUCKET}" + +if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + echo "ERROR: release '$TAG' not found in $REPO" >&2 + exit 1 +fi + +echo "Release: $TAG" +echo "Target: ${BUCKET_URI}" +$DRY_RUN && echo "Mode: dry-run" + +ASSETS=() +while IFS= read -r line || [[ -n "$line" ]]; do + [[ -n "$line" ]] && ASSETS+=("$line") +done < <(gh release view "$TAG" --repo "$REPO" --json assets \ + --jq '.assets[].name') + +if [[ "${#ASSETS[@]}" -eq 0 ]]; then + echo "ERROR: release $TAG has no assets" >&2 + exit 1 +fi + +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +uploaded=0 +skipped=0 +for asset in "${ASSETS[@]}"; do + if [[ "$asset" =~ ^firecracker-(amd64|arm64)$ ]]; then + arch="${BASH_REMATCH[1]}" + dst="${BUCKET_URI}/${TAG}/${arch}/firecracker" + elif [[ "$asset" == "firecracker" ]]; then + dst="${BUCKET_URI}/${TAG}/firecracker" + else + continue + fi + + if gcloud storage ls "$dst" >/dev/null 2>&1; then + echo " EXISTS $dst" + skipped=$((skipped + 1)) + continue + fi + + if $DRY_RUN; then + echo " WOULD $asset -> $dst" + uploaded=$((uploaded + 1)) + continue + fi + + echo " UPLOAD $asset -> $dst" + gh release download "$TAG" --repo "$REPO" \ + --pattern "$asset" --dir "$TMP_DIR" --clobber >/dev/null + gcloud storage cp "$TMP_DIR/$asset" "$dst" + rm -f "$TMP_DIR/$asset" + uploaded=$((uploaded + 1)) +done + +echo "" +if $DRY_RUN; then + echo "Dry run complete. Would upload: $uploaded, already in GCS: $skipped." +else + echo "Done. Uploaded: $uploaded, already in GCS: $skipped." +fi