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.yml — dist 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
Summary
Today
clickhousectlis only installable via GitHub Releases (manual download) orcargo installfrom source. LLMs and humans alike reach forbrew install,pip install,npm install -g, andcargo 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 binstallreads existing GitHub Releases if archive naming follows the convention. Effectively free coverage once aligned..github/workflows/release.ymlto produce archives namedclickhousectl-{target}-v{version}.{ext}(.tar.gzfor unix,.zipfor Windows if/when added), each containing a folder of the same name with the binary inside. Today we upload bare binaries namedclickhousectl-{target}— needs to become an archived tree.clickhousectlcrate to crates.io (manifest already exists atcrates/clickhousectl/Cargo.toml) socargo binstall clickhousectlcan resolve it. Add a[package.metadata.binstall]block if the convention needs an explicit override.cargo binstall clickhousectlworks on a clean machine after the next tag.This also unblocks
mise/aqua/ubibackends, which consume GH Releases passively via the same naming convention.Phase 2 — Adopt
distfor brew + npm + shell installersdist(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 intoCargo.tomland a generated workflow.ClickHouse/homebrew-clickhousectltap repo for the generated formula to land in. Install becomesbrew install clickhouse/clickhousectl/clickhousectl.clickhousectl, and ideally@clickhouse/clickhousectlfor the scoped wrapper) before publishing.dist's generated workflow replacesrelease.yml—distowns 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.ymltriggered on the same tag.src/server.rsandsrc/version_manager/and is out of scope here. Restrict thedisttarget 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_TOKENdist's built-in
npmpublish job consumes a granularNPM_TOKENsecret — 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 jobsid-token: writeautomatically.Concrete shape:
installers = ["npm", ...]so dist still generates the wrapper package.publish-jobs = ["./publish-npm", ...]— replace the built-in npm publish job..github/workflows/publish-npm.yml(reusable workflow):setup-nodewithnode-version: ">=22.14.0"(npm CLI ≥ 11.5.1, required for OIDC),registry-url: "https://registry.npmjs.org", download the dist-built npm artifact, thennpm publish. Do not setNODE_AUTH_TOKEN— per npm docs, even an emptyNODE_AUTH_TOKENbreaks OIDC because npm uses the (empty) token instead of falling through to OIDC.ClickHouse/clickhousectl+ the dist release workflow filename + thepublish-npmjob.Phase 3 — PyPI via maturin
distdoes 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/zizmoras the bin crate) shipping a no-Python wheel that wraps the binary.pyproject.tomlat the repo root with[tool.maturin] bindings = \"bin\"andmanifest-path = \"crates/clickhousectl/Cargo.toml\". Reference: zizmor's pyproject.toml..github/workflows/release-pypi.ymlthat builds a matrix of prebuilt platform wheels viaPyO3/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.clickhousectlname on PyPI before first publish.Acceptance criteria
After all three phases, a fresh tag push produces working install paths for:
cargo binstall clickhousectlbrew install clickhouse/clickhousectl/clickhousectlnpm install -g clickhousectl(andnpx clickhousectl)pip install clickhousectlcurl --proto '=https' --tlsv1.2 -LsSf https://github.com/ClickHouse/clickhousectl/releases/latest/download/clickhousectl-installer.sh | shREADME
Installationsection updated to list all of the above.Out of scope
References
distinstaller docs: https://axodotdev.github.io/cargo-dist/book/installers/distcustom publish-jobs — how to swap dist's built-in npm publish for an OIDC-based reusable workflow.cargo-binstallarchive naming convention.