From 7d566778ae21ff1b560853e54e7366c94730d435 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 9 Apr 2026 13:45:33 -0700 Subject: [PATCH 1/5] ci: auto-bump Homebrew formula on CLI release When a cli-v* tag is published as a GitHub Release: 1. Compute SHA256 for each platform tarball 2. Rewrite Formula/superset.rb with the new version and SHAs 3. Push to superset-sh/homebrew-tap Requires a HOMEBREW_TAP_TOKEN secret (GitHub PAT with repo access to superset-sh/homebrew-tap). --- .github/workflows/bump-homebrew.yml | 105 ++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/bump-homebrew.yml diff --git a/.github/workflows/bump-homebrew.yml b/.github/workflows/bump-homebrew.yml new file mode 100644 index 00000000000..7aa89d5df00 --- /dev/null +++ b/.github/workflows/bump-homebrew.yml @@ -0,0 +1,105 @@ +name: Bump Homebrew Formula + +# Triggers when a CLI release (tag matching cli-v*) is published, computes +# SHA256 for each platform tarball, and pushes an updated formula to the +# superset-sh/homebrew-tap repository. + +on: + release: + types: [published] + +jobs: + bump: + if: startsWith(github.event.release.tag_name, 'cli-v') + runs-on: ubuntu-latest + steps: + - name: Extract version from tag + id: version + run: | + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#cli-v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Compute SHA256 for each tarball + id: shas + env: + TAG: ${{ steps.version.outputs.tag }} + run: | + for target in darwin-arm64 darwin-x64 linux-x64; do + url="https://github.com/superset-sh/superset/releases/download/${TAG}/superset-${target}.tar.gz" + echo "Fetching SHA for $url" + sha=$(curl -fsSL "$url" | shasum -a 256 | awk '{print $1}') + if [ -z "$sha" ]; then + echo "::error::Failed to compute SHA for $target" + exit 1 + fi + echo "${target//-/_}_sha=$sha" >> "$GITHUB_OUTPUT" + done + + - name: Checkout homebrew-tap + uses: actions/checkout@v4 + with: + repository: superset-sh/homebrew-tap + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: homebrew-tap + + - name: Update formula + env: + VERSION: ${{ steps.version.outputs.version }} + DARWIN_ARM64_SHA: ${{ steps.shas.outputs.darwin_arm64_sha }} + DARWIN_X64_SHA: ${{ steps.shas.outputs.darwin_x64_sha }} + LINUX_X64_SHA: ${{ steps.shas.outputs.linux_x64_sha }} + run: | + cd homebrew-tap + cat > Formula/superset.rb < Date: Thu, 9 Apr 2026 13:29:12 -0700 Subject: [PATCH 2/5] feat: add install script at superset.sh/cli/install.sh One-liner install for the CLI distribution: curl -fsSL https://superset.sh/cli/install.sh | sh - Detects platform/arch (macOS arm64/x64, Linux x64) - Downloads latest release tarball from GitHub - Extracts to ~/superset/ (or $SUPERSET_HOME) - Adds ~/superset/bin to PATH in the user's shell profile - Respects SUPERSET_VERSION for pinned installs Also: tarball no longer has a top-level superset-/ wrapper, so `tar -xzf ... -C ~/superset` drops contents directly. --- apps/marketing/public/cli/install.sh | 146 +++++++++++++++++++++++++++ packages/cli/scripts/build-dist.ts | 10 +- 2 files changed, 149 insertions(+), 7 deletions(-) create mode 100755 apps/marketing/public/cli/install.sh diff --git a/apps/marketing/public/cli/install.sh b/apps/marketing/public/cli/install.sh new file mode 100755 index 00000000000..d482be8ea82 --- /dev/null +++ b/apps/marketing/public/cli/install.sh @@ -0,0 +1,146 @@ +#!/bin/sh +# Superset CLI installer +# +# Usage: +# curl -fsSL https://superset.sh/cli/install.sh | sh +# +# Installs the Superset CLI and host-service to ~/superset/. +# Adds ~/superset/bin to PATH via your shell profile. + +set -eu + +REPO="superset-sh/superset" +INSTALL_DIR="${SUPERSET_HOME:-$HOME/superset}" +TAG="${SUPERSET_VERSION:-latest}" + +BOLD='\033[1m' +GREEN='\033[32m' +YELLOW='\033[33m' +RED='\033[31m' +RESET='\033[0m' + +info() { printf "${GREEN}==>${RESET} %s\n" "$1"; } +warn() { printf "${YELLOW}warning:${RESET} %s\n" "$1"; } +error() { printf "${RED}error:${RESET} %s\n" "$1" >&2; exit 1; } + +detect_target() { + os="$(uname -s)" + arch="$(uname -m)" + + case "$os" in + Darwin) + case "$arch" in + arm64) echo "darwin-arm64" ;; + x86_64) echo "darwin-x64" ;; + *) error "Unsupported macOS architecture: $arch" ;; + esac + ;; + Linux) + case "$arch" in + x86_64) echo "linux-x64" ;; + *) error "Unsupported Linux architecture: $arch (only x64 is supported)" ;; + esac + ;; + *) + error "Unsupported OS: $os (only macOS and Linux are supported)" + ;; + esac +} + +download_tarball() { + target="$1" + tarball="superset-${target}.tar.gz" + + if [ "$TAG" = "latest" ]; then + url="https://github.com/${REPO}/releases/latest/download/${tarball}" + else + url="https://github.com/${REPO}/releases/download/${TAG}/${tarball}" + fi + + info "Downloading $url" + tmp="$(mktemp -t superset-install.XXXXXX)" + if ! curl -fsSL -o "$tmp" "$url"; then + rm -f "$tmp" + error "Failed to download $url" + fi + echo "$tmp" +} + +extract_tarball() { + tarball="$1" + info "Extracting to $INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + tar -xzf "$tarball" -C "$INSTALL_DIR" + rm -f "$tarball" +} + +detect_shell_profile() { + case "${SHELL:-}" in + */zsh) echo "$HOME/.zshrc" ;; + */bash) + if [ -f "$HOME/.bash_profile" ]; then + echo "$HOME/.bash_profile" + else + echo "$HOME/.bashrc" + fi + ;; + */fish) echo "$HOME/.config/fish/config.fish" ;; + *) echo "" ;; + esac +} + +update_path() { + bin_dir="$INSTALL_DIR/bin" + + # Check if it's already in PATH + case ":$PATH:" in + *":$bin_dir:"*) + info "$bin_dir is already in PATH" + return + ;; + esac + + profile="$(detect_shell_profile)" + if [ -z "$profile" ]; then + warn "Could not detect your shell profile. Add this to your shell config:" + printf " export PATH=\"%s:\$PATH\"\n" "$bin_dir" + return + fi + + export_line="export PATH=\"$bin_dir:\$PATH\"" + if [ "$profile" = "$HOME/.config/fish/config.fish" ]; then + export_line="set -gx PATH $bin_dir \$PATH" + fi + + if [ -f "$profile" ] && grep -Fq "$bin_dir" "$profile"; then + info "PATH already configured in $profile" + return + fi + + info "Adding $bin_dir to PATH in $profile" + mkdir -p "$(dirname "$profile")" + { + printf "\n# Superset CLI\n" + printf "%s\n" "$export_line" + } >> "$profile" +} + +main() { + printf "${BOLD}Installing Superset CLI${RESET}\n\n" + + target="$(detect_target)" + info "Platform: $target" + + tarball="$(download_tarball "$target")" + extract_tarball "$tarball" + + chmod +x "$INSTALL_DIR/bin/superset" "$INSTALL_DIR/bin/superset-host" 2>/dev/null || true + + update_path + + printf "\n${GREEN}${BOLD}Installed!${RESET}\n" + printf "Run ${BOLD}superset auth login${RESET} to get started.\n" + printf "You may need to restart your shell (or run \`source \`) for the PATH to take effect.\n" +} + +main "$@" diff --git a/packages/cli/scripts/build-dist.ts b/packages/cli/scripts/build-dist.ts index a56fdda250d..7bed3d4c383 100644 --- a/packages/cli/scripts/build-dist.ts +++ b/packages/cli/scripts/build-dist.ts @@ -277,13 +277,9 @@ async function main(): Promise { const tarball = join(cliDir, "dist", `superset-${target}.tar.gz`); console.log(`[build-dist] creating ${tarball}`); - await exec("tar", [ - "-czf", - tarball, - "-C", - dirname(stagingRoot), - `superset-${target}`, - ]); + // Tar from inside the staging dir so contents extract directly to the + // install target (no top-level superset-/ wrapper). + await exec("tar", ["-czf", tarball, "-C", stagingRoot, "."]); console.log(`[build-dist] done: ${tarball}`); } From 9a0fcc8187197c0400d64552fc0a602c7e7ef66c Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 9 Apr 2026 13:52:50 -0700 Subject: [PATCH 3/5] fix: address PR review findings on bump-homebrew workflow - Add set -euo pipefail to all run blocks - Curl to tempfile + check exit status instead of piping to shasum (pipes without pipefail mask curl failures and produce the empty-input SHA256) - Inject tag via env: instead of direct ${{ }} interpolation to prevent script injection via malicious tag names. Validate tag format with regex before use. - Render formula via python3 reading from env, eliminating YAML+shell heredoc indentation pitfalls - Add concurrency group to serialize concurrent release publishes - Retry push once with rebase as belt-and-suspenders --- .github/workflows/bump-homebrew.yml | 68 +++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/.github/workflows/bump-homebrew.yml b/.github/workflows/bump-homebrew.yml index 7aa89d5df00..55aaccbfb36 100644 --- a/.github/workflows/bump-homebrew.yml +++ b/.github/workflows/bump-homebrew.yml @@ -8,6 +8,11 @@ on: release: types: [published] +# Serialize runs so two concurrent releases can't race and drop a bump. +concurrency: + group: homebrew-tap-formula-bump + cancel-in-progress: false + jobs: bump: if: startsWith(github.event.release.tag_name, 'cli-v') @@ -15,8 +20,15 @@ jobs: steps: - name: Extract version from tag id: version + env: + TAG: ${{ github.event.release.tag_name }} run: | - TAG="${{ github.event.release.tag_name }}" + set -euo pipefail + # Validate tag format: cli-v. Rejects tags with shell metacharacters. + if ! printf '%s' "$TAG" | grep -Eq '^cli-v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$'; then + echo "::error::Invalid tag format: $TAG" + exit 1 + fi VERSION="${TAG#cli-v}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT" @@ -26,14 +38,18 @@ jobs: env: TAG: ${{ steps.version.outputs.tag }} run: | + set -euo pipefail for target in darwin-arm64 darwin-x64 linux-x64; do url="https://github.com/superset-sh/superset/releases/download/${TAG}/superset-${target}.tar.gz" echo "Fetching SHA for $url" - sha=$(curl -fsSL "$url" | shasum -a 256 | awk '{print $1}') - if [ -z "$sha" ]; then - echo "::error::Failed to compute SHA for $target" + tmp=$(mktemp) + if ! curl -fsSL -o "$tmp" "$url"; then + echo "::error::Failed to download $url" + rm -f "$tmp" exit 1 fi + sha=$(shasum -a 256 "$tmp" | awk '{print $1}') + rm -f "$tmp" echo "${target//-/_}_sha=$sha" >> "$GITHUB_OUTPUT" done @@ -44,36 +60,37 @@ jobs: token: ${{ secrets.HOMEBREW_TAP_TOKEN }} path: homebrew-tap - - name: Update formula + - name: Render formula env: VERSION: ${{ steps.version.outputs.version }} DARWIN_ARM64_SHA: ${{ steps.shas.outputs.darwin_arm64_sha }} DARWIN_X64_SHA: ${{ steps.shas.outputs.darwin_x64_sha }} LINUX_X64_SHA: ${{ steps.shas.outputs.linux_x64_sha }} run: | - cd homebrew-tap - cat > Formula/superset.rb < homebrew-tap/Formula/superset.rb + import os + template = '''class Superset < Formula desc "CLI and host-service for Superset" homepage "https://superset.sh" - version "${VERSION}" + version "{version}" license "MIT" on_macos do on_arm do - url "https://github.com/superset-sh/superset/releases/download/cli-v#{version}/superset-darwin-arm64.tar.gz" - sha256 "${DARWIN_ARM64_SHA}" + url "https://github.com/superset-sh/superset/releases/download/cli-v#{{version}}/superset-darwin-arm64.tar.gz" + sha256 "{darwin_arm64}" end on_intel do - url "https://github.com/superset-sh/superset/releases/download/cli-v#{version}/superset-darwin-x64.tar.gz" - sha256 "${DARWIN_X64_SHA}" + url "https://github.com/superset-sh/superset/releases/download/cli-v#{{version}}/superset-darwin-x64.tar.gz" + sha256 "{darwin_x64}" end end on_linux do on_intel do - url "https://github.com/superset-sh/superset/releases/download/cli-v#{version}/superset-linux-x64.tar.gz" - sha256 "${LINUX_X64_SHA}" + url "https://github.com/superset-sh/superset/releases/download/cli-v#{{version}}/superset-linux-x64.tar.gz" + sha256 "{linux_x64}" end end @@ -84,15 +101,23 @@ jobs: end test do - assert_match "superset", shell_output("#{bin}/superset --version") + assert_match "superset", shell_output("#{{bin}}/superset --version") end end - EOF + ''' + print(template.format( + version=os.environ["VERSION"], + darwin_arm64=os.environ["DARWIN_ARM64_SHA"], + darwin_x64=os.environ["DARWIN_X64_SHA"], + linux_x64=os.environ["LINUX_X64_SHA"], + ), end="") + PYEOF - name: Commit and push env: VERSION: ${{ steps.version.outputs.version }} run: | + set -euo pipefail cd homebrew-tap git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -102,4 +127,11 @@ jobs: exit 0 fi git commit -m "superset ${VERSION}" - git push + + # Retry once on non-fast-forward in case the remote moved between + # checkout and push (shouldn't happen with the concurrency group, + # but belt-and-suspenders). + if ! git push; then + git pull --rebase + git push + fi From 17794c3431873e860b5fb57dd445c461954cd8fd Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 9 Apr 2026 14:05:13 -0700 Subject: [PATCH 4/5] fix: redirect install.sh info/warn to stderr and verify binaries info() wrote to stdout, corrupting command substitution in tarball="$(download_tarball ...)". Redirect both info and warn to stderr. Replace silent `chmod ... || true` with an executable verification loop so a broken tarball fails loudly. --- apps/marketing/public/cli/install.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/marketing/public/cli/install.sh b/apps/marketing/public/cli/install.sh index d482be8ea82..cc26ae2edca 100755 --- a/apps/marketing/public/cli/install.sh +++ b/apps/marketing/public/cli/install.sh @@ -19,8 +19,8 @@ YELLOW='\033[33m' RED='\033[31m' RESET='\033[0m' -info() { printf "${GREEN}==>${RESET} %s\n" "$1"; } -warn() { printf "${YELLOW}warning:${RESET} %s\n" "$1"; } +info() { printf "${GREEN}==>${RESET} %s\n" "$1" >&2; } +warn() { printf "${YELLOW}warning:${RESET} %s\n" "$1" >&2; } error() { printf "${RED}error:${RESET} %s\n" "$1" >&2; exit 1; } detect_target() { @@ -134,7 +134,14 @@ main() { tarball="$(download_tarball "$target")" extract_tarball "$tarball" - chmod +x "$INSTALL_DIR/bin/superset" "$INSTALL_DIR/bin/superset-host" 2>/dev/null || true + # Verify binaries exist and are executable. Tarball already ships them + # with +x, so this is a sanity check, not a chmod fallback. + for bin in superset superset-host; do + path="$INSTALL_DIR/bin/$bin" + if [ ! -x "$path" ]; then + error "Expected executable not found: $path" + fi + done update_path From 6b1340fee772241db7a7897c2f2f75bb71d9b293 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 9 Apr 2026 14:11:43 -0700 Subject: [PATCH 5/5] fix: require regular file in install.sh binary check --- apps/marketing/public/cli/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/marketing/public/cli/install.sh b/apps/marketing/public/cli/install.sh index cc26ae2edca..87ae5765c07 100755 --- a/apps/marketing/public/cli/install.sh +++ b/apps/marketing/public/cli/install.sh @@ -138,8 +138,8 @@ main() { # with +x, so this is a sanity check, not a chmod fallback. for bin in superset superset-host; do path="$INSTALL_DIR/bin/$bin" - if [ ! -x "$path" ]; then - error "Expected executable not found: $path" + if [ ! -f "$path" ] || [ ! -x "$path" ]; then + error "Expected executable file not found: $path" fi done