Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions .github/workflows/bump-homebrew.yml
Original file line number Diff line number Diff line change
@@ -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:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Extract version from tag
id: version
env:
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
# Validate tag format: cli-v<semver>. 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"
Comment on lines +25 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Script injection via tag name

${{ github.event.release.tag_name }} is interpolated directly into the shell script by the Actions runner before the shell parses it. If a tag name were crafted with shell metacharacters (e.g. a backtick, $(...), or a double-quote), it could execute arbitrary code in the runner. GitHub's own security hardening guide explicitly flags this pattern as a script-injection vector.

The fix is to pass context values through an env: block so they arrive as environment variables — not as literal text spliced into the script:

Suggested change
run: |
TAG="${{ github.event.release.tag_name }}"
VERSION="${TAG#cli-v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Extract version from tag
id: version
env:
TAG: ${{ github.event.release.tag_name }}
run: |
VERSION="${TAG#cli-v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

This change is low-risk (git tags are restricted in practice) but it's a well-established best practice and keeps the workflow consistent with the shas step below, which already uses env: correctly.


- 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- 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
153 changes: 153 additions & 0 deletions apps/marketing/public/cli/install.sh
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
tmp="$(mktemp -t superset-install.XXXXXX)"
if ! curl -fsSL -o "$tmp" "$url"; then
rm -f "$tmp"
error "Failed to download $url"
fi
echo "$tmp"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 <your-profile>\`) for the PATH to take effect.\n"
}

main "$@"
10 changes: 3 additions & 7 deletions packages/cli/scripts/build-dist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,9 @@ async function main(): Promise<void> {

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-<target>/ wrapper).
await exec("tar", ["-czf", tarball, "-C", stagingRoot, "."]);

console.log(`[build-dist] done: ${tarball}`);
}
Expand Down
Loading