Files
o11y-foundry/scripts/foundry.sh
T
NageshbansalandGitHub 73ad12dd3c feat(scripts): add install.sh for foundryctl (#112)
## Summary

- Adds `scripts/install.sh` : a curl-pipe-bash installer for foundryctl,
served from `signoz.io/signoz.sh` (Vercel rewrite to this repo's
`main`).
- Detects OS/arch (darwin, linux, windows shells; amd64, arm64),
resolves the latest version via the GitHub `/releases/latest` redirect
(no API rate-limit), downloads the tarball + verifies SHA-256 against
the published checksums file, installs into `$FOUNDRY_INSTALL_DIR` →
`$XDG_BIN_HOME` → `~/.local/bin`, and runs the binary as a post-install
smoke test.
- Idempotent: same-version re-runs short-circuit early;
different-version runs prompt interactively, auto-proceed when piped
(curl-pipe-bash, CI). `-y` / `FOUNDRY_ASSUME_YES` bypasses the prompt
explicitly.
- Wires `sh-fmt` and `sh-lint` jobs into `ci.yaml` (via
`signoz/primus.workflows`) so the script is enforced against shellcheck
and shfmt on every PR.

Related: SigNoz/platform-pod#2065,
https://github.com/SigNoz/foundry/issues/96,
https://github.com/SigNoz/foundry/issues/97
2026-05-04 16:48:14 +05:30

351 lines
9.9 KiB
Bash
Executable File

#!/bin/bash
# foundry.sh - Downloads, verifies, and installs the foundryctl binary
# from GitHub releases.
#
# Usage:
# curl -fsSL https://signoz.io/foundry.sh | bash
# curl -fsSL https://signoz.io/foundry.sh | FOUNDRY_VERSION=v0.1.4 bash
# bash foundry.sh -v v0.1.4
# bash foundry.sh -d /usr/local/bin
# bash foundry.sh -h
#
# OPTIONS:
# -v <version> Version to install (e.g. v0.1.4). Default: latest.
# -d <path> Install directory. Default: $XDG_BIN_HOME or ~/.local/bin.
# -y Auto-confirm upgrade prompt.
# -h Show help message.
#
# Environment:
# FOUNDRY_VERSION Equivalent to -v.
# FOUNDRY_INSTALL_DIR Equivalent to -d.
# FOUNDRY_ASSUME_YES Equivalent to -y. Set to "true" to enable.
# NO_COLOR When set, disables ANSI color output (https://no-color.org).
set -euo pipefail
# Constants.
readonly NAME="foundry.sh"
readonly REPO="SigNoz/foundry"
readonly BINARY="foundryctl"
# User input (env vars; flags overwrite these in the getopts loop below).
FOUNDRY_VERSION="${FOUNDRY_VERSION:-}"
FOUNDRY_INSTALL_DIR="${FOUNDRY_INSTALL_DIR:-}"
FOUNDRY_ASSUME_YES="${FOUNDRY_ASSUME_YES:-false}"
# https://no-color.org honoured; auto-stripped when stderr is not a TTY.
if [[ -t 2 ]] && [[ -z "${NO_COLOR:-}" ]]; then
readonly C_INFO=$'\033[32;1m'
readonly C_WARN=$'\033[33;1m'
readonly C_ERROR=$'\033[31;1m'
readonly C_RESET=$'\033[0m'
else
readonly C_INFO=""
readonly C_WARN=""
readonly C_ERROR=""
readonly C_RESET=""
fi
info() {
echo "${C_INFO}[INFO]${C_RESET} $*"
}
warn() {
echo "${C_WARN}[WARN]${C_RESET} $*" >&2
}
err() {
echo "${C_ERROR}[ERROR]${C_RESET} $*" >&2
}
die() {
err "$*"
exit 1
}
help() {
printf "NAME\n"
printf "\t%s - Install %s, the SigNoz Foundry CLI\n\n" "${NAME}" "${BINARY}"
printf "USAGE\n"
printf "\t%s [-v version] [-d directory] [-y] [-h]\n\n" "${NAME}"
printf "DESCRIPTION\n"
printf "\tDownloads, verifies, and installs the %s binary from GitHub releases.\n\n" "${BINARY}"
printf "OPTIONS\n"
printf "\t-v <version>\tVersion to install (e.g. v0.1.4). [env: FOUNDRY_VERSION] [default: latest]\n"
printf "\t-d <path>\tInstall directory. [env: FOUNDRY_INSTALL_DIR] [default: \$XDG_BIN_HOME or ~/.local/bin]\n"
printf "\t-y\t\tAuto-confirm upgrade prompt. [env: FOUNDRY_ASSUME_YES]\n"
printf "\t-h\t\tShow this help message.\n\n"
printf "EXAMPLES\n"
printf "\t%s -v v0.1.4\n" "${NAME}"
printf "\t%s -d /usr/local/bin\n" "${NAME}"
printf "\tcurl -fsSL https://signoz.io/foundry.sh | bash\n"
printf "\tcurl -fsSL https://signoz.io/foundry.sh | FOUNDRY_VERSION=v0.1.4 bash\n"
}
# Sets PLATFORM_* (OS, ARCH, BIN_SUFFIX); Windows shells map to OS=windows and .exe suffix.
init_platform() {
local raw_arch raw_os
raw_arch="$(uname -m)"
case "${raw_arch}" in
x86_64 | amd64) PLATFORM_ARCH="amd64" ;;
aarch64 | arm64) PLATFORM_ARCH="arm64" ;;
*) die "Unsupported architecture: ${raw_arch}" ;;
esac
raw_os="$(uname | tr '[:upper:]' '[:lower:]')"
PLATFORM_BIN_SUFFIX=""
case "${raw_os}" in
darwin | linux) PLATFORM_OS="${raw_os}" ;;
mingw* | cygwin* | msys*)
PLATFORM_OS="windows"
PLATFORM_BIN_SUFFIX=".exe"
;;
*) die "Unsupported operating system: ${raw_os}" ;;
esac
}
# Sets HAS_CURL/HAS_WGET and SHA256_CMD for downstream reuse.
verify_prereqs() {
HAS_CURL="$(command -v curl >/dev/null 2>&1 && echo true || echo false)"
HAS_WGET="$(command -v wget >/dev/null 2>&1 && echo true || echo false)"
if [[ "${HAS_CURL}" != "true" ]] && [[ "${HAS_WGET}" != "true" ]]; then
die "Missing prerequisite: curl or wget"
fi
local cmd
for cmd in tar mktemp install; do
if ! command -v "${cmd}" >/dev/null 2>&1; then
die "Missing prerequisite: ${cmd}"
fi
done
if command -v sha256sum >/dev/null 2>&1; then
SHA256_CMD="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
SHA256_CMD="shasum -a 256"
else
die "Missing prerequisite: sha256sum or shasum"
fi
}
# fetch downloads URL to OUT using whichever of curl/wget is available.
fetch() {
local url="$1"
local out="$2"
if [[ "${HAS_CURL}" == "true" ]]; then
curl -fsSL "${url}" -o "${out}"
else
wget -q -O "${out}" "${url}"
fi
}
# fetch_effective_url follows redirects on URL and prints the final location.
fetch_effective_url() {
local url="$1"
if [[ "${HAS_CURL}" == "true" ]]; then
curl -sIL -o /dev/null -w '%{url_effective}' "${url}"
else
wget --max-redirect=5 --server-response --spider "${url}" 2>&1 \
| awk '/^ Location: /{u=$2} END{print u}'
fi
}
# Sets TAG from FOUNDRY_VERSION or /releases/latest redirect; validates semver shape.
resolve_version() {
if [[ -n "${FOUNDRY_VERSION}" ]]; then
case "${FOUNDRY_VERSION}" in
v*) TAG="${FOUNDRY_VERSION}" ;;
*) TAG="v${FOUNDRY_VERSION}" ;;
esac
else
local latest_url="https://github.com/${REPO}/releases/latest"
local resolved
resolved="$(fetch_effective_url "${latest_url}")"
TAG="${resolved##*/tag/}"
if [[ -z "${TAG}" ]] || [[ "${TAG}" == "${resolved}" ]]; then
die "Could not resolve latest release tag from ${latest_url}"
fi
fi
if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
die "Invalid version: ${TAG} (expected vMAJOR.MINOR.PATCH)"
fi
}
resolve_install_dir() {
if [[ -n "${FOUNDRY_INSTALL_DIR}" ]]; then
INSTALL_DIR="${FOUNDRY_INSTALL_DIR}"
elif [[ -n "${XDG_BIN_HOME:-}" ]]; then
INSTALL_DIR="${XDG_BIN_HOME}"
else
INSTALL_DIR="${HOME}/.local/bin"
fi
mkdir -p "${INSTALL_DIR}"
if [[ ! -w "${INSTALL_DIR}" ]]; then
err "Install directory is not writable: ${INSTALL_DIR}"
die "Set FOUNDRY_INSTALL_DIR to a writable path or run with appropriate permissions."
fi
DEST="${INSTALL_DIR}/${BINARY}${PLATFORM_BIN_SUFFIX}"
}
# Same version: skip and exit 0. Different version: prompt if interactive,
# auto-proceed if piped (curl-pipe-bash, CI).
check_existing() {
if [[ ! -f "${DEST}" ]]; then
return
fi
local current
current="$("${DEST}" version 2>/dev/null | awk '/^[[:space:]]*Version:/ {print $NF; exit}' || true)"
if [[ -z "${current}" ]]; then
return
fi
if [[ "${current}" == "${TAG}" ]]; then
info "${BINARY} ${TAG} is already installed at ${DEST}"
exit 0
fi
if [[ "${FOUNDRY_ASSUME_YES}" == "true" ]] || [[ ! -t 0 ]]; then
info "Updating ${BINARY} ${current} -> ${TAG}"
return
fi
local answer
read -r -p "Update ${BINARY} ${current} to ${TAG}? [Y/n] " answer
case "${answer}" in
"" | y | Y | yes | YES) ;;
*) die "Aborted by user" ;;
esac
}
# Sets TARBALL, CHECKSUMS, TARBALL_URL, CHECKSUMS_URL from PLATFORM_* and TAG.
compute_release_artifacts() {
TARBALL="foundry_${PLATFORM_OS}_${PLATFORM_ARCH}.tar.gz"
CHECKSUMS="foundry_${TAG#v}_checksums.txt"
TARBALL_URL="https://github.com/${REPO}/releases/download/${TAG}/${TARBALL}"
CHECKSUMS_URL="https://github.com/${REPO}/releases/download/${TAG}/${CHECKSUMS}"
}
# Creates TMP_ROOT and fetches the tarball and checksums into it.
download_release() {
TMP_ROOT="$(mktemp -d -t foundry-installer-XXXXXX)"
TARBALL_PATH="${TMP_ROOT}/${TARBALL}"
CHECKSUMS_PATH="${TMP_ROOT}/${CHECKSUMS}"
info "Downloading ${TARBALL_URL}"
fetch "${TARBALL_URL}" "${TARBALL_PATH}"
fetch "${CHECKSUMS_URL}" "${CHECKSUMS_PATH}"
}
# Checksums file lists "<sha256> <filename>" or "<sha256> *<filename>".
verify_checksum() {
local expected
expected="$(awk -v f="${TARBALL}" '$2 == f || $2 == "*"f {print $1; exit}' "${CHECKSUMS_PATH}")"
if [[ -z "${expected}" ]]; then
die "Checksum for ${TARBALL} not found in ${CHECKSUMS}"
fi
local actual
actual="$(${SHA256_CMD} "${TARBALL_PATH}" | awk '{print $1}')"
if [[ "${expected}" != "${actual}" ]]; then
err "Checksum mismatch for ${TARBALL}"
err " expected: ${expected}"
die " actual: ${actual}"
fi
}
install_binary() {
local extract_dir="${TMP_ROOT}/extract"
mkdir -p "${extract_dir}"
tar -xzf "${TARBALL_PATH}" -C "${extract_dir}"
local src="${extract_dir}/foundry_${PLATFORM_OS}_${PLATFORM_ARCH}/bin/${BINARY}${PLATFORM_BIN_SUFFIX}"
if [[ ! -f "${src}" ]]; then
die "Expected binary not found in tarball: ${src#"${extract_dir}"/}"
fi
install -m 0755 "${src}" "${DEST}"
info "Installed ${BINARY}${PLATFORM_BIN_SUFFIX} ${TAG} to ${DEST}"
}
# Smoke test: catches arch/platform mismatches that slipped past checksum.
verify_install() {
local output
if ! output="$("${DEST}" version 2>&1)"; then
err "Installed binary failed to run: ${DEST}"
err "This may indicate a wrong-arch download or a permissions issue."
die "${output}"
fi
echo
echo "${output}"
}
print_path_hint() {
case ":${PATH}:" in
*":${INSTALL_DIR}:"*) return ;;
*) ;;
esac
local rc_file
# shellcheck disable=SC2088
case "${SHELL:-}" in
*/zsh) rc_file='~/.zshrc' ;;
*/bash) rc_file='~/.bashrc (Linux) or ~/.bash_profile (macOS)' ;;
*/fish) rc_file='~/.config/fish/config.fish' ;;
*) rc_file='your shell config' ;;
esac
echo
warn "${INSTALL_DIR} is not on your PATH."
echo "To use ${BINARY} from any shell, add this to ${rc_file}:"
echo
echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
}
cleanup() {
if [[ -n "${TMP_ROOT:-}" ]] && [[ -d "${TMP_ROOT}" ]]; then
rm -rf "${TMP_ROOT}"
fi
}
fail_trap() {
local rc=$?
if [[ ${rc} -ne 0 ]]; then
err "Failed to install ${BINARY}."
err "For support, see https://github.com/${REPO}/issues"
fi
cleanup
exit "${rc}"
}
run() {
init_platform
verify_prereqs
resolve_version
resolve_install_dir
check_existing
compute_release_artifacts
download_release
verify_checksum
install_binary
verify_install
print_path_hint
cleanup
}
trap fail_trap EXIT
while getopts 'v:d:yh' opt; do
case "${opt}" in
v) FOUNDRY_VERSION="${OPTARG:-}" ;;
d) FOUNDRY_INSTALL_DIR="${OPTARG:-}" ;;
y) FOUNDRY_ASSUME_YES="true" ;;
h)
help
trap - EXIT
exit 0
;;
?) die "Invalid option: -${OPTARG:-}" ;;
*) die "Unknown error while processing options" ;;
esac
done
run
trap - EXIT