Repository navigation
build: add install script for systems without homebrew - #223
Conversation
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>
| # | ||
| # 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
|
||
| archive="qcloud-$os-$arch.tar.gz" | ||
| tmp=$(mktemp -d) | ||
| trap 'rm -rf "$tmp"' EXIT |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| repo="qdrant/qcloud-cli" | ||
| version="${QCLOUD_VERSION:-latest}" | ||
| install_dir="${QCLOUD_INSTALL_DIR:-$HOME/.local/bin}" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Done, using your case.
| case ":$PATH:" in | ||
| *":$install_dir:"*) ;; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Yes, added: on darwin/amd64, if sysctl -n hw.optional.arm64 is 1, it installs arm64.
| # 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| - name: Install latest | ||
| run: sh install.sh | ||
| env: | ||
| QCLOUD_INSTALL_DIR: ${{ runner.temp }}/bin |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| env: | ||
| QCLOUD_INSTALL_DIR: ${{ runner.temp }}/bin | ||
|
|
||
| - name: Run it |
There was a problem hiding this comment.
nit: "Run it" → "Run qcloud version", to match the other step names ("Run shellcheck", "Run tests").
|
|
||
| ## Installation | ||
|
|
||
| ### With Homebrew |
There was a problem hiding this comment.
nit: The new headings say "With …" but the existing ones say "From …". Not a blocker.
There was a problem hiding this comment.
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>
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 | shWhat it does
linux,darwin) and architecture (amd64,arm64). Anything else gets an error that points to the releases page, so Windows keeps the zip.qcloud-<os>-<arch>.tar.gzandchecksums.txtfromreleases/latest/download/, or from the release named inQCLOUD_VERSION(v0.32.0or0.32.0). It never calls the GitHub API, so it can't hit rate limits.checksums.txt. If it doesn't match, or the archive isn't listed, nothing is installed.~/.local/bin, or toQCLOUD_INSTALL_DIRif set. It copies the binary next to the target and renames it over the old one, so replacing a runningqclouddoesn't fail with "text file busy".qcloud version, and says how to add the directory toPATHif it isn't there.It's plain POSIX
shand needscurlorwget,sha256sumorshasum, andtar. That covers macOS, glibc Linux, and BusyBox/Alpine.CI
.github/workflows/install-script.ymlruns wheninstall.sh,.goreleaser.yaml, or the workflow itself changes. The archive names come fromname_templatein.goreleaser.yaml, so changing them would break the script.shellcheck --shell=shqcloud versiononubuntu-latest,ubuntu-24.04-arm, andmacos-latestalpine:3.22, which uses BusyBoxwgetand has no bashActions 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 toqdrant/homebrew-tapon 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)
sh install.shanddash install.shsource=Notarized Developer ID)QCLOUD_VERSION=0.31.0 sh < install.shPATHQCLOUD_VERSION=v9.9.9checksums.txttampered withchecksum mismatch, exit 1, nothing installedNotes for maintainers
buildso releaser-pleaser doesn't cut a release, since the binary is unchanged. If you'd rather list the script in the changelog, switch it tofeat.qdrant.tech/install/qcloud.sh) with a rewrite there. The script would still live here.🤖 Generated with Claude Code