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
71 changes: 71 additions & 0 deletions .github/workflows/install-script.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Install script

permissions:
contents: read

on:
push:
branches: [main]
paths:
- install.sh
- .github/workflows/install-script.yml
pull_request:
paths:
- install.sh
- .github/workflows/install-script.yml

jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Run shellcheck
run: shellcheck --shell=sh install.sh

# Runs against published releases, so it can't catch a rename of the archives
# in .goreleaser.yaml until a release with the new names ships.
install:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install latest
run: sh install.sh
env:
QCLOUD_INSTALL_DIR: ${{ runner.temp }}/bin
Comment on lines +43 to +46

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in CI runs the QCLOUD_VERSION path, including the v prefix handling. Could we add a step or matrix entry pinned to a known release, for example QCLOUD_VERSION: 0.31.0, and check that qcloud version reports it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the install job on all three runners: it installs QCLOUD_VERSION: 0.31.0 (without the v, which is the path that adds it) and checks for qcloud 0.31.0 with grep -Fx. The v path is the plain URL case. I tested it locally.


- name: Run qcloud version
run: '"$RUNNER_TEMP/bin/qcloud" version'

- name: Install pinned version
run: sh install.sh
env:
QCLOUD_VERSION: 0.31.0
QCLOUD_INSTALL_DIR: ${{ runner.temp }}/pinned

- name: Check pinned version
run: '"$RUNNER_TEMP/pinned/qcloud" version | grep -Fx "qcloud 0.31.0"'

# BusyBox userland: wget instead of curl, no bash. The common case for
# containers and sandboxed agents.
install-alpine:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install in Alpine
run: docker run --rm -v "$PWD:/src:ro" alpine:3.22 sh -c 'sh /src/install.sh && /root/.local/bin/qcloud version'
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@

## Installation

### From Homebrew

On macOS and Linux:

```sh
brew install qdrant/tap/qcloud
```

### From the install script

For systems without Homebrew, such as CI runners, containers, and sandboxed agents:

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

The script installs the latest release for your OS and CPU architecture to `~/.local/bin`, after checking the archive against the release's `checksums.txt`. Set `QCLOUD_VERSION` (for example `v0.32.0`) to pin a release, or `QCLOUD_INSTALL_DIR` to install somewhere else. It needs `curl` or `wget`, `sha256sum` or `shasum`, and `tar`.

### From GitHub Releases

Download the latest release from [GitHub Releases](https://github.com/qdrant/qcloud-cli/releases).
Expand Down
105 changes: 105 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/bin/sh
# Install qcloud, the Qdrant Cloud CLI, from GitHub Releases.
#
# curl -fsSL https://raw.githubusercontent.com/qdrant/qcloud-cli/main/install.sh | sh
#
# For macOS and Linux on x86-64 or ARM64. On Windows, download the zip from
# https://github.com/qdrant/qcloud-cli/releases instead.
#
# Environment variables:
# QCLOUD_VERSION release to install, such as v0.32.0 (default: latest)
# QCLOUD_INSTALL_DIR directory for the binary (default: $HOME/.local/bin)
#
# The archive is verified against the release's checksums.txt before anything
# is installed. Needs curl or wget, sha256sum or shasum, and tar.
set -eu

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script runs at the top level, so if curl | sh gets cut off midway, sh runs whatever arrived. I fed dash a copy cut off after line 78: it downloaded and verified the archive, left .qcloud.new in the install directory, and exited 0. Tailscale's installer avoids this by wrapping everything in main() { ... } and calling main "$@" on the last line. Can we do the same?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d3137bd: everything is in main(), called on the last line. Cutting the script off after line 80 now gives Syntax error: end of file unexpected from dash, and nothing is downloaded or installed.


repo="qdrant/qcloud-cli"
version="${QCLOUD_VERSION:-latest}"

fail() {
echo "qcloud install: $*" >&2
exit 1
}

# Everything runs from main, called on the last line, so a download of this
# script cut off midway runs nothing instead of half an install.
main() {
case "$(uname -s)" in
Linux) os=linux ;;
Darwin) os=darwin ;;
*) fail "unsupported OS $(uname -s). Download a release from https://github.com/$repo/releases" ;;
esac

case "$(uname -m)" in
x86_64 | amd64) arch=amd64 ;;
arm64 | aarch64) arch=arm64 ;;
*) fail "unsupported architecture $(uname -m). Download a release from https://github.com/$repo/releases" ;;
esac
# A shell under Rosetta reports x86_64 on Apple Silicon; install the native build.
if [ "$os" = darwin ] && [ "$arch" = amd64 ] && [ "$(sysctl -n hw.optional.arm64 2>/dev/null)" = 1 ]; then
arch=arm64
fi

case "$version" in
latest) base="https://github.com/$repo/releases/latest/download" ;;
v*) base="https://github.com/$repo/releases/download/$version" ;;
*) base="https://github.com/$repo/releases/download/v$version" ;;
esac

if command -v curl >/dev/null 2>&1; then
fetch() { curl -fsSL -o "$2" "$1"; }
elif command -v wget >/dev/null 2>&1; then
fetch() { wget -q -O "$2" "$1"; }
else
fail "needs curl or wget"
fi

if command -v sha256sum >/dev/null 2>&1; then
sha256() { sha256sum "$1" | cut -d ' ' -f 1; }
elif command -v shasum >/dev/null 2>&1; then
sha256() { shasum -a 256 "$1" | cut -d ' ' -f 1; }
else
fail "needs sha256sum or shasum to verify the download"
fi

archive="qcloud-$os-$arch.tar.gz"
tmp=$(mktemp -d)
# dash skips EXIT traps on signals, so turn them into an exit.
trap 'rm -rf "$tmp"' EXIT
trap 'exit 1' INT TERM

echo "Downloading $archive ($version)"
fetch "$base/$archive" "$tmp/$archive" || fail "could not download $base/$archive"
fetch "$base/checksums.txt" "$tmp/checksums.txt" || fail "could not download $base/checksums.txt"

expected=$(awk -v f="$archive" '$2 == f { print $1 }' "$tmp/checksums.txt")
[ -n "$expected" ] || fail "$archive is not listed in checksums.txt"
actual=$(sha256 "$tmp/$archive")
[ "$expected" = "$actual" ] || fail "checksum mismatch for $archive: expected $expected, got $actual"

tar -xzf "$tmp/$archive" -C "$tmp" qcloud
install_dir="${QCLOUD_INSTALL_DIR:-$HOME/.local/bin}"
# A quoted ~ from Dockerfile ENV or YAML env: isn't expanded by the shell.
case "$install_dir" in \~/*) install_dir="$HOME/${install_dir#\~/}" ;; esac
mkdir -p "$install_dir"
# Absolute, without a trailing slash, so the PATH check below matches.
install_dir=$(CDPATH='' cd -- "$install_dir" && pwd)
# Copy next to the target, then rename over it: a running qcloud is replaced
# atomically instead of failing with "text file busy".
cp "$tmp/qcloud" "$install_dir/.qcloud.new"
chmod 755 "$install_dir/.qcloud.new"
mv -f "$install_dir/.qcloud.new" "$install_dir/qcloud"

echo "Installed $("$install_dir/qcloud" version) to $install_dir/qcloud"

case ":$PATH:" in
*":$install_dir:"*) ;;
*)
echo "$install_dir is not on your PATH. Add it, for example:"
echo " export PATH=\"$install_dir:\$PATH\""
;;
esac
}

main "$@"
Loading