diff --git a/.github/workflows/bump-homebrew.yml b/.github/workflows/bump-homebrew.yml new file mode 100644 index 00000000000..55aaccbfb36 --- /dev/null +++ b/.github/workflows/bump-homebrew.yml @@ -0,0 +1,137 @@ +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] + +# 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') + runs-on: ubuntu-latest + steps: + - name: Extract version from tag + id: version + env: + TAG: ${{ github.event.release.tag_name }} + run: | + 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" + + - name: Compute SHA256 for each tarball + id: shas + 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" + 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 + + - name: Checkout homebrew-tap + uses: actions/checkout@v4 + with: + repository: superset-sh/homebrew-tap + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: homebrew-tap + + - 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: | + set -euo pipefail + python3 - <<'PYEOF' > homebrew-tap/Formula/superset.rb + import os + template = '''class Superset < Formula + desc "CLI and host-service for Superset" + homepage "https://superset.sh" + 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}" + end + on_intel do + 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}" + end + end + + def install + libexec.install Dir["*"] + bin.install_symlink libexec/"bin/superset" + bin.install_symlink libexec/"bin/superset-host" + end + + test do + assert_match "superset", shell_output("#{{bin}}/superset --version") + end + end + ''' + 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" + git add Formula/superset.rb + if git diff --cached --quiet; then + echo "No changes to formula" + exit 0 + fi + git commit -m "superset ${VERSION}" + + # 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 diff --git a/apps/marketing/public/cli/install.sh b/apps/marketing/public/cli/install.sh new file mode 100755 index 00000000000..87ae5765c07 --- /dev/null +++ b/apps/marketing/public/cli/install.sh @@ -0,0 +1,153 @@ +#!/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" >&2; } +warn() { printf "${YELLOW}warning:${RESET} %s\n" "$1" >&2; } +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" + + # 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 [ ! -f "$path" ] || [ ! -x "$path" ]; then + error "Expected executable file not found: $path" + fi + done + + 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}`); }