Skip to content

Distribute clickhousectl via cargo-binstall, dist (brew/npm), and PyPI #188

Description

@sdairs

Summary

Today clickhousectl is only installable via GitHub Releases (manual download) or cargo install from source. LLMs and humans alike reach for brew install, pip install, npm install -g, and cargo binstall — none of which work. Make the same prebuilt binary installable through those channels so the "obvious" install incantations succeed.

Implement in three phases, smallest-blast-radius first. Each phase is independently shippable.

Phase 1 — cargo-binstall compatibility

cargo binstall reads existing GitHub Releases if archive naming follows the convention. Effectively free coverage once aligned.

  • Adjust .github/workflows/release.yml to produce archives named clickhousectl-{target}-v{version}.{ext} (.tar.gz for unix, .zip for Windows if/when added), each containing a folder of the same name with the binary inside. Today we upload bare binaries named clickhousectl-{target} — needs to become an archived tree.
  • Publish the clickhousectl crate to crates.io (manifest already exists at crates/clickhousectl/Cargo.toml) so cargo binstall clickhousectl can resolve it. Add a [package.metadata.binstall] block if the convention needs an explicit override.
  • Verify: cargo binstall clickhousectl works on a clean machine after the next tag.

This also unblocks mise/aqua/ubi backends, which consume GH Releases passively via the same naming convention.

Phase 2 — Adopt dist for brew + npm + shell installers

dist (formerly cargo-dist) generates a Homebrew formula, npm wrapper package, shell installer (curl | sh), and PowerShell installer from one config, all triggered from the same tag push.

  • cargo install cargo-dist && dist init — picks installers and targets interactively, writes config into Cargo.toml and a generated workflow.
  • Create a ClickHouse/homebrew-clickhousectl tap repo for the generated formula to land in. Install becomes brew install clickhouse/clickhousectl/clickhousectl.
  • Squat the npm name (clickhousectl, and ideally @clickhouse/clickhousectl for the scoped wrapper) before publishing.
  • Decision: dist's generated workflow replaces release.ymldist owns the build matrix; running two workflows producing release artifacts from the same tag is a race. Preserve the existing multi-distro smoke-test matrix as a separate .github/workflows/smoke-test.yml triggered on the same tag.
  • Decision: no Windows target in this issue. Today we ship zero Windows binaries; adding it touches src/server.rs and src/version_manager/ and is out of scope here. Restrict the dist target list to the current four unix targets. Revisit in a follow-up if telemetry justifies.

Constraint: npm publishing must use OIDC trusted publishing, not a long-lived NPM_TOKEN

dist's built-in npm publish job consumes a granular NPM_TOKEN secret — no OIDC switch. Long-lived npm tokens are not acceptable here.

npm trusted publishing went GA in July 2025 and is the path forward. dist supports this via its custom publish-jobs mechanism: set publish-jobs = ["./publish-npm"] (custom reusable workflow) instead of ["npm"] (built-in). dist grants custom publish jobs id-token: write automatically.

Concrete shape:

  • Keep installers = ["npm", ...] so dist still generates the wrapper package.
  • Set publish-jobs = ["./publish-npm", ...] — replace the built-in npm publish job.
  • Add .github/workflows/publish-npm.yml (reusable workflow): setup-node with node-version: ">=22.14.0" (npm CLI ≥ 11.5.1, required for OIDC), registry-url: "https://registry.npmjs.org", download the dist-built npm artifact, then npm publish. Do not set NODE_AUTH_TOKEN — per npm docs, even an empty NODE_AUTH_TOKEN breaks OIDC because npm uses the (empty) token instead of falling through to OIDC.
  • Configure the trusted publisher on npmjs.com pointing at ClickHouse/clickhousectl + the dist release workflow filename + the publish-npm job.
  • One-time seed: npm requires the package to exist before its OIDC settings can be configured, so the maintainer publishes once with a granular token, configures the trusted publisher, then revokes the token. All subsequent publishes go through OIDC and automatically generate provenance attestations.

Phase 3 — PyPI via maturin

dist does not generate PyPI wheels (tracked upstream, no ETA). Add a separate maturin-based workflow, copying zizmor's setup almost verbatim — zizmor is a Rust workspace with the same shape (crates/zizmor as the bin crate) shipping a no-Python wheel that wraps the binary.

  • Add pyproject.toml at the repo root with [tool.maturin] bindings = \"bin\" and manifest-path = \"crates/clickhousectl/Cargo.toml\". Reference: zizmor's pyproject.toml.
  • Add .github/workflows/release-pypi.yml that builds a matrix of prebuilt platform wheels via PyO3/maturin-action (manylinux 2_28 x86_64/aarch64, musllinux, macOS x86_64+arm64, Windows if Phase 2 added it) + sdist. Reference: zizmor's release-pypi.yml.
  • Configure PyPI Trusted Publishing for this repo (no API tokens in CI).
  • Squat the clickhousectl name on PyPI before first publish.

Acceptance criteria

After all three phases, a fresh tag push produces working install paths for:

  • cargo binstall clickhousectl
  • brew install clickhouse/clickhousectl/clickhousectl
  • npm install -g clickhousectl (and npx clickhousectl)
  • pip install clickhousectl
  • curl --proto '=https' --tlsv1.2 -LsSf https://github.com/ClickHouse/clickhousectl/releases/latest/download/clickhousectl-installer.sh | sh
  • The existing direct GH Release download path still works.

README Installation section updated to list all of the above.

Out of scope

  • WinGet, Scoop, Homebrew core, apt/deb, snap, AUR, Docker. Revisit if telemetry justifies.
  • Renaming archives in a way that breaks existing direct-download docs/links without a redirect note in README.

References

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions