Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ body:
description: How did you install clickhousectl?
options:
- Quick install script (curl ... | sh)
- Homebrew (brew tap clickhouse/tap)
- cargo binstall
- npm
- pip / pipx / uv (PyPI)
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,26 @@ jobs:
working-directory: npm
run: npm publish --provenance --access public

publish-homebrew:
name: Publish to Homebrew tap
needs: release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
environment: release-publishing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Compute version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Update Homebrew formula
env:
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
run: ./scripts/update-homebrew-formula.sh "${{ steps.version.outputs.version }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Homebrew ignores Cargo tag alignment

Medium Severity

The new publish-homebrew job sets the formula version from the git tag and runs brew test-style checks against the binary, but release archives still come from the existing build job, which never realigns crates/clickhousectl/Cargo.toml to the tag. The build-wheels job already documents and fixes that mismatch for PyPI; Homebrew can publish a tap version that does not match --version from the shipped binary when the tag was pushed without a Cargo bump.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 84d0f9f. Configure here.


build-wheels:
name: Build wheel (${{ matrix.target }})
needs: release
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/test-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- "crates/clickhousectl/src/main.rs"
- "crates/clickhousectl/src/cli.rs"
- "crates/clickhousectl/Cargo.toml"
- "homebrew/**"
- "scripts/update-homebrew-formula.sh"
- ".github/workflows/test-install.yml"

