One CLI for all your CI. Python, Rust, TypeScript, Go — same tool locally and in GitHub Actions. No bash scripts, no composite actions, no submodules.
Version-first single-run pipeline. A Release: true git trailer on
your head commit is the single signal that a push is a release. The CI
run predicts the next version up front, stamps it into Cargo.toml /
VERSION / pyproject.toml / package.json before the build, then tags
+ uploads to all configured registries — all in one workflow. No second
"catch-up" build, no version-stamp drift between binary and tag.
Tag-on-publish. A git tag exists iff the artefact is in the registry. Aligns with kubernetes / rust / python OSS conventions. No more orphan tags from "tag every fix:, publish later" mode.
100% FOSS pipeline. Every artefact publishes to public registries:
crates.io, PyPI, npm, GHCR, GitHub Releases, and Cloudflare R2
(downloads.hyperi.io). The legacy publish.target knob in
.hyperi-ci.yaml is read by nothing and warns. The only switch left to flip for full open-source visibility
is making the source repos themselves public.
See docs/migration/onboarding.md for the v1 -> v2
migration. Pre-v2.1.4 docs that mention private-registry targets, the
destinations_internal block, or target: internal are historical
only -- those code paths have been removed.
You get:
- One command before every push:
hyperi-ci check - Same quality / test / build runs locally as in CI — no "works on my machine"
- Automatic versioning via semantic-release (just use conventional commits)
- One-shot release:
hyperi-ci push --release(single CI run, single tag, single registry upload) - Commit message validation that actually helps ("Computer says no.")
Your repo gets:
- A 5-line GitHub Actions workflow (calls our reusable workflow)
- A Makefile with
make check,make quality,make test,make build - Semantic-release config that just works
- A commit hook that catches bad messages before they hit CI
hyperi-ci is the preventative layer: gitleaks blocks a secret before the push, commit validation blocks a bad message before it lands, quality gates block a broken build before it merges. GitHub's native security features (secret scanning alerts, CodeQL code scanning, Dependabot) are the detective layer - they find what already landed, or what arrived from upstream in repos hyperi-ci never runs on (external forks, mirrors). Run both: hyperi-ci stops you making the mess, the GitHub side catches the mess you inherited.
uv tool install hyperi-cicd my-project
hyperi-ci init # Auto-detects language, generates everything
git config core.hooksPath .githooks # Activate commit validation hookThis creates .hyperi-ci.yaml, Makefile, .github/workflows/ci.yml,
and .githooks/commit-msg. Commit and push. No .releaserc is scaffolded -
version bumps follow semantic-release's own default rules (a repo commits a
.releaserc.json only for a genuine exception).
# 1. Write code
# 2. Check before pushing (mandatory)
hyperi-ci check # Quality + test
hyperi-ci check --quick # Quality only (fast)
hyperi-ci check --full # Quality + test + build
hyperi-ci check --strict # Also fail on warn-tier findings (zero warnings)
# 3. Commit (hook validates your message format)
git commit -m "fix: resolve timeout in auth handler"
# 4. Push (ships nothing -- no tag, no release)
hyperi-ci push
# That's it. Quality and test run if the pushed range is release-worthy.
# Nothing compiles, no image is built, no tag, no registry is touched.You opt in to a release explicitly. Two ways:
git commit -m "fix: handle empty tenant id"
hyperi-ci push --release # --publish still worksThis amends your head commit with the Release: true git trailer, then
pushes. The single CI run:
- Reads the trailer in setup → declares this a release run
- Runs
npx semantic-release --dry-runto predict the next version (e.g. v1.5.4) - Stamps that version into Cargo.toml + VERSION before build
- Builds (binary now embeds CARGO_PKG_VERSION = 1.5.4)
- Builds + pushes container image to GHCR (multi-arch)
- Runs
npx semantic-releasefor real → creates tag, CHANGELOG commit - Uploads binaries to GitHub Release + R2; publishes to crates.io / PyPI / npm
One workflow run, one tag, one release.
Sometimes the work you want to ship is a docs-only PR, a refactor, or
just a "force a rebuild" — none of which warrant a semver bump under
conventional-commits rules. To avoid having to invent a fake fix:
commit:
hyperi-ci push --bump-patch # +0.0.1 even with docs:/chore: commits
hyperi-ci push --bump-minor # +0.1.0Either flag implies --release. Under the hood, the tool adds an empty
fix(release): force patch bump (or feat(release): force minor bump)
commit on top of HEAD with the Release: true trailer. semantic-release
sees that and cuts the version. Honest git history: the marker commit
explicitly states "this is a forced bump."
Major bumps are deliberately excluded from this flag — they require a
human-written BREAKING CHANGE: footer per HyperI commit-type discipline.
If a previous release run failed mid-way (e.g. registry timeout) and you want to retry without re-tagging:
hyperi-ci release v1.5.4 # `publish` still worksThis dispatches a workflow_dispatch event for the tag and runs
build → container → publish from the existing tagged source.
hyperi-ci release --list # see unreleased tagsA plain hyperi-ci push ships nothing and builds nothing -- run-build is
release-only, so no cargo compile and no container job runs at all. Quality
and test run when the pushed range is release-worthy (it carries a feat:,
fix: or perf:), and skip entirely when it is not.
So the default state of main is "landed, and tested if it was
release-worthy" -- not "built and ready to ship". You release explicitly by
running hyperi-ci push --release on the next conventional commit.
Conventional commits are enforced by a git hook and CI. The format:
<type>: <description>
<type>(scope): <description>
Get it wrong and you'll hear about it:
Computer says no.
Unknown commit type: "yolo"
Did you mean one of these?
style — code formatting, linting, cosmetic changes
spike — experimental, throwaway investigation
Types that bump the version: feat: (minor), fix: (patch), perf:,
hotfix:, security: / sec: (all patch).
Types that don't: docs, test, refactor, chore, ci, build,
deps, style, revert, wip, cleanup, data, debt, design,
infra, meta, ops, review, spike, ui.
Full list: hyperi-ci check-commit --list
Control where artifacts go with one line in .hyperi-ci.yaml:
release:
channel: release # alpha | beta | releaseThe namespace was publish: and still works -- a publish: block folds into
release: at load time and names each key it moved. Nothing has to change to
keep building.
Every artefact publishes to the OSS registry stack:
| Artefact type | Destination |
|---|---|
| Containers | GHCR (ghcr.io/<org>) |
| Rust crates | crates.io |
| Python packages | PyPI |
| npm packages | npmjs.com |
| Binaries (per-tag) | GitHub Releases |
| Binaries (web-downloadable) | Cloudflare R2 (downloads.hyperi.io) |
The publish.target config field is read by nothing, and a warning names it
until it is deleted. The publish-target workflow input is still declared so
existing callers keep starting, and nothing reads it either. The only
remaining toggle for full FOSS visibility is making the source repos
themselves public on GitHub.
A config key hyperi-ci no longer reads gets one warning per key:
<key> is no longer read by hyperi-ci and can be deleted. It never fails the
build. The list is REMOVED_KEYS in src/hyperi_ci/vocabulary.py.
Pre-release channels (alpha, beta) flag GH Releases as
prerelease and prefix R2 paths. Stable releases require channel: release.
| Channel | GH Release | R2 path |
|---|---|---|
alpha |
Prerelease | /{project}/alpha/v1.3.0/ |
beta |
Prerelease | /{project}/beta/v1.3.0/ |
release |
GA | /{project}/v1.3.0/ + /{project}/latest/ |
alpha -> beta -> release
Each step is a one-line change to release.channel in .hyperi-ci.yaml.
No code changes, no workflow changes.
| Command | What it does |
|---|---|
hyperi-ci check |
Pre-push validation (quality + test) |
hyperi-ci check --quick |
Quality only |
hyperi-ci check --full |
Quality + test + build |
hyperi-ci check --strict |
Also fail on warn-tier findings - see docs/quality-gate-overrides.md |
hyperi-ci push |
Push -- ships nothing, quality + test if release-worthy |
hyperi-ci push --release |
Stamp Release: true trailer, push, single-run release |
hyperi-ci push --bump-patch |
Force +0.0.1 release even with no-bump commits |
hyperi-ci push --bump-minor |
Force +0.1.0 release even with no-bump commits |
hyperi-ci push --no-ci |
Push with [skip ci] (skip CI entirely) |
hyperi-ci release <tag> |
Retroactive: dispatch a release on an existing tag |
hyperi-ci release --list |
List unreleased version tags |
hyperi-ci run quality|test|build|container|publish |
Run a single stage locally |
hyperi-ci check-commit --list |
Show all accepted commit types |
hyperi-ci detect |
Show detected language |
hyperi-ci config |
Show merged config |
hyperi-ci trigger [--watch] [--input K=V] |
Trigger CI workflow; --input once per workflow_dispatch input |
hyperi-ci watch [RUN_ID] [--workflow NAME] |
Watch HEAD's own CI run (default 3600s; --timeout 0 disables) |
hyperi-ci rerun [RUN_ID] [--all] |
Re-run a run's failed jobs — for infra incidents, not for repeating a flake |
hyperi-ci logs [RUN_ID] [--workflow NAME] [--failed] |
Show CI run logs for HEAD's own run |
hyperi-ci init |
Scaffold a new project |
hyperi-ci update |
Update to the channel's release (see autoupdate) |
hyperi-ci autoupdate [status|channel live|stable|freeze|unfreeze] |
Show/set how the CLI updates itself |
watch and logs resolve the run built from the commit at HEAD, pinned
to the workflow declared in the project's .github/workflows/ci.yml -
never "whichever ran last". Name another with --workflow; where the
choice is still ambiguous they refuse and list the candidates.
hyperi-ci release is the canonical verb. hyperi-ci publish still works and
warns. An earlier notice deprecated release for removal; that was the wrong
way round and is withdrawn -- release is the name that stays.
Every old spelling keeps working: the Publish: true trailer, a publish:
config block, push --publish, and hyperi-ci publish. Each warns and names
its replacement. No project has to change anything to keep building.
Your Project hyperi-ci
├── .github/workflows/ci.yml ├── .github/
│ (5 lines — calls reusable) │ ├── workflows/
├── .hyperi-ci.yaml │ │ ├── rust-ci.yml (per-language)
├── .githooks/commit-msg │ │ ├── python-ci.yml (per-language)
└── Makefile │ │ ├── go-ci.yml (per-language)
│ │ ├── ts-ci.yml (per-language)
│ │ └── _release-tail.yml (shared: container + publish)
│ └── actions/
│ └── predict-version/ (shared composite)
└── src/hyperi_ci/
├── cli.py (entry point)
├── dispatch.py (stage router)
├── push.py (push --release)
├── release/ (binaries, charts, dispatch)
├── container/ (docker build/push)
├── deployment/ (contract / artefact gen)
└── languages/ (per-language stage handlers)
flowchart LR
P[plan] -->|release-worthy| Q[quality]
P -->|release-worthy| T[test]
P -->|not release-worthy| S[everything skips]
No build, no container, no tag, no registry upload. A release-worthy merge is
TESTED, not shipped -- run-build is release-only, so nothing compiles and no
image is produced until you release.
flowchart LR
Q[quality] --> S["setup<br/>(predict next-version)"]
T[test] --> S
S --> B["build<br/>(stamp version)"]
B --> C["container<br/>(push to registries)"]
C --> TP["tag-and-publish<br/>(semantic-release + run publish)"]
One workflow, one tag, one release.
.hyperi-ci.yaml in the project root. Cascade (highest wins):
CLI flags -> ENV vars (HYPERCI_*) -> .hyperi-ci.yaml -> defaults.yaml -> hardcoded
language: rust # Auto-detected if omitted
release: # was `publish:` -- still accepted, warns
enabled: true
channel: release # alpha | beta | release
build:
strategies: [native]
rust:
targets:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
quality:
gitleaks: blockingThe container stage builds from the repo's own Dockerfile and nothing else.
With no Dockerfile, enabled: auto skips (with a warning for a runnable
project) and enabled: true fails the stage.
Images push to GHCR (ghcr.io/hyperi-io/<app>). Tags:
- Push to main with
Release: true::vX.Y.Z+:latest+:sha-abc1234 - workflow_dispatch on tag: same tag set on the existing tagged source
Enable in .hyperi-ci.yaml:
release:
container:
enabled: auto # auto | true | false
platforms: [linux/amd64, linux/arm64]| Language | Quality | Test | Build | Publish |
|---|---|---|---|---|
| Python | ruff (lint, format, S rules), ty, pip-audit | pytest | uv build | uv publish (PyPI) |
| Rust | cargo fmt, clippy, audit, deny, feature_matrix | cargo test/nextest | cargo build (cross) | cargo publish (crates.io) |
| TypeScript | eslint, prettier, tsc, npm audit | vitest/jest | npm/pnpm build | npm publish (npmjs / GH Packages) |
| Go (beta) | gofmt, go vet, golangci-lint, gosec | go test -race | go build (cross) | go proxy, gh release |
Go support is beta — functional but not battle-tested to the same degree as Rust, Python, and TypeScript. Verify results carefully on production pipelines.
Per-language version stamping (release runs only):
| Language | Stamps |
|---|---|
| Rust | Cargo.toml [package].version (and [workspace.package].version for workspaces) + VERSION |
| Python | pyproject.toml [project].version + VERSION |
| Go | VERSION (consumed via -ldflags "-X main.Version=...") |
| TypeScript | package.json (via npm version --no-git-tag-version) + VERSION |
Rust projects automatically get a cargo hack --each-feature clippy --lib
pass during quality checks (check in place of clippy when clippy is disabled). This catches feature-gating bugs where a module behind
feature X uses a crate only declared by feature Y — without this check,
transitive deps from other features mask the bug until a downstream consumer
enables only X.
Default behaviour (always on, zero config): runs the bare-crate pass
(cargo clippy --no-default-features --lib) plus the each-feature pass.
Neither pass edits Cargo.toml or Cargo.lock. Under cargo's feature resolver 1 (edition 2018 or older, or a virtual workspace with no resolver key) a dev-dependency's features reach the library build and can hide the bug, so the matrix warns and asks for resolver = "2" or later.
Opt out (requires a reason; CI fails if reason is missing):
quality:
rust:
feature_matrix:
enabled: false
reason: "tracked in dfe-loader#87, remediating 2026-04-18"Rust projects with C/C++ dependencies (librdkafka, openssl, zstd) are
supported. The build handler auto-detects native -dev packages, downloads
cross-arch equivalents into a private sysroot, and sets all compiler/linker
environment variables. Configure targets in .hyperi-ci.yaml:
build:
rust:
targets:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnuPush to main without Release: true builds amd64 only (validation).
Release runs build the full matrix.
- Version-first — predict version up front, stamp before build. No catch-up rebuild.
- Tag-on-publish — git tags exist iff the artefact is in the registry.
- No silent skips — fail loud on broken handoffs (missing artefacts, missing handlers, etc.).
- No bash — all CI logic is Python.
subprocess.run()with list args. - Semantic release — push to main with
Release: truetriggers a single-run release. - uv for everything — venv, sync, lock, tool install, build.
- Cross-platform — Linux (CI) and macOS (dev).
- Self-hosting — hyperi-ci uses itself for its own CI.
CONTRIBUTING.md has the commit format, the DCO sign-off, and
what a local hyperi-ci check green does and does not promise.
It also carries a For coding agents section, which is binding rather than
advisory: the boy-scout rule for the ci-test-* fixtures, the DO-NOT-FIX
exception for planted failures, the fixture-git wrapper, and why a green test
suite does not prove a workflow change. Read it before touching a fixture.
This software is licensed under the Business Source License 1.1 (BUSL-1.1). See LICENSE for terms and COMMERCIAL.md for commercial use; each version converts to Apache 2.0 three years after its release. (c) 2026 HYPERI PTY LIMITED.