Skip to content

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

@sdairs

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
No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions