Skip to content

build: add install script for systems without homebrew - #223

Merged
Davidonium merged 3 commits into
mainfrom
build/install-script
Oct 20, 2026
Merged

Davidonium merged 3 commits into
mainfrom
build/install-script

Conversation

@kanungle

Copy link
Copy Markdown
Contributor

Adds install.sh, a one-line install for systems without Homebrew: CI runners, Docker images, and sandboxed coding agents.

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

What it does

  1. Detects the OS (linux, darwin) and architecture (amd64, arm64). Anything else gets an error that points to the releases page, so Windows keeps the zip.
  2. Downloads qcloud-<os>-<arch>.tar.gz and checksums.txt from releases/latest/download/, or from the release named in QCLOUD_VERSION (v0.32.0 or 0.32.0). It never calls the GitHub API, so it can't hit rate limits.
  3. Checks the archive's SHA-256 against checksums.txt. If it doesn't match, or the archive isn't listed, nothing is installed.
  4. Installs to ~/.local/bin, or to QCLOUD_INSTALL_DIR if set. It copies the binary next to the target and renames it over the old one, so replacing a running qcloud doesn't fail with "text file busy".
  5. Prints qcloud version, and says how to add the directory to PATH if it isn't there.

It's plain POSIX sh and needs curl or wget, sha256sum or shasum, and tar. That covers macOS, glibc Linux, and BusyBox/Alpine.

CI

.github/workflows/install-script.yml runs when install.sh, .goreleaser.yaml, or the workflow itself changes. The archive names come from name_template in .goreleaser.yaml, so changing them would break the script.

  • shellcheck --shell=sh
  • A real install and qcloud version on ubuntu-latest, ubuntu-24.04-arm, and macos-latest
  • The same in alpine:3.22, which uses BusyBox wget and has no bash

Actions are pinned to SHAs with persist-credentials: false, matching the other workflows, for zizmor.

README

The Installation section now lists Homebrew first (brew install qdrant/tap/qcloud). The cask was already published to qdrant/homebrew-tap on every release, but the README didn't mention it. The install script comes second, then GitHub Releases and building from source, which are unchanged.

Tested locally (macOS arm64)

Case Result
sh install.sh and dash install.sh Installs 0.32.0. The binary passes Gatekeeper (source=Notarized Developer ID)
QCLOUD_VERSION=0.31.0 sh < install.sh Installs 0.31.0 when piped through stdin
Install directory already on PATH No PATH hint
QCLOUD_VERSION=v9.9.9 404, exit 1, nothing installed
checksums.txt tampered with checksum mismatch, exit 1, nothing installed

Notes for maintainers

  • Commit type: I used build so releaser-pleaser doesn't cut a release, since the binary is unchanged. If you'd rather list the script in the changelog, switch it to feat.
  • Short URL: the qdrant.tech docs could later serve it at a shorter URL (for example qdrant.tech/install/qcloud.sh) with a rewrite there. The script would still live here.

🤖 Generated with Claude Code

install.sh downloads the latest (or QCLOUD_VERSION) release archive for the
current OS and architecture, verifies it against checksums.txt, and installs
the binary to ~/.local/bin (or QCLOUD_INSTALL_DIR). CI runs shellcheck and
installs on Linux x86-64 and ARM64, macOS, and Alpine. The README now lists
Homebrew and the script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kanungle
kanungle requested a review from a team October 17, 2026 22:32
@kanungle
kanungle requested a review from a team as a code owner October 17, 2026 22:32
AstraBert
AstraBert previously approved these changes Oct 19, 2026

@AstraBert AstraBert left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks solid to me 👍

Comment thread install.sh
#
# 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.

Comment thread install.sh Outdated

archive="qcloud-$os-$arch.tar.gz"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

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.

EXIT traps don't run on SIGINT/SIGTERM under dash, so Ctrl-C during the download leaves the temp dir behind. trap 'rm -rf "$tmp"' EXIT INT TERM covers 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.

Done, with a small variation: trap 'exit 1' INT TERM alongside the EXIT trap. Putting rm in the INT handler alone would let the script carry on after Ctrl-C, while exit fires the EXIT trap, which does the cleanup.

Comment thread install.sh Outdated

repo="qdrant/qcloud-cli"
version="${QCLOUD_VERSION:-latest}"
install_dir="${QCLOUD_INSTALL_DIR:-$HOME/.local/bin}"

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.

nit: A quoted ~ in QCLOUD_INSTALL_DIR (common in Dockerfile ENV or YAML env:) isn't expanded, so the script creates a literal ./~/bin. A case $install_dir in "~/"*) install_dir="$HOME/${install_dir#\~/}" ;; esac would handle it. Fine to skip.

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, using your case.

Comment thread install.sh Outdated
Comment on lines +83 to +84
case ":$PATH:" in
*":$install_dir:"*) ;;

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.

nit: If QCLOUD_INSTALL_DIR has a trailing slash or is a relative path, this match fails and prints the PATH hint even when the directory is already on PATH. Cosmetic.

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.

Fixed. After mkdir -p, the directory is turned into an absolute path with CDPATH= cd -- "$install_dir" && pwd. That handles a trailing slash and a relative path. I checked QCLOUD_INSTALL_DIR=dir/ with dir on PATH, and no hint is printed.

Comment thread install.sh Outdated
Comment on lines +32 to +36
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

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.

question: On an Apple Silicon Mac, a shell running under Rosetta reports x86_64, so the script installs the amd64 build. It still works through Rosetta, but rustup and Homebrew check sysctl -n hw.optional.arm64 to pick arm64. Worth doing here?

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.

Yes, added: on darwin/amd64, if sysctl -n hw.optional.arm64 is 1, it installs arm64.

Comment thread .github/workflows/install-script.yml Outdated
Comment on lines +31 to +32
# Installs the latest release on each platform it supports. A change to the
# archive names in .goreleaser.yaml breaks the script, so that file runs this too.

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 first sentence restates the job. The second sentence overstates the check. This job installs the latest published release, so a PR that renames archives in .goreleaser.yaml still passes, and the script only breaks after the next release ships. Either say that in the comment, or make the check real by running the script against a goreleaser release --snapshot build.

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.

Went with the honest comment, and dropped .goreleaser.yaml from the triggers, since running on that file proved nothing. A snapshot build would need the script to accept a local base URL, which I would rather not add to the script for CI alone.

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

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.

Comment thread .github/workflows/install-script.yml Outdated
env:
QCLOUD_INSTALL_DIR: ${{ runner.temp }}/bin

- name: Run it

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.

nit: "Run it" → "Run qcloud version", to match the other step names ("Run shellcheck", "Run tests").

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.

Comment thread README.md Outdated

## Installation

### With Homebrew

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.

nit: The new headings say "With …" but the existing ones say "From …". Not a blocker.

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: "From Homebrew" and "From the install script".

Wrap the script in main so a truncated download runs nothing, clean up
the temp dir on INT/TERM, expand a quoted ~ and normalize the install
dir, prefer arm64 under Rosetta, and test a pinned QCLOUD_VERSION in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Davidonium
Davidonium merged commit aebf75e into main Oct 20, 2026
12 checks passed
@Davidonium
Davidonium deleted the build/install-script branch October 20, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants