Zum Inhalt

pipeline/scripts/fetch_brush.sh

#!/usr/bin/env bash
# Fetch the Brush Mac-Silicon binary from GitHub releases.
# Spec §12.1 decision: pin to a known-good version, override via BRUSH_VERSION env.

set -euo pipefail

# Pin to a known-good release. Override via env var:
#   BRUSH_VERSION=v0.1.5 ./scripts/fetch_brush.sh
readonly DEFAULT_BRUSH_VERSION="latest"
readonly BRUSH_VERSION="${BRUSH_VERSION:-${DEFAULT_BRUSH_VERSION}}"

readonly BRUSH_REPO="ArthurBrussee/brush"
readonly INSTALL_DIR="${BRUSH_INSTALL_DIR:-${HOME}/AutoSplat/bin}"
readonly INSTALL_PATH="${INSTALL_DIR}/brush"
readonly VERSION_FILE="${INSTALL_DIR}/.brush-version"

log() { printf '\033[1;36m[fetch_brush]\033[0m %s\n' "$*"; }
fail() { printf '\033[1;31m[fetch_brush]\033[0m %s\n' "$*" >&2; exit 1; }

if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then
    fail "Brush binary requires macOS / Apple Silicon (detected $(uname -s)/$(uname -m))"
fi

mkdir -p "${INSTALL_DIR}"

# Resolve the actual release tag (so 'latest' becomes a concrete version).
# Note: we deliberately don't pipe `curl` straight into `grep -m1` here because
# pipefail + SIGPIPE on early-exit grep causes curl to exit non-zero, which
# breaks the resolve step on perfectly healthy responses.
if [[ "${BRUSH_VERSION}" == "latest" ]]; then
    log "Resolving latest release tag..."
    latest_json=$(curl -fsSL "https://api.github.com/repos/${BRUSH_REPO}/releases/latest") \
        || fail "GitHub API request failed"
    resolved_tag=$(printf '%s' "${latest_json}" \
        | grep '"tag_name":' \
        | head -1 \
        | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
    [[ -z "${resolved_tag}" ]] && fail "Could not parse tag_name from API response"
else
    resolved_tag="${BRUSH_VERSION}"
fi

log "Using Brush version: ${resolved_tag}"

# Check if already at correct version
if [[ -f "${VERSION_FILE}" && -f "${INSTALL_PATH}" ]]; then
    current="$(cat "${VERSION_FILE}")"
    if [[ "${current}" == "${resolved_tag}" ]]; then
        log "Brush ${resolved_tag} already installed at ${INSTALL_PATH}"
        exit 0
    fi
fi

# Attempt to find the Mac-Silicon asset. Brush release naming is upstream-dependent.
# We probe a few plausible patterns and fall back to an interactive note.
log "Listing release assets..."
assets_json=$(curl -fsSL "https://api.github.com/repos/${BRUSH_REPO}/releases/tags/${resolved_tag}") \
    || fail "Could not fetch release ${resolved_tag}"

asset_url=$(printf '%s' "${assets_json}" \
    | grep '"browser_download_url":' \
    | grep -iE 'mac|darwin|aarch64|arm64' \
    | head -1 \
    | sed -E 's/.*"browser_download_url": *"([^"]+)".*/\1/' || true)

if [[ -z "${asset_url}" ]]; then
    cat >&2 <<EOF

[fetch_brush] Could not auto-detect a Mac-Silicon asset for ${resolved_tag}.

This usually means Brush's release naming changed. Manual fallback:
  1. Open https://github.com/${BRUSH_REPO}/releases/tag/${resolved_tag}
  2. Download the macOS / arm64 binary
  3. Move it to: ${INSTALL_PATH}
  4. Make it executable: chmod +x ${INSTALL_PATH}
  5. Record version: echo "${resolved_tag}" > ${VERSION_FILE}

EOF
    exit 4
fi

log "Downloading: ${asset_url}"
tmp_file="$(mktemp)"
curl -fsSL "${asset_url}" -o "${tmp_file}"

# Handle compressed archives heuristically.
# Note: Brush 0.3.0+ ships as `.tar.xz` with `brush_app.app/Contents/MacOS/brush_app`.
extract_and_install_binary() {
    local archive_dir="$1"
    # Prefer an executable named brush or brush_app, anywhere in the extracted tree.
    local bin
    bin=$(find "${archive_dir}" -type f \( -name 'brush_app' -o -name 'brush' \) -perm -u+x | head -1 || true)
    if [[ -z "${bin}" ]]; then
        # Fall back to any file named brush_app or brush (may need chmod after move)
        bin=$(find "${archive_dir}" -type f \( -name 'brush_app' -o -name 'brush' \) | head -1 || true)
    fi
    [[ -z "${bin}" ]] && fail "No 'brush' or 'brush_app' binary in archive"
    mv "${bin}" "${INSTALL_PATH}"
}

case "${asset_url}" in
    *.tar.gz|*.tgz)
        tmp_dir="$(mktemp -d)"
        tar -xzf "${tmp_file}" -C "${tmp_dir}"
        extract_and_install_binary "${tmp_dir}"
        rm -rf "${tmp_dir}"
        rm -f "${tmp_file}"
        ;;
    *.tar.xz|*.txz)
        tmp_dir="$(mktemp -d)"
        tar -xJf "${tmp_file}" -C "${tmp_dir}"
        extract_and_install_binary "${tmp_dir}"
        rm -rf "${tmp_dir}"
        rm -f "${tmp_file}"
        ;;
    *.zip)
        tmp_dir="$(mktemp -d)"
        unzip -q "${tmp_file}" -d "${tmp_dir}"
        extract_and_install_binary "${tmp_dir}"
        rm -rf "${tmp_dir}"
        rm -f "${tmp_file}"
        ;;
    *)
        # Assume the asset itself is the binary
        mv "${tmp_file}" "${INSTALL_PATH}"
        ;;
esac

chmod +x "${INSTALL_PATH}"
printf '%s\n' "${resolved_tag}" > "${VERSION_FILE}"

log "Installed Brush ${resolved_tag} → ${INSTALL_PATH}"
"${INSTALL_PATH}" --version || log "Note: --version probe failed, binary may still be functional"