permissions:
Expand Down Expand Up @@ -144,3 +146,36 @@ jobs:
run: |
version_dir=$(ls -d ~/.clickhouse/versions/*/clickhouse | head -1)
$version_dir --version

homebrew-formula:
name: Homebrew formula
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Install Ruby
uses: ruby/setup-ruby@d5123d83b85bf5fc943d23f88231b89204f0f3e9 # v1.207.0
with:
ruby-version: '3.3'

- name: Render template and verify Ruby syntax
run: |
# Render the template with placeholder values to verify it produces
# syntactically valid Ruby. Uses dummy SHA256 hashes.
TMPDIR=$(mktemp -d)
sed \
-e 's|{{VERSION}}|0.0.0-test|g' \
-e 's|{{SHA256_X86_64_LINUX_MUSL}}|0000000000000000000000000000000000000000000000000000000000000000|g' \
-e 's|{{SHA256_AARCH64_LINUX_MUSL}}|0000000000000000000000000000000000000000000000000000000000000000|g' \
-e 's|{{SHA256_X86_64_APPLE_DARWIN}}|0000000000000000000000000000000000000000000000000000000000000000|g' \
-e 's|{{SHA256_AARCH64_APPLE_DARWIN}}|0000000000000000000000000000000000000000000000000000000000000000|g' \
homebrew/clickhousectl.rb.tmpl > "${TMPDIR}/clickhousectl.rb"

ruby -c "${TMPDIR}/clickhousectl.rb"

- name: Shellcheck update script
run: |
# shellcheck is pre-installed on ubuntu-latest.
shellcheck scripts/update-homebrew-formula.sh
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ Use `cargo add` to add new dependencies. Use the latest version of packages. Spe
- Releases are triggered by pushing a version tag (e.g. `git tag v0.2.3 && git push origin v0.2.3`), which runs the GitHub Actions workflow
- Bump all of these to the same version in lockstep: `crates/clickhousectl/Cargo.toml` (`version` and the `clickhouse-cloud-api` dep version), `crates/clickhouse-cloud-api/Cargo.toml`, and `npm/package.json`. The workflow also re-aligns `npm/package.json` to the tag at publish time, but bump it in the repo too so the source-of-truth matches. `pypi/pyproject.toml` does *not* need a manual bump — maturin pulls the wheel version from `crates/clickhousectl/Cargo.toml` (via `dynamic = ["version"]`), and the `build-wheels` job also re-aligns the Cargo version to the tag at publish time.
- For `clickhouse-cloud-api`, the crate is published to crates.io.
- For `clickhousectl`, releases are published to GitHub releases, crates.io, npm, and PyPI. The npm and PyPI packages are thin wrappers to make it easier for LLMs to find and install. crates.io uses a token, while npm & PyPI use OIDC. All of these releases are triggered by the same release workflow, in separate jobs.
- For `clickhousectl`, releases are published to GitHub releases, crates.io, npm, PyPI, and the Homebrew tap. The npm and PyPI packages are thin wrappers to make it easier for LLMs to find and install. crates.io uses a token, while npm & PyPI use OIDC. All of these releases are triggered by the same release workflow, in separate jobs.
- The Homebrew tap lives in `ClickHouse/homebrew-tap` (formula at `Formula/clickhousectl.rb`). The `publish-homebrew` job runs `scripts/update-homebrew-formula.sh`, which downloads the 4 GitHub release archives, computes SHA256, renders `homebrew/clickhousectl.rb.tmpl`, and pushes the result to the tap repo. The formula's download URLs point at `builds.clickhouse.com` (same bytes) so installs use the same distribution path as the other install methods. The job uses the `HOMEBREW_TAP_DEPLOY_KEY` secret (an SSH deploy key with write access on the tap repo) stored in the `release-publishing` GitHub environment.

## Git workflow

Expand Down
22 changes: 9 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ With `clickhousectl` you can:
curl https://clickhouse.com/cli | sh
```

The install script will download the correct version for your OS and install to `~/.local/bin/clickhousectl`. A `chctl` alias is also created automatically for convenience.
Installs to `~/.local/bin/clickhousectl`. A `chctl` alias is also created automatically for convenience.

### `cargo binstall`
### Homebrew

If you already have [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall), this pulls the prebuilt binary from `builds.clickhouse.com`:
```bash
brew install clickhouse/tap/clickhousectl
```

### `cargo binstall`

```bash
cargo binstall clickhousectl
Expand All @@ -40,8 +44,6 @@ cargo binstall clickhousectl
npm install -g clickhousectl
```

This installs an npm wrapper package that downloads the matching prebuilt binary from `builds.clickhouse.com` at install time. Both `clickhousectl` and `chctl` are exposed as commands. If you use `npm install --ignore-scripts`, the download is skipped — fall back to one of the other install paths.

### pip

```bash
Expand All @@ -52,8 +54,6 @@ pipx install clickhousectl
uv tool install clickhousectl
```

This installs a prebuilt wheel containing the matching `clickhousectl` binary. Linux (glibc and musl, x86_64 and aarch64) and macOS (Intel and Apple Silicon) wheels are published to PyPI.

### From crates.io

Builds from source:
Expand All @@ -68,10 +68,6 @@ cargo install clickhousectl
cargo install --path crates/clickhousectl
```

### Direct download

Prebuilt archives for each release are hosted at `https://builds.clickhouse.com/clickhousectl/`. Archives are named `clickhousectl-{target}-v{version}.tar.gz` and contain a single directory of the same name with the `clickhousectl` binary inside. Supported targets: `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`, `x86_64-apple-darwin`, `aarch64-apple-darwin`. Example: `https://builds.clickhouse.com/clickhousectl/clickhousectl-aarch64-apple-darwin-v0.3.0.tar.gz`.

## Local

### Installing and managing ClickHouse versions
Expand Down Expand Up @@ -872,5 +868,5 @@ export CONTINUE_ON_NON_BLOCKING_FAILURES=1

## Requirements

- macOS (aarch64, x86_64) or Linux (aarch64, x86_64)
- Cloud commands require a [ClickHouse Cloud API key](https://clickhouse.com/docs/en/cloud/manage/api)
- macOS (aarch64, x86_64) or Linux (aarch64, x86_64) or WSL
- Cloud commands require a [ClickHouse Cloud API key](https://clickhouse.com/docs/en/cloud/manage/api)
51 changes: 51 additions & 0 deletions homebrew/clickhousectl.rb.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# This file is auto-generated by scripts/update-homebrew-formula.sh on each
# release. Do not edit this file directly — edit the template in the
# clickhousectl repo (homebrew/clickhousectl.rb.tmpl) and re-run the script.

class Clickhousectl < Formula
desc "CLI for ClickHouse: local and cloud"
homepage "https://github.com/ClickHouse/clickhousectl"
version "{{VERSION}}"
license "Apache-2.0"

# Prebuilt binaries are served from builds.clickhouse.com. The SHA256 is
# computed from the identical GitHub release assets (same bytes) by
# scripts/update-homebrew-formula.sh at release time.
livecheck do
url :stable
strategy :github_latest
end

on_macos do
on_intel do
url "https://builds.clickhouse.com/clickhousectl/clickhousectl-x86_64-apple-darwin-v{{VERSION}}.tar.gz"
sha256 "{{SHA256_X86_64_APPLE_DARWIN}}"
end
on_arm do
url "https://builds.clickhouse.com/clickhousectl/clickhousectl-aarch64-apple-darwin-v{{VERSION}}.tar.gz"
sha256 "{{SHA256_AARCH64_APPLE_DARWIN}}"
end
end

on_linux do
on_intel do
url "https://builds.clickhouse.com/clickhousectl/clickhousectl-x86_64-unknown-linux-musl-v{{VERSION}}.tar.gz"
sha256 "{{SHA256_X86_64_LINUX_MUSL}}"
end
on_arm do
url "https://builds.clickhouse.com/clickhousectl/clickhousectl-aarch64-unknown-linux-musl-v{{VERSION}}.tar.gz"
sha256 "{{SHA256_AARCH64_LINUX_MUSL}}"
end
end

def install
# The archive contains a single directory: clickhousectl-{target}-v{version}/clickhousectl
bin.install "clickhousectl"
# Match install.sh: create a chctl symlink for convenience.
bin.install_symlink "clickhousectl" => "chctl"
end

test do
assert_match(/^clickhousectl #{Regexp.escape(version.to_s)}$/, shell_output("#{bin}/clickhousectl --version"))
end
end
138 changes: 138 additions & 0 deletions scripts/update-homebrew-formula.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
#
# update-homebrew-formula.sh — render the Homebrew formula template with the
# version and SHA256 hashes for a given release, then commit it to the
# ClickHouse/homebrew-tap repo.
#
# Usage:
# ./scripts/update-homebrew-formula.sh <version>
#
# <version> Release version WITHOUT leading "v" (e.g. 0.3.1).
#
# Environment:
# HOMEBREW_TAP_DEPLOY_KEY Private SSH key registered as a deploy key with
# write access on ClickHouse/homebrew-tap (required).
# GITHUB_REPOSITORY Set by GitHub Actions (e.g. ClickHouse/clickhousectl).
#
# The script:
# 1. Downloads the 4 prebuilt archives from the GitHub release (immediately
# available, no mirror propagation delay) and computes SHA256 for each.
# 2. Renders homebrew/clickhousectl.rb.tmpl with the version + hashes.
# 3. Clones ClickHouse/homebrew-tap, replaces Formula/clickhousectl.rb,
# commits and pushes.
#
# The formula's download URLs point at builds.clickhouse.com (same bytes) so
# installs use the same distribution path as install.sh, cargo binstall, npm,
# and pip.
set -euo pipefail

VERSION="${1:?usage: $0 <version> (without leading v)}"
TAG="v${VERSION}"
TAP_REPO="ClickHouse/homebrew-tap"
TEMPLATE="homebrew/clickhousectl.rb.tmpl"

if [ -z "${HOMEBREW_TAP_DEPLOY_KEY:-}" ]; then
echo "error: HOMEBREW_TAP_DEPLOY_KEY is not set" >&2
exit 1
fi

# ── 1. Download archives and compute SHA256 ────────────────────────────────
# GitHub release assets are named clickhousectl-{target}-v{version}.tar.gz.
# The bytes are identical to builds.clickhouse.com, so we compute hashes from
# GitHub (available immediately after the release job) and use builds URLs in
# the formula for the canonical distribution path.
GH_BASE="https://github.com/ClickHouse/clickhousectl/releases/download/${TAG}"

declare -A TARGETS=(
[X86_64_LINUX_MUSL]="x86_64-unknown-linux-musl"
[AARCH64_LINUX_MUSL]="aarch64-unknown-linux-musl"
[X86_64_APPLE_DARWIN]="x86_64-apple-darwin"
[AARCH64_APPLE_DARWIN]="aarch64-apple-darwin"
)

declare -A HASHES

TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT

for key in "${!TARGETS[@]}"; do
target="${TARGETS[$key]}"
asset="clickhousectl-${target}-${TAG}.tar.gz"
url="${GH_BASE}/${asset}"
echo "Downloading ${asset}..."
curl -fsSL "$url" -o "${TMPDIR}/${asset}"
hash="$(shasum -a 256 "${TMPDIR}/${asset}" | awk '{print $1}')"
HASHES[$key]="$hash"
echo " sha256: ${hash}"
done

# ── 1b. Verify builds.clickhouse.com URLs are reachable ────────────────────
# The formula serves from builds.clickhouse.com, which is populated async
# from the GitHub release. Fail fast if the mirror isn't ready yet so the
# tap isn't published with 404 URLs — re-run this job after the async push
# completes.
BUILDS_BASE="https://builds.clickhouse.com/clickhousectl"
for key in "${!TARGETS[@]}"; do
target="${TARGETS[$key]}"
asset="clickhousectl-${target}-${TAG}.tar.gz"
builds_url="${BUILDS_BASE}/${asset}"
echo "Verifying ${builds_url}..."
if ! curl -fsSI "$builds_url" -o /dev/null; then
echo "error: ${builds_url} is not reachable." >&2
echo " builds.clickhouse.com may not have propagated this release yet." >&2
echo " Trigger the async push and re-run this job." >&2
exit 1
fi
echo " OK"
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Mirror SHA256 never verified

Medium Severity

The update script records SHA256 from GitHub release downloads but only sends HTTP HEAD to builds.clickhouse.com before publishing the formula. Homebrew installs fetch those builds URLs and verify the embedded checksum. If mirror bytes differ from GitHub, the tap can ship with hashes that fail every install.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9e8d2d6. Configure here.


# ── 2. Render the template ─────────────────────────────────────────────────
if [ ! -f "$TEMPLATE" ]; then
echo "error: template not found: ${TEMPLATE}" >&2
exit 1
fi

RENDERED="${TMPDIR}/clickhousectl.rb"
sed \
-e "s|{{VERSION}}|${VERSION}|g" \
-e "s|{{SHA256_X86_64_LINUX_MUSL}}|${HASHES[X86_64_LINUX_MUSL]}|g" \
-e "s|{{SHA256_AARCH64_LINUX_MUSL}}|${HASHES[AARCH64_LINUX_MUSL]}|g" \
-e "s|{{SHA256_X86_64_APPLE_DARWIN}}|${HASHES[X86_64_APPLE_DARWIN]}|g" \
-e "s|{{SHA256_AARCH64_APPLE_DARWIN}}|${HASHES[AARCH64_APPLE_DARWIN]}|g" \
"$TEMPLATE" > "$RENDERED"

echo "Rendered formula:"
head -5 "$RENDERED"

# ── 3. Clone the tap, update, commit, push ─────────────────────────────────
# Use a deploy key (SSH) for authentication — scoped to the tap repo only,
# not tied to any individual's GitHub account.
TAP_DIR="${TMPDIR}/homebrew-tap"
SSH_KEY="${TMPDIR}/deploy_key"
printf '%s\n' "$HOMEBREW_TAP_DEPLOY_KEY" > "$SSH_KEY"
chmod 600 "$SSH_KEY"

# Pin GitHub's published ed25519 SSH key so a MITM attacker can't substitute
# their own host key during the deploy-key session. See:
# https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
echo "github.com ssh-ed25519 AAAAC3NzC1lZDI1NTE5AAAAIOMqqnkVzq0S2G4Ue0hKQipxwlPGB0XzYxgO0GR6djQx" > "${TMPDIR}/known_hosts"

GIT_SSH_COMMAND="ssh -i ${SSH_KEY} -o UserKnownHostsFile=${TMPDIR}/known_hosts -o IdentitiesOnly=yes" \
git clone "git@github.com:${TAP_REPO}.git" "$TAP_DIR"
mkdir -p "${TAP_DIR}/Formula"
cp "$RENDERED" "${TAP_DIR}/Formula/clickhousectl.rb"

git -C "$TAP_DIR" add Formula/clickhousectl.rb
# Allow a no-op commit when the formula is already up to date (e.g. a
# re-run of the release job after the first push succeeded).
if ! git -C "$TAP_DIR" diff --cached --quiet; then
git -C "$TAP_DIR" -c user.name="github-actions[bot]" \
-c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -m "clickhousectl ${VERSION}"
else
echo "Formula unchanged; nothing to commit."
fi

GIT_SSH_COMMAND="ssh -i ${SSH_KEY} -o UserKnownHostsFile=${TMPDIR}/known_hosts -o IdentitiesOnly=yes" \
git -C "$TAP_DIR" push origin HEAD
echo "Pushed clickhousectl ${VERSION} to ${TAP_REPO}"
Loading