From eb947a1cb9621c621a2a789f5fba767087ff96c0 Mon Sep 17 00:00:00 2001 From: "cody.lee" Date: Wed, 14 Oct 2026 16:24:29 +0000 Subject: [PATCH 1/4] feat(install): add curl | bash install script Adds a one-line installer for pup that works on macOS, Linux, and Windows (Git Bash/MSYS2/Cygwin). - macOS with Homebrew: runs `brew install datadog-labs/pack/pup` - Otherwise: resolves the latest release (or PUP_VERSION), downloads the matching archive, verifies its SHA-256 against the release checksums file, and installs to PUP_INSTALL_DIR (~/.local/bin) - New Install Script workflow: shellcheck plus install tests on Linux x86_64/arm64, macOS, and Windows, the Homebrew path, and a nonexistent-version failure case - README documents the installer and its environment variables Co-Authored-By: Claude Opus 5.5 --- .github/workflows/install-script.yml | 80 +++++++++++++ README.md | 18 +++ install.sh | 168 +++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 .github/workflows/install-script.yml create mode 100755 install.sh diff --git a/.github/workflows/install-script.yml b/.github/workflows/install-script.yml new file mode 100644 index 00000000..bd2d1bf9 --- /dev/null +++ b/.github/workflows/install-script.yml @@ -0,0 +1,80 @@ +name: Install Script + +on: + push: + branches: [main] + paths: + - install.sh + - .github/workflows/install-script.yml + pull_request: + paths: + - install.sh + - .github/workflows/install-script.yml + +permissions: + contents: read + +concurrency: + group: install-script-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Shellcheck + run: shellcheck install.sh + + install: + name: Install (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] + defaults: + run: + shell: bash + env: + PUP_INSTALL_DIR: ${{ github.workspace }}/pup-bin + PUP_NO_BREW: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install latest via pipe + run: bash < install.sh + - name: Verify latest version + run: | + latest="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/DataDog/pup/releases/latest)" + latest="${latest##*/tag/v}" + "$PUP_INSTALL_DIR/pup" --version | tee version.txt + grep -F "$latest" version.txt + - name: Install pinned version with v prefix + run: PUP_VERSION=v1.27.0 bash install.sh + - name: Verify pinned version + run: | + "$PUP_INSTALL_DIR/pup" --version | tee version.txt + grep -F "1.27.0" version.txt + - name: Nonexistent version fails + run: | + if PUP_VERSION=0.0.0 bash install.sh; then + echo "expected install of a nonexistent version to fail" + exit 1 + fi + + install-brew: + name: Install via Homebrew (macOS) + runs-on: macos-latest + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALL_CLEANUP: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install via pipe + run: bash < install.sh 2>&1 | tee install.log + - name: Verify Homebrew was used + run: | + grep -F "Homebrew detected" install.log + brew list --formula pup + pup --version diff --git a/README.md b/README.md index 59064ba8..9485129e 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,24 @@ list of commands as built. ## Installation +### Install Script (macOS/Linux/Windows) + +```bash +curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | bash +``` + +On macOS with Homebrew installed, the script runs `brew install datadog-labs/pack/pup`. Otherwise (Linux, macOS without Homebrew, and Windows via Git Bash, MSYS2, or Cygwin) it downloads the release archive, verifies its SHA-256 checksum, and installs the binary to `~/.local/bin`. Environment variables override the defaults: + +| Variable | Default | Description | +|----------|---------|-------------| +| `PUP_VERSION` | latest release | Version to install (e.g. `1.27.0`) | +| `PUP_INSTALL_DIR` | `~/.local/bin` | Directory to install the binary into | +| `PUP_NO_BREW` | unset | Set to `1` on macOS to skip Homebrew and download directly | + +```bash +curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | PUP_VERSION=1.27.0 PUP_INSTALL_DIR=/usr/local/bin bash +``` + ### Homebrew (macOS/Linux) ```bash diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..1ead2850 --- /dev/null +++ b/install.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# Install the pup CLI (https://github.com/DataDog/pup). +# +# curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | bash +# +# On macOS with Homebrew available, installs via `brew install datadog-labs/pack/pup`. +# Everywhere else (Linux, macOS without Homebrew, Windows via Git Bash/MSYS2/Cygwin), +# downloads the release archive, verifies its SHA-256 checksum, and installs the binary. +# +# Environment variables: +# PUP_VERSION Version to install, e.g. 1.27.0 (default: latest release) +# PUP_INSTALL_DIR Directory for the binary (default: $HOME/.local/bin) +# PUP_NO_BREW Set to 1 to skip Homebrew on macOS and download directly + +set -eu + +REPO="DataDog/pup" +BREW_FORMULA="datadog-labs/pack/pup" + +say() { printf 'pup-install: %s\n' "$*" >&2; } +die() { + say "error: $*" + exit 1 +} + +need() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +detect_os() { + case "$(uname -s)" in + Darwin) echo Darwin ;; + Linux) echo Linux ;; + MINGW* | MSYS* | CYGWIN*) echo Windows ;; + *) die "unsupported operating system: $(uname -s)" ;; + esac +} + +detect_arch() { + local os="$1" arch + arch="$(uname -m)" + case "$arch" in + x86_64 | amd64) arch=x86_64 ;; + arm64 | aarch64) arch=arm64 ;; + *) die "unsupported architecture: $arch" ;; + esac + # A shell running under Rosetta reports x86_64 on Apple Silicon. + if [ "$os" = Darwin ] && [ "$arch" = x86_64 ] && + [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then + arch=arm64 + fi + # Only an x86_64 Windows build is published; Windows on ARM runs it under emulation. + if [ "$os" = Windows ]; then + arch=x86_64 + fi + echo "$arch" +} + +latest_version() { + local url + # Follow the /releases/latest redirect instead of the rate-limited GitHub API. + url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest")" || + die "could not resolve the latest release" + case "$url" in + */tag/v*) echo "${url##*/tag/v}" ;; + *) die "unexpected latest release URL: $url" ;; + esac +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + die "sha256sum or shasum is required to verify the download" + fi +} + +verify_checksum() { + local file="$1" checksums="$2" name expected actual + name="$(basename "$file")" + expected="$(awk -v f="$name" '$2 == f {print $1}' "$checksums")" + [ -n "$expected" ] || die "no checksum listed for $name" + actual="$(sha256_of "$file")" + [ "$expected" = "$actual" ] || die "checksum mismatch for $name (expected $expected, got $actual)" +} + +extract() { + local archive="$1" dest="$2" + case "$archive" in + *.tar.gz) tar -xzf "$archive" -C "$dest" ;; + *.zip) + if command -v unzip >/dev/null 2>&1; then + unzip -q "$archive" -d "$dest" + else + powershell.exe -NoProfile -Command \ + "Expand-Archive -LiteralPath '$(cygpath -w "$archive")' -DestinationPath '$(cygpath -w "$dest")'" + fi + ;; + esac +} + +install_with_brew() { + say "Homebrew detected; running: brew install $BREW_FORMULA" + brew install "$BREW_FORMULA" /dev/null 2>&1; then + install_with_brew + return + fi + need curl + need tar + need awk + install_direct "$os" +} + +# Everything runs from main so a truncated `curl | bash` download executes nothing. +main "$@" From 9d98e7ae27c2d0514bef77bffd2fb1e736095497 Mon Sep 17 00:00:00 2001 From: "cody.lee" Date: Wed, 14 Oct 2026 16:36:37 +0000 Subject: [PATCH 2/4] refactor(install): make install script POSIX sh Run the installer with `curl ... | sh` instead of requiring bash. - Drop `local` (not POSIX) and the bash shebang - Call `main` without "$@", which posh rejects under `set -u` - Shellcheck in sh mode; CI also installs under dash, posh, and busybox sh - README shows `| sh` Co-Authored-By: Claude Opus 5.5 --- .github/workflows/install-script.yml | 28 +++++++++++++++++++++++----- README.md | 4 ++-- install.sh | 23 ++++++++++++----------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/.github/workflows/install-script.yml b/.github/workflows/install-script.yml index bd2d1bf9..b6bc3bf6 100644 --- a/.github/workflows/install-script.yml +++ b/.github/workflows/install-script.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Shellcheck - run: shellcheck install.sh + run: shellcheck --shell=sh install.sh install: name: Install (${{ matrix.os }}) @@ -43,7 +43,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install latest via pipe - run: bash < install.sh + run: sh < install.sh - name: Verify latest version run: | latest="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/DataDog/pup/releases/latest)" @@ -51,18 +51,36 @@ jobs: "$PUP_INSTALL_DIR/pup" --version | tee version.txt grep -F "$latest" version.txt - name: Install pinned version with v prefix - run: PUP_VERSION=v1.27.0 bash install.sh + run: PUP_VERSION=v1.27.0 sh install.sh - name: Verify pinned version run: | "$PUP_INSTALL_DIR/pup" --version | tee version.txt grep -F "1.27.0" version.txt - name: Nonexistent version fails run: | - if PUP_VERSION=0.0.0 bash install.sh; then + if PUP_VERSION=0.0.0 sh install.sh; then echo "expected install of a nonexistent version to fail" exit 1 fi + install-posix-shells: + name: Install (${{ matrix.shell }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shell: [dash, posh, busybox sh] + env: + PUP_INSTALL_DIR: ${{ github.workspace }}/pup-bin + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install shells + run: sudo apt-get install -y dash posh busybox + - name: Install via pipe + run: ${{ matrix.shell }} < install.sh + - name: Verify install + run: '"$PUP_INSTALL_DIR/pup" --version' + install-brew: name: Install via Homebrew (macOS) runs-on: macos-latest @@ -72,7 +90,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install via pipe - run: bash < install.sh 2>&1 | tee install.log + run: sh < install.sh 2>&1 | tee install.log - name: Verify Homebrew was used run: | grep -F "Homebrew detected" install.log diff --git a/README.md b/README.md index 9485129e..084d0ffe 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ list of commands as built. ### Install Script (macOS/Linux/Windows) ```bash -curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | sh ``` On macOS with Homebrew installed, the script runs `brew install datadog-labs/pack/pup`. Otherwise (Linux, macOS without Homebrew, and Windows via Git Bash, MSYS2, or Cygwin) it downloads the release archive, verifies its SHA-256 checksum, and installs the binary to `~/.local/bin`. Environment variables override the defaults: @@ -207,7 +207,7 @@ On macOS with Homebrew installed, the script runs `brew install datadog-labs/pac | `PUP_NO_BREW` | unset | Set to `1` on macOS to skip Homebrew and download directly | ```bash -curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | PUP_VERSION=1.27.0 PUP_INSTALL_DIR=/usr/local/bin bash +curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | PUP_VERSION=1.27.0 PUP_INSTALL_DIR=/usr/local/bin sh ``` ### Homebrew (macOS/Linux) diff --git a/install.sh b/install.sh index 1ead2850..1e1ce8e7 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ -#!/usr/bin/env bash +#!/bin/sh # Install the pup CLI (https://github.com/DataDog/pup). # -# curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | bash +# curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | sh # # On macOS with Homebrew available, installs via `brew install datadog-labs/pack/pup`. # Everywhere else (Linux, macOS without Homebrew, Windows via Git Bash/MSYS2/Cygwin), @@ -37,7 +37,7 @@ detect_os() { } detect_arch() { - local os="$1" arch + os="$1" arch="$(uname -m)" case "$arch" in x86_64 | amd64) arch=x86_64 ;; @@ -57,7 +57,6 @@ detect_arch() { } latest_version() { - local url # Follow the /releases/latest redirect instead of the rate-limited GitHub API. url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest")" || die "could not resolve the latest release" @@ -78,7 +77,8 @@ sha256_of() { } verify_checksum() { - local file="$1" checksums="$2" name expected actual + file="$1" + checksums="$2" name="$(basename "$file")" expected="$(awk -v f="$name" '$2 == f {print $1}' "$checksums")" [ -n "$expected" ] || die "no checksum listed for $name" @@ -87,7 +87,8 @@ verify_checksum() { } extract() { - local archive="$1" dest="$2" + archive="$1" + dest="$2" case "$archive" in *.tar.gz) tar -xzf "$archive" -C "$dest" ;; *.zip) @@ -108,7 +109,7 @@ install_with_brew() { } install_direct() { - local os="$1" arch version ext bin asset base tmp install_dir + os="$1" arch="$(detect_arch "$os")" version="${PUP_VERSION:-$(latest_version)}" version="${version#v}" @@ -123,7 +124,7 @@ install_direct() { install_dir="${PUP_INSTALL_DIR:-$HOME/.local/bin}" tmp="$(mktemp -d)" - # shellcheck disable=SC2064 # expand $tmp now; it is local to this function + # shellcheck disable=SC2064 # expand $tmp now, not when the trap fires trap "rm -rf '$tmp'" EXIT say "downloading $asset" @@ -152,7 +153,6 @@ install_direct() { main() { need uname - local os os="$(detect_os)" if [ "$os" = Darwin ] && [ "${PUP_NO_BREW:-0}" != 1 ] && command -v brew >/dev/null 2>&1; then install_with_brew @@ -164,5 +164,6 @@ main() { install_direct "$os" } -# Everything runs from main so a truncated `curl | bash` download executes nothing. -main "$@" +# POSIX sh: no `local`, so function variables are script-global. +# Everything runs from main so a truncated `curl | sh` download executes nothing. +main From 42d53b11ee45b8b38a40a48f5a202f8c65d9ac8c Mon Sep 17 00:00:00 2001 From: "cody.lee" Date: Wed, 14 Oct 2026 17:21:24 +0000 Subject: [PATCH 3/4] ci(install): trim install script test matrix Drop the dash/posh/busybox job and the ubuntu arm runner; keep ubuntu-latest, macos-latest, windows-latest, and the Homebrew job. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/install-script.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/install-script.yml b/.github/workflows/install-script.yml index b6bc3bf6..ea641ab8 100644 --- a/.github/workflows/install-script.yml +++ b/.github/workflows/install-script.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] defaults: run: shell: bash @@ -63,24 +63,6 @@ jobs: exit 1 fi - install-posix-shells: - name: Install (${{ matrix.shell }}) - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shell: [dash, posh, busybox sh] - env: - PUP_INSTALL_DIR: ${{ github.workspace }}/pup-bin - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Install shells - run: sudo apt-get install -y dash posh busybox - - name: Install via pipe - run: ${{ matrix.shell }} < install.sh - - name: Verify install - run: '"$PUP_INSTALL_DIR/pup" --version' - install-brew: name: Install via Homebrew (macOS) runs-on: macos-latest From 2d2f1ac110599645104584572250f5e3b2f5aaae Mon Sep 17 00:00:00 2001 From: "cody.lee" Date: Wed, 14 Oct 2026 17:54:50 +0000 Subject: [PATCH 4/4] fix(install): honor PUP_VERSION/PUP_INSTALL_DIR when Homebrew is present On macOS with Homebrew, the installer took the brew path even when PUP_VERSION or PUP_INSTALL_DIR was set, silently ignoring both. Route explicitly customized installs through the direct download instead. - Skip Homebrew when either override is set - Document the behavior in the script header and README - CI: assert overrides bypass Homebrew on macOS Co-Authored-By: Claude Opus 5.5 --- .github/workflows/install-script.yml | 8 ++++++++ README.md | 2 +- install.sh | 7 +++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/install-script.yml b/.github/workflows/install-script.yml index ea641ab8..257cc505 100644 --- a/.github/workflows/install-script.yml +++ b/.github/workflows/install-script.yml @@ -78,3 +78,11 @@ jobs: grep -F "Homebrew detected" install.log brew list --formula pup pup --version + - name: Overrides skip Homebrew + run: | + PUP_VERSION=1.27.0 PUP_INSTALL_DIR="$RUNNER_TEMP/pup-bin" sh install.sh 2>&1 | tee override.log + if grep -F "Homebrew detected" override.log; then + echo "expected PUP_VERSION/PUP_INSTALL_DIR to bypass Homebrew" + exit 1 + fi + "$RUNNER_TEMP/pup-bin/pup" --version | grep -F "1.27.0" diff --git a/README.md b/README.md index 084d0ffe..d579d79e 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ list of commands as built. curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | sh ``` -On macOS with Homebrew installed, the script runs `brew install datadog-labs/pack/pup`. Otherwise (Linux, macOS without Homebrew, and Windows via Git Bash, MSYS2, or Cygwin) it downloads the release archive, verifies its SHA-256 checksum, and installs the binary to `~/.local/bin`. Environment variables override the defaults: +On macOS with Homebrew installed, the script runs `brew install datadog-labs/pack/pup`, unless `PUP_VERSION` or `PUP_INSTALL_DIR` is set. Otherwise (Linux, macOS without Homebrew, and Windows via Git Bash, MSYS2, or Cygwin) it downloads the release archive, verifies its SHA-256 checksum, and installs the binary to `~/.local/bin`. Environment variables override the defaults: | Variable | Default | Description | |----------|---------|-------------| diff --git a/install.sh b/install.sh index 1e1ce8e7..75c8bb6d 100755 --- a/install.sh +++ b/install.sh @@ -3,7 +3,8 @@ # # curl -fsSL https://raw.githubusercontent.com/DataDog/pup/main/install.sh | sh # -# On macOS with Homebrew available, installs via `brew install datadog-labs/pack/pup`. +# On macOS with Homebrew available, installs via `brew install datadog-labs/pack/pup` +# unless PUP_VERSION or PUP_INSTALL_DIR is set, since Homebrew cannot honor them. # Everywhere else (Linux, macOS without Homebrew, Windows via Git Bash/MSYS2/Cygwin), # downloads the release archive, verifies its SHA-256 checksum, and installs the binary. # @@ -154,7 +155,9 @@ install_direct() { main() { need uname os="$(detect_os)" - if [ "$os" = Darwin ] && [ "${PUP_NO_BREW:-0}" != 1 ] && command -v brew >/dev/null 2>&1; then + if [ "$os" = Darwin ] && [ "${PUP_NO_BREW:-0}" != 1 ] && + [ -z "${PUP_VERSION:-}" ] && [ -z "${PUP_INSTALL_DIR:-}" ] && + command -v brew >/dev/null 2>&1; then install_with_brew return fi