diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30c143d..cd2094d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,3 +49,18 @@ jobs: run: gh release edit "$GITHUB_REF_NAME" --prerelease=false --latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + install-script: + name: install.sh (${{ matrix.os }}) + needs: goreleaser + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - run: sh install.sh --version "$GITHUB_REF_NAME" --bin-dir "$RUNNER_TEMP/bin" + - run: flagsmith --version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d319a8..17f6954 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,16 @@ repos: - id: check-added-large-files - id: check-merge-conflict + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck + + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.13.1-1 + hooks: + - id: shfmt + - repo: https://github.com/golangci/golangci-lint rev: v2.11.4 hooks: diff --git a/README.md b/README.md index 366cb91..2879bbc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,25 @@ The next-generation Flagsmith command-line interface (work in progress). +## Install + +```sh +curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh +``` + +Installs to `$HOME/.local/bin` and adds it to your `PATH`. Options: + +```sh +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 +curl -fsSL https://raw.githubusercontent.com/Flagsmith/flagsmith-cli/main/install.sh | sh -s -- --help +``` + +`FLAGSMITH_CLI_VERSION`, `FLAGSMITH_INSTALL_DIR` and `FLAGSMITH_NO_MODIFY_PATH` do the same if exported first. + +To pin the installer itself, fetch it at a commit you trust: `raw.githubusercontent.com/Flagsmith/flagsmith-cli//install.sh`. + +Alternatively, `go install github.com/Flagsmith/flagsmith-cli@latest`, or grab an archive from [Releases](https://github.com/Flagsmith/flagsmith-cli/releases). + ## Build ```sh diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..6243ab3 --- /dev/null +++ b/install.sh @@ -0,0 +1,270 @@ +#!/bin/sh +# Install the Flagsmith CLI. +# +# curl -fsSL https://get.flagsmith.com | sh +# curl -fsSL https://get.flagsmith.com | sh -s -- --version +# +# A variable assignment in front of `curl` applies to curl, not to sh, so pass +# the version as a flag or export it first. +set -eu + +DEFAULT_VERSION="v2.0.0-beta.1" # x-release-please-version + +REPO="Flagsmith/flagsmith-cli" +BIN_NAME="flagsmith" +BASE_URL="${FLAGSMITH_CLI_BASE_URL:-https://github.com/${REPO}/releases/download}" + +usage() { + cat < Version to install (default: ${DEFAULT_VERSION}) + --bin-dir Where to install (default: \$HOME/.local/bin) + --no-modify-path Leave shell startup files alone + --dry-run Report what would be installed, then stop + -h, --help Show this message + +Environment: + FLAGSMITH_CLI_VERSION Same as --version + FLAGSMITH_INSTALL_DIR Same as --bin-dir + FLAGSMITH_NO_MODIFY_PATH Set to 1 for --no-modify-path + FLAGSMITH_CLI_BASE_URL Release download base URL +EOF +} + +say() { printf '%s\n' "$*"; } +err() { + printf '%s\n' "install.sh: $*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || err "need '$1' (command not found)" +} + +download() { + if [ "$DOWNLOADER" = curl ]; then + case "$1" in + # Refuse a downgrade to http on our own URLs; a base URL someone set + # themselves is their call. + https://*) curl --proto '=https' --tlsv1.2 -fsSL --retry 3 -o "$2" "$1" ;; + *) curl -fsSL --retry 3 -o "$2" "$1" ;; + esac + else + wget --quiet --output-document="$2" "$1" + fi +} + +parse_args() { + VERSION="${FLAGSMITH_CLI_VERSION:-$DEFAULT_VERSION}" + INSTALL_DIR="${FLAGSMITH_INSTALL_DIR:-}" + NO_MODIFY_PATH="${FLAGSMITH_NO_MODIFY_PATH:-0}" + DRY_RUN=0 + + while [ $# -gt 0 ]; do + case "$1" in + -v | --version) + [ $# -ge 2 ] || err "--version needs a value, e.g. --version ${DEFAULT_VERSION}" + VERSION="$2" + shift 2 + ;; + --bin-dir) + [ $# -ge 2 ] || err "--bin-dir needs a value" + INSTALL_DIR="$2" + shift 2 + ;; + --no-modify-path) + NO_MODIFY_PATH=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) err "unknown option '$1' (try --help)" ;; + esac + done + + case "$VERSION" in + v*) ;; + *) VERSION="v${VERSION}" ;; + esac + : "${INSTALL_DIR:=${HOME}/.local/bin}" +} + +# detect_platform sets OS and ARCH to the halves of a release archive name. +detect_platform() { + OS=$(uname -s) + ARCH=$(uname -m) + + case "$OS" in + Linux) OS=linux ;; + Darwin) OS=darwin ;; + MINGW* | MSYS* | CYGWIN* | Windows_NT) + err "Windows is not supported by this script — download the .zip from https://github.com/${REPO}/releases" + ;; + *) err "unsupported operating system '${OS}'" ;; + esac + + case "$ARCH" in + x86_64 | amd64) ARCH=amd64 ;; + aarch64 | arm64) ARCH=arm64 ;; + *) err "unsupported architecture '${ARCH}' — 'go install github.com/${REPO}@${VERSION}' builds from source" ;; + esac + + # uname reports x86_64 under Rosetta. + if [ "$OS" = darwin ] && [ "$ARCH" = amd64 ] && + [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then + ARCH=arm64 + fi +} + +verify_checksum() { + _archive="$1" + _name=$(basename "$_archive") + _matches=$(awk -v name="$_name" '$2 == name || $2 == "*" name {print $1}' "$2") + [ "$(printf '%s' "$_matches" | grep -c .)" = 1 ] || + err "expected exactly one checksum for ${_name} in checksums.txt" + + if command -v sha256sum >/dev/null 2>&1; then + _actual=$(sha256sum "$_archive" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + _actual=$(shasum -a 256 "$_archive" | awk '{print $1}') + elif command -v openssl >/dev/null 2>&1; then + _actual=$(openssl dgst -sha256 "$_archive" | awk '{print $NF}') + else + err "need 'sha256sum', 'shasum' or 'openssl' to verify the download" + fi + + [ "$_actual" = "$_matches" ] || + err "checksum mismatch for ${_name}: expected ${_matches}, got ${_actual}" +} + +# write_env_scripts writes the snippets the startup files source. They live +# under our own directory: $HOME/.local/bin/env belongs to cargo-dist. +write_env_scripts() { + mkdir -p "$(dirname "$ENV_SCRIPT")" + cat >"$ENV_SCRIPT" <"${ENV_SCRIPT}.fish" <>"$GITHUB_PATH" + say " added ${INSTALL_DIR} to \$GITHUB_PATH" +} + +# add_source_line appends to a startup file, once. +add_source_line() { + grep -qF "$2" "$1" && return 0 + printf '\n%s\n' "$2" >>"$1" + say " updated $1" +} + +modify_path() { + case ":${PATH}:" in + *:"${INSTALL_DIR}":*) return 0 ;; + esac + + write_env_scripts + _line=". \"${ENV_SCRIPT}\"" + _edited=0 + for _rc in .profile .bashrc .bash_profile .bash_login .zshrc .zshenv; do + if [ -f "${HOME}/${_rc}" ]; then + add_source_line "${HOME}/${_rc}" "$_line" + _edited=1 + fi + done + if [ "$_edited" = 0 ]; then + printf '%s\n' "$_line" >>"${HOME}/.profile" + say " created ${HOME}/.profile" + fi + if [ -d "${HOME}/.config/fish" ]; then + mkdir -p "${HOME}/.config/fish/conf.d" + printf 'source "%s"\n' "${ENV_SCRIPT}.fish" >"${HOME}/.config/fish/conf.d/flagsmith.fish" + say " updated ${HOME}/.config/fish/conf.d/flagsmith.fish" + fi + PATH_MODIFIED=1 +} + +main() { + parse_args "$@" + + need_cmd uname + need_cmd tar + if command -v curl >/dev/null 2>&1; then + DOWNLOADER=curl + elif command -v wget >/dev/null 2>&1; then + DOWNLOADER=wget + else + err "need 'curl' or 'wget'" + fi + + detect_platform + ENV_SCRIPT="${XDG_DATA_HOME:-${HOME}/.local/share}/flagsmith/env" + PATH_MODIFIED=0 + + archive_name="${BIN_NAME}_${VERSION#v}_${OS}_${ARCH}.tar.gz" + archive_url="${BASE_URL}/${VERSION}/${archive_name}" + sums_url="${BASE_URL}/${VERSION}/checksums.txt" + + if [ "$DRY_RUN" = 1 ]; then + say "would install ${BIN_NAME} ${VERSION} (${OS}/${ARCH}) to ${INSTALL_DIR}" + say " archive: ${archive_url}" + say " checksums: ${sums_url}" + return 0 + fi + + tmp=$(mktemp -d 2>/dev/null || mktemp -d -t flagsmith) + trap 'rm -rf "$tmp"' EXIT INT TERM + + say "downloading ${BIN_NAME} ${VERSION} (${OS}/${ARCH})" + download "$archive_url" "${tmp}/${archive_name}" || + err "cannot download ${archive_url} +If ${VERSION} was released moments ago its archives may still be uploading — retry shortly, or choose a version with --version." + download "$sums_url" "${tmp}/checksums.txt" || err "cannot download ${sums_url}" + verify_checksum "${tmp}/${archive_name}" "${tmp}/checksums.txt" + + tar -xzf "${tmp}/${archive_name}" -C "$tmp" "$BIN_NAME" + mkdir -p "$INSTALL_DIR" + chmod 755 "${tmp}/${BIN_NAME}" + mv -f "${tmp}/${BIN_NAME}" "${INSTALL_DIR}/${BIN_NAME}" + + installed=$("${INSTALL_DIR}/${BIN_NAME}" --version 2>/dev/null) || + err "${INSTALL_DIR}/${BIN_NAME} was installed but will not run — wrong platform?" + say "installed ${installed} to ${INSTALL_DIR}/${BIN_NAME}" + + if [ "$NO_MODIFY_PATH" != 1 ]; then + modify_path + add_ci_path + fi + + say "" + if [ "$PATH_MODIFIED" = 1 ]; then + say "Run '. \"${ENV_SCRIPT}\"' or open a new shell, then '${BIN_NAME} init' to get started." + else + say "Run '${BIN_NAME} init' to get started." + fi +} + +main "$@" diff --git a/release-please-config.json b/release-please-config.json index 0d0a7c1..46ba24f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,7 +8,10 @@ "prerelease": true, "prerelease-type": "beta", "draft": false, - "include-component-in-tag": false + "include-component-in-tag": false, + "extra-files": [ + "install.sh" + ] } }, "changelog-sections": [