Skip to content

Commit 9e65a63

Browse files
authored
feat: install.sh (#59)
* feat: add install.sh The script pins the version it was released alongside, so the common path makes no API call and cannot be rate limited: release-please rewrites DEFAULT_VERSION through the `extra-files` entry, whose Generic updater replaces the first semver token on any line carrying `x-release-please-version`. Only the token changes, so the leading v survives. Everything lives in functions with `main "$@"` last, so a truncated download cannot execute half a script. The strict curl flags apply to https URLs only — FLAGSMITH_CLI_BASE_URL exists to point the script at a local server for testing, and that server is plain http. PATH handling follows uv: an env script the startup files source, edited once each, skipped when the install dir is already on PATH. The env script lives under our own directory because $HOME/.local/bin/env belongs to cargo-dist and uv writes it. beep boop * feat: put the install dir on $GITHUB_PATH Each `run:` step gets a fresh shell, and an Actions shell is neither a login nor an interactive one, so nothing it starts reads the startup files the installer edited. Appending the directory to the file $GITHUB_PATH points at is the mechanism the runner does honour: it prepends those entries for every later step. So `curl ... | sh` in one step and `flagsmith` in the next now works on any runner, rather than only on images that already ship ~/.local/bin on PATH. Taken from uv's Add-Ci-Path. The release smoke job drops its explicit sourcing of the env script and relies on this instead. beep boop * fix: stop suppressing errexit for the whole install POSIX ignores -e for every command of an AND-OR list but the last, and that suppression covers the entire body of a function called there. So `main "$@" || exit 1` disabled set -e for the whole script: tar, mkdir, chmod and mv could all fail unnoticed. Reinstalling over an existing binary with a corrupt archive printed the success message and exited 0. The idiom came from rustup and cargo-dist, which set -u only and wrap every command in `ensure`, so the trailing `|| exit 1` costs them nothing. It is not transferable to a script that relies on -e. beep boop
1 parent ebb84d9 commit 9e65a63

5 files changed

Lines changed: 318 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,18 @@ jobs:
4949
run: gh release edit "$GITHUB_REF_NAME" --prerelease=false --latest
5050
env:
5151
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
53+
install-script:
54+
name: install.sh (${{ matrix.os }})
55+
needs: goreleaser
56+
strategy:
57+
fail-fast: false
58+
matrix:
59+
os: [ubuntu-latest, macos-latest]
60+
runs-on: ${{ matrix.os }}
61+
steps:
62+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
63+
with:
64+
persist-credentials: false
65+
- run: sh install.sh --version "$GITHUB_REF_NAME" --bin-dir "$RUNNER_TEMP/bin"
66+
- run: flagsmith --version

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ repos:
88
- id: check-added-large-files
99
- id: check-merge-conflict
1010

11+
- repo: https://github.com/shellcheck-py/shellcheck-py
12+
rev: v0.11.0.1
13+
hooks:
14+
- id: shellcheck
15+
16+
- repo: https://github.com/scop/pre-commit-shfmt
17+
rev: v3.13.1-1
18+
hooks:
19+
- id: shfmt
20+
1121
- repo: https://github.com/golangci/golangci-lint
1222
rev: v2.11.4
1323
hooks:

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
The next-generation Flagsmith command-line interface (work in progress).
44

5+
## Install
6+
7+
```sh
8+
curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh
9+
```
10+
11+
Installs to `$HOME/.local/bin` and adds it to your `PATH`. Options:
12+
13+
```sh
14+
curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh -s -- --version v2.0.0 --bin-dir /usr/local/bin --no-modify-path
15+
curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh -s -- --help
16+
```
17+
18+
`FLAGSMITH_CLI_VERSION`, `FLAGSMITH_INSTALL_DIR` and `FLAGSMITH_NO_MODIFY_PATH` do the same if exported first.
19+
20+
To pin the installer itself, fetch it at a commit you trust: `raw.githubusercontent.com/Flagsmith/flagsmith-cli/<sha>/install.sh`.
21+
22+
Alternatively, `go install github.com/Flagsmith/flagsmith-cli@latest`, or grab an archive from [Releases](https://github.com/Flagsmith/flagsmith-cli/releases).
23+
524
## Build
625

726
```sh

install.sh

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
#!/bin/sh
2+
# Install the Flagsmith CLI.
3+
#
4+
# curl -fsSL https://get.flagsmith.com | sh
5+
# curl -fsSL https://get.flagsmith.com | sh -s -- --version <tag>
6+
#
7+
# A variable assignment in front of `curl` applies to curl, not to sh, so pass
8+
# the version as a flag or export it first.
9+
set -eu
10+
11+
DEFAULT_VERSION="v2.0.0-beta.1" # x-release-please-version
12+
13+
REPO="Flagsmith/flagsmith-cli"
14+
BIN_NAME="flagsmith"
15+
BASE_URL="${FLAGSMITH_CLI_BASE_URL:-https://github.com/${REPO}/releases/download}"
16+
17+
usage() {
18+
cat <<EOF
19+
Install the Flagsmith CLI.
20+
21+
Usage: install.sh [options]
22+
23+
Options:
24+
-v, --version <tag> Version to install (default: ${DEFAULT_VERSION})
25+
--bin-dir <dir> Where to install (default: \$HOME/.local/bin)
26+
--no-modify-path Leave shell startup files alone
27+
--dry-run Report what would be installed, then stop
28+
-h, --help Show this message
29+
30+
Environment:
31+
FLAGSMITH_CLI_VERSION Same as --version
32+
FLAGSMITH_INSTALL_DIR Same as --bin-dir
33+
FLAGSMITH_NO_MODIFY_PATH Set to 1 for --no-modify-path
34+
FLAGSMITH_CLI_BASE_URL Release download base URL
35+
EOF
36+
}
37+
38+
say() { printf '%s\n' "$*"; }
39+
err() {
40+
printf '%s\n' "install.sh: $*" >&2
41+
exit 1
42+
}
43+
44+
need_cmd() {
45+
command -v "$1" >/dev/null 2>&1 || err "need '$1' (command not found)"
46+
}
47+
48+
download() {
49+
if [ "$DOWNLOADER" = curl ]; then
50+
case "$1" in
51+
# Refuse a downgrade to http on our own URLs; a base URL someone set
52+
# themselves is their call.
53+
https://*) curl --proto '=https' --tlsv1.2 -fsSL --retry 3 -o "$2" "$1" ;;
54+
*) curl -fsSL --retry 3 -o "$2" "$1" ;;
55+
esac
56+
else
57+
wget --quiet --output-document="$2" "$1"
58+
fi
59+
}
60+
61+
parse_args() {
62+
VERSION="${FLAGSMITH_CLI_VERSION:-$DEFAULT_VERSION}"
63+
INSTALL_DIR="${FLAGSMITH_INSTALL_DIR:-}"
64+
NO_MODIFY_PATH="${FLAGSMITH_NO_MODIFY_PATH:-0}"
65+
DRY_RUN=0
66+
67+
while [ $# -gt 0 ]; do
68+
case "$1" in
69+
-v | --version)
70+
[ $# -ge 2 ] || err "--version needs a value, e.g. --version ${DEFAULT_VERSION}"
71+
VERSION="$2"
72+
shift 2
73+
;;
74+
--bin-dir)
75+
[ $# -ge 2 ] || err "--bin-dir needs a value"
76+
INSTALL_DIR="$2"
77+
shift 2
78+
;;
79+
--no-modify-path)
80+
NO_MODIFY_PATH=1
81+
shift
82+
;;
83+
--dry-run)
84+
DRY_RUN=1
85+
shift
86+
;;
87+
-h | --help)
88+
usage
89+
exit 0
90+
;;
91+
*) err "unknown option '$1' (try --help)" ;;
92+
esac
93+
done
94+
95+
case "$VERSION" in
96+
v*) ;;
97+
*) VERSION="v${VERSION}" ;;
98+
esac
99+
: "${INSTALL_DIR:=${HOME}/.local/bin}"
100+
}
101+
102+
# detect_platform sets OS and ARCH to the halves of a release archive name.
103+
detect_platform() {
104+
OS=$(uname -s)
105+
ARCH=$(uname -m)
106+
107+
case "$OS" in
108+
Linux) OS=linux ;;
109+
Darwin) OS=darwin ;;
110+
MINGW* | MSYS* | CYGWIN* | Windows_NT)
111+
err "Windows is not supported by this script — download the .zip from https://github.com/${REPO}/releases"
112+
;;
113+
*) err "unsupported operating system '${OS}'" ;;
114+
esac
115+
116+
case "$ARCH" in
117+
x86_64 | amd64) ARCH=amd64 ;;
118+
aarch64 | arm64) ARCH=arm64 ;;
119+
*) err "unsupported architecture '${ARCH}' — 'go install github.com/${REPO}@${VERSION}' builds from source" ;;
120+
esac
121+
122+
# uname reports x86_64 under Rosetta.
123+
if [ "$OS" = darwin ] && [ "$ARCH" = amd64 ] &&
124+
[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then
125+
ARCH=arm64
126+
fi
127+
}
128+
129+
verify_checksum() {
130+
_archive="$1"
131+
_name=$(basename "$_archive")
132+
_matches=$(awk -v name="$_name" '$2 == name || $2 == "*" name {print $1}' "$2")
133+
[ "$(printf '%s' "$_matches" | grep -c .)" = 1 ] ||
134+
err "expected exactly one checksum for ${_name} in checksums.txt"
135+
136+
if command -v sha256sum >/dev/null 2>&1; then
137+
_actual=$(sha256sum "$_archive" | awk '{print $1}')
138+
elif command -v shasum >/dev/null 2>&1; then
139+
_actual=$(shasum -a 256 "$_archive" | awk '{print $1}')
140+
elif command -v openssl >/dev/null 2>&1; then
141+
_actual=$(openssl dgst -sha256 "$_archive" | awk '{print $NF}')
142+
else
143+
err "need 'sha256sum', 'shasum' or 'openssl' to verify the download"
144+
fi
145+
146+
[ "$_actual" = "$_matches" ] ||
147+
err "checksum mismatch for ${_name}: expected ${_matches}, got ${_actual}"
148+
}
149+
150+
# write_env_scripts writes the snippets the startup files source. They live
151+
# under our own directory: $HOME/.local/bin/env belongs to cargo-dist.
152+
write_env_scripts() {
153+
mkdir -p "$(dirname "$ENV_SCRIPT")"
154+
cat >"$ENV_SCRIPT" <<EOF
155+
# Added by the Flagsmith CLI installer.
156+
case ":\${PATH}:" in
157+
*:"${INSTALL_DIR}":*) ;;
158+
*) export PATH="${INSTALL_DIR}:\${PATH}" ;;
159+
esac
160+
EOF
161+
cat >"${ENV_SCRIPT}.fish" <<EOF
162+
# Added by the Flagsmith CLI installer.
163+
if not contains "${INSTALL_DIR}" \$PATH
164+
set -gx PATH "${INSTALL_DIR}" \$PATH
165+
end
166+
EOF
167+
}
168+
169+
# add_ci_path makes the CLI available to later steps of a GitHub Actions job.
170+
# GITHUB_PATH does not expand variables, so write the resolved directory.
171+
add_ci_path() {
172+
[ -n "${GITHUB_PATH:-}" ] || return 0
173+
printf '%s\n' "$INSTALL_DIR" >>"$GITHUB_PATH"
174+
say " added ${INSTALL_DIR} to \$GITHUB_PATH"
175+
}
176+
177+
# add_source_line appends to a startup file, once.
178+
add_source_line() {
179+
grep -qF "$2" "$1" && return 0
180+
printf '\n%s\n' "$2" >>"$1"
181+
say " updated $1"
182+
}
183+
184+
modify_path() {
185+
case ":${PATH}:" in
186+
*:"${INSTALL_DIR}":*) return 0 ;;
187+
esac
188+
189+
write_env_scripts
190+
_line=". \"${ENV_SCRIPT}\""
191+
_edited=0
192+
for _rc in .profile .bashrc .bash_profile .bash_login .zshrc .zshenv; do
193+
if [ -f "${HOME}/${_rc}" ]; then
194+
add_source_line "${HOME}/${_rc}" "$_line"
195+
_edited=1
196+
fi
197+
done
198+
if [ "$_edited" = 0 ]; then
199+
printf '%s\n' "$_line" >>"${HOME}/.profile"
200+
say " created ${HOME}/.profile"
201+
fi
202+
if [ -d "${HOME}/.config/fish" ]; then
203+
mkdir -p "${HOME}/.config/fish/conf.d"
204+
printf 'source "%s"\n' "${ENV_SCRIPT}.fish" >"${HOME}/.config/fish/conf.d/flagsmith.fish"
205+
say " updated ${HOME}/.config/fish/conf.d/flagsmith.fish"
206+
fi
207+
PATH_MODIFIED=1
208+
}
209+
210+
main() {
211+
parse_args "$@"
212+
213+
need_cmd uname
214+
need_cmd tar
215+
if command -v curl >/dev/null 2>&1; then
216+
DOWNLOADER=curl
217+
elif command -v wget >/dev/null 2>&1; then
218+
DOWNLOADER=wget
219+
else
220+
err "need 'curl' or 'wget'"
221+
fi
222+
223+
detect_platform
224+
ENV_SCRIPT="${XDG_DATA_HOME:-${HOME}/.local/share}/flagsmith/env"
225+
PATH_MODIFIED=0
226+
227+
archive_name="${BIN_NAME}_${VERSION#v}_${OS}_${ARCH}.tar.gz"
228+
archive_url="${BASE_URL}/${VERSION}/${archive_name}"
229+
sums_url="${BASE_URL}/${VERSION}/checksums.txt"
230+
231+
if [ "$DRY_RUN" = 1 ]; then
232+
say "would install ${BIN_NAME} ${VERSION} (${OS}/${ARCH}) to ${INSTALL_DIR}"
233+
say " archive: ${archive_url}"
234+
say " checksums: ${sums_url}"
235+
return 0
236+
fi
237+
238+
tmp=$(mktemp -d 2>/dev/null || mktemp -d -t flagsmith)
239+
trap 'rm -rf "$tmp"' EXIT INT TERM
240+
241+
say "downloading ${BIN_NAME} ${VERSION} (${OS}/${ARCH})"
242+
download "$archive_url" "${tmp}/${archive_name}" ||
243+
err "cannot download ${archive_url}
244+
If ${VERSION} was released moments ago its archives may still be uploading — retry shortly, or choose a version with --version."
245+
download "$sums_url" "${tmp}/checksums.txt" || err "cannot download ${sums_url}"
246+
verify_checksum "${tmp}/${archive_name}" "${tmp}/checksums.txt"
247+
248+
tar -xzf "${tmp}/${archive_name}" -C "$tmp" "$BIN_NAME"
249+
mkdir -p "$INSTALL_DIR"
250+
chmod 755 "${tmp}/${BIN_NAME}"
251+
mv -f "${tmp}/${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}"
252+
253+
installed=$("${INSTALL_DIR}/${BIN_NAME}" --version 2>/dev/null) ||
254+
err "${INSTALL_DIR}/${BIN_NAME} was installed but will not run — wrong platform?"
255+
say "installed ${installed} to ${INSTALL_DIR}/${BIN_NAME}"
256+
257+
if [ "$NO_MODIFY_PATH" != 1 ]; then
258+
modify_path
259+
add_ci_path
260+
fi
261+
262+
say ""
263+
if [ "$PATH_MODIFIED" = 1 ]; then
264+
say "Run '. \"${ENV_SCRIPT}\"' or open a new shell, then '${BIN_NAME} init' to get started."
265+
else
266+
say "Run '${BIN_NAME} init' to get started."
267+
fi
268+
}
269+
270+
main "$@"

release-please-config.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
"prerelease": true,
99
"prerelease-type": "beta",
1010
"draft": false,
11-
"include-component-in-tag": false
11+
"include-component-in-tag": false,
12+
"extra-files": [
13+
"install.sh"
14+
]
1215
}
1316
},
1417
"changelog-sections": [

0 commit comments

Comments
 (0)