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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ jobs:
with:
version: v2.14.0
install-mode: goinstall
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y shellcheck dash
- run: shellcheck -s sh install.sh scripts/test-install
- run: scripts/test-install dash sh bash
# Alpine has no curl and a BusyBox wget (no --max-redirect); the whole suite
# runs there so the wget fallback is exercised for real.
- run: >
docker run --rm --network host -v "$PWD:/src:ro" alpine:3.23
sh -c 'apk add --no-cache python3 >/dev/null && sh /src/scripts/test-install sh'
integration:
runs-on: ubuntu-latest
steps:
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,27 @@

Install the binary in `~/.local/bin` (it must be on your `PATH`). The commands below use `v0.4.0`, which exists only once that release is published; until then, use the latest tag on [GitHub Releases](https://github.com/BasisTI/taiga-cli/releases).

Download the tarball for your platform together with `SHA256SUMS`; the binary is installed only if its checksum matches. The block runs in a subshell with `set -eu`, so any failing step (download, a missing or failed checksum, extraction) stops it before anything is installed, and your shell stays open. On macOS, where `sha256sum` may be missing, it uses `shasum -a 256`:
One line, with `curl` (or `wget`) and no `gh`:

```sh
curl -fsSL https://raw.githubusercontent.com/BasisTI/taiga-cli/main/install.sh | sh
```

[`install.sh`](install.sh) is POSIX `sh` and never uses `sudo`. It picks the latest release (no GitHub API call), downloads the tarball for your OS and architecture (Linux or macOS, amd64 or arm64) with `SHA256SUMS`, installs only if the checksum matches, and finishes by running `taiga version`. Pass options after `sh -s --`, or use the environment variables:

```sh
# a specific version (with or without the "v") in another directory
curl -fsSL https://raw.githubusercontent.com/BasisTI/taiga-cli/main/install.sh | sh -s -- --version v0.4.0 --dir "$HOME/bin"
curl -fsSL https://raw.githubusercontent.com/BasisTI/taiga-cli/main/install.sh | TAIGA_VERSION=0.4.0 TAIGA_INSTALL_DIR="$HOME/bin" sh
```

It warns when the destination is not on your `PATH`. `TAIGA_INSTALL_BASE_URL` replaces the releases URL and exists only to test the installer against a local fixture (`scripts/test-install`); do not set it otherwise.

Prefer to read the script first? Download `install.sh`, review it and run `sh install.sh`.

### Manual install

To do it by hand, download the tarball for your platform together with `SHA256SUMS`; the binary is installed only if its checksum matches. The block runs in a subshell with `set -eu`, so any failing step (download, a missing or failed checksum, extraction) stops it before anything is installed, and your shell stays open. On macOS, where `sha256sum` may be missing, it uses `shasum -a 256`:

```sh
(
Expand Down
195 changes: 195 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/bin/sh
# Install the taiga CLI from a GitHub release.
#
# curl -fsSL https://raw.githubusercontent.com/BasisTI/taiga-cli/main/install.sh | sh
# curl -fsSL .../install.sh | sh -s -- --version v0.4.0 --dir "$HOME/bin"
#
# Options (the environment variable is used when the option is absent):
# -v, --version VERSION release to install, with or without "v" (TAIGA_VERSION; default: latest)
# -d, --dir DIR destination directory (TAIGA_INSTALL_DIR; default: ~/.local/bin)
# -h, --help show this help
#
# TAIGA_INSTALL_BASE_URL replaces https://github.com/BasisTI/taiga-cli/releases and exists
# only so the installer can be tested against a local fixture; do not set it otherwise.
#
# It never uses sudo.

# Everything below sits in one brace group, so the shell runs nothing until it has
# read the closing brace: a download cut at any point is a syntax error, not a
# partial install.
{
main() {
set -eu

repo_url="https://github.com/BasisTI/taiga-cli/releases"
base_url="${TAIGA_INSTALL_BASE_URL:-$repo_url}"
base_url="${base_url%/}"
version="${TAIGA_VERSION:-}"
dir="${TAIGA_INSTALL_DIR:-${HOME:-}/.local/bin}"

while [ $# -gt 0 ]; do
case "$1" in
-v | --version)
[ $# -ge 2 ] || die "$1 needs a value"
version="$2"
shift 2
;;
--version=*)
version="${1#--version=}"
shift
;;
-d | --dir)
[ $# -ge 2 ] || die "$1 needs a value"
dir="$2"
shift 2
;;
--dir=*)
dir="${1#--dir=}"
shift
;;
-h | --help)
usage
return 0
;;
*)
die "unknown option: $1 (try --help)"
;;
esac
done

if [ -z "$dir" ] || [ "$dir" = "/.local/bin" ]; then
die "cannot work out the destination: set HOME, TAIGA_INSTALL_DIR or --dir"
fi
while [ "${#dir}" -gt 1 ] && [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done

if [ -e "$dir/taiga" ] && [ ! -f "$dir/taiga" ]; then
die "$dir/taiga exists and is not a regular file; nothing installed"
fi

if command -v curl >/dev/null 2>&1; then
fetcher=curl
elif command -v wget >/dev/null 2>&1; then
fetcher=wget
else
die "curl or wget is required"
fi
if command -v sha256sum >/dev/null 2>&1; then
hasher="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
hasher="shasum -a 256"
else
die "sha256sum or shasum is required to verify the download"
fi
command -v tar >/dev/null 2>&1 || die "tar is required"

case "$(uname -s)" in
Linux) os=linux ;;
Darwin) os=darwin ;;
*) die "unsupported operating system: $(uname -s) (supported: Linux, Darwin)" ;;
esac
case "$(uname -m)" in
x86_64 | amd64) arch=amd64 ;;
aarch64 | arm64) arch=arm64 ;;
*) die "unsupported architecture: $(uname -m) (supported: x86_64, arm64)" ;;
esac

if [ -z "$version" ]; then
say "Looking up the latest release"
version=$(latest_version)
[ -n "$version" ] || die "could not work out the latest release from $base_url/latest"
fi
version="${version#v}"
case "$version" in
'' | *[!0-9A-Za-z._+-]*) die "invalid version: $version" ;;
esac

file="taiga_${version}_${os}_${arch}.tar.gz"
url="$base_url/download/v$version"

tmp=$(mktemp -d "${TMPDIR:-/tmp}/taiga-install.XXXXXX") || die "cannot create a temporary directory"
staged=""
trap 'cleanup' EXIT
trap 'exit 1' INT TERM HUP

say "Downloading taiga v$version for $os/$arch"
download "$url/$file" "$tmp/$file" || die "could not download $url/$file"
download "$url/SHA256SUMS" "$tmp/SHA256SUMS" || die "could not download $url/SHA256SUMS"

expected=$(awk -v f="$file" '$2 == f { print $1; exit }' "$tmp/SHA256SUMS")
[ -n "$expected" ] || die "$file is not listed in SHA256SUMS; nothing installed"
actual=$($hasher "$tmp/$file" | awk '{ print $1 }')
if [ "$expected" != "$actual" ]; then
die "checksum mismatch for $file (expected $expected, got $actual); nothing installed"
fi
say "Checksum OK"

tar -xzf "$tmp/$file" -C "$tmp" taiga || die "could not extract taiga from $file"
[ -f "$tmp/taiga" ] || die "$file does not contain a taiga binary"

mkdir -p "$dir" || die "cannot create $dir"
staged="$dir/.taiga.$$"
cp "$tmp/taiga" "$staged" || die "cannot write to $dir"
chmod 0755 "$staged"
mv -f "$staged" "$dir/taiga" || die "cannot install into $dir"
staged=""

say "Installed $dir/taiga"
"$dir/taiga" version </dev/null || die "the installed binary does not run"

case ":${PATH:-}:" in
*":$dir:"*) ;;
*) say "warning: $dir is not on your PATH; add it to your shell profile to run taiga" ;;
esac
}

usage() {
printf '%s\n' \
'Usage: install.sh [--version VERSION] [--dir DIR]' \
'' \
' -v, --version VERSION release to install, with or without "v" (TAIGA_VERSION; default: latest)' \
' -d, --dir DIR destination directory (TAIGA_INSTALL_DIR; default: ~/.local/bin)' \
' -h, --help show this help' \
'' \
'Downloads the release tarball and SHA256SUMS, verifies the checksum and installs' \
'the binary atomically. It never uses sudo.'
}

say() {
printf '%s\n' "$*" >&2
}

die() {
printf 'install.sh: %s\n' "$*" >&2
exit 1
}

cleanup() {
[ -z "${staged:-}" ] || rm -f "$staged"
[ -z "${tmp:-}" ] || rm -rf "$tmp"
}

download() {
if [ "$fetcher" = curl ]; then
curl -fsSL -o "$2" "$1"
else
wget -q -O "$2" "$1"
fi
}

# Prints the latest version from the redirect of <base>/latest (…/tag/vX.Y.Z), without the GitHub API.
latest_version() {
if [ "$fetcher" = curl ]; then
location=$(curl -fsS -o /dev/null -w '%{redirect_url}' "$base_url/latest") || return 0
else
# No --max-redirect: BusyBox wget lacks it. Follow the redirect and read the
# first Location header from the -S output (GNU and BusyBox both print it).
location=$(wget -q -S -O /dev/null "$base_url/latest" 2>&1 | awk 'tolower($1) == "location:" { print $2; exit }' | tr -d '\r') || return 0
fi
tag="${location##*/}"
case "$tag" in
v[0-9]*) printf '%s\n' "${tag#v}" ;;
esac
}

main "$@"
}
Loading
Loading