Horu CLI
MVPThe portable game build toolkit.
One Go binary and one horu.yaml, dropped into a build lane you already have. It checks the environment before an expensive stage, runs the target matrix, collapses engine log noise into a failure anchor, reports what the cache did, sanitises the package, writes manifests with checksums, and emits a report. Nothing above this line requires a Horu server.
Quickstart
horu init --engine unity --platform windows macos --template starter
horu build --dry-run
horu build --platform windows --configuration release
horu report --format text,json,html --out ./out/horu
How to read the MVP badge
The command surface above is implemented and exercised by our own smoke tests. The binary is not on a public download page yet, and diagnostics beyond the baseline rule packs are still shallow. Flags and configuration keys may change before 1.0. If that is an acceptable trade for the problem you have, evaluate it now and tell us where it breaks. If it is not, the roadmap will tell you when to look again.
Mental model
Horu CLI is a build engine, not an API client.
That single sentence decides most of the design. A thin client would need a server, an account and a network path before it did anything, and would fail closed when any of those were missing. An engine does the work locally and treats upload as an extra.
It also means the comparison set is Terraform, Make and Docker rather than a CI plugin: a small number of stable commands, with the interesting behaviour in flags and a config file. Adding a feature should usually mean adding a flag, not a seventh verb.
And it is why Horu CI is not a separate implementation. The runner executes the same build modules, so a run started by a person on a laptop and a run started by a queue produce records of the same shape.
Command surface
Six commands, kept deliberately small.
Help output is treated as public surface area: horu --help gives a grouped overview, and horu <command> --help documents that command's flags. Both are updated whenever behaviour changes.
| Command | What it does |
|---|---|
horu init |
Create a project horu.yaml from an engine, template and platform profile, and run environment checks against the selected profile. |
horu build |
Execute configured targets from build.targets, filtered by host OS compatibility, with cache restore and save attempted by default. |
horu run |
Wrap a build stage. Capture timing, logs and exit state, and classify the failure if there is one. |
horu cache |
Restore, save, invalidate and report cache behaviour, including hit and miss state, miss reason, size and duration. |
horu package |
Sanitise, validate, checksum and manifest build outputs before they are published or promoted. |
horu report |
Emit text, JSON, HTML and CI-native annotations, with optional upload to Horu CI. |
Deliberately not commands yet
horu login is only meaningful when connecting to a Horu CI server, and local value must not require a login. Upload stays a flag on report and package until its behaviour is complex enough to deserve a top-level verb.
Why init and not doctor
doctor reads as an informal support tool. init communicates the real job: verifying the machine before an expensive or release-critical operation begins. Naming is part of the interface.
Configuration
Building up horu.yaml, one block at a time.
YAML rather than a new DSL, because CI users already read it and game teams should not have to learn a language to describe a build. Below is the same file assembled in five steps.
-
01 project
Name the project and the engine.
The engine selects which rule packs, scaffold templates and package defaults apply.
project: name: space-racer engine: unity -
02 profiles
Declare what has to exist before a long stage starts.
Required tools, required environment variables and a disk floor. This is what
horu initchecks. Profile selection is automatic when omitted:ciwhenCI=trueand that profile exists, otherwisedefault.profiles: unity-windows: tools: required: - git - unity - rsync environment: required: - UNITY_LICENSE disk: min_free_gb: 40 -
03 logs
Collapse the noise, then classify what is left.
collapse_patternsfolds away repeating engine chatter.failure_packsselects the classifiers used to find a failure anchor and suggest a remediation.logs: unity: engine: unity collapse_patterns: - "^Refreshing native plugins" - "^Asset import worker" failure_packs: - unity-licensing - unity-il2cpp - unity-addressables -
04 cache
Derive the key from files you name.
Paths are cached per entry rather than as one archive.
excludekeeps high-churn directories out of the key and out of the payload. When an Accelerator endpoint is configured, its flags are passed to Unity while Horu still owns restore and save.cache: unity-library: paths: - Library exclude: - Library/Bee - Library/ShaderCache key: files: - Packages/manifest.json - ProjectSettings/ProjectVersion.txt - ProjectSettings/EditorBuildSettings.asset accelerator_endpoint: 10.0.0.15:10080 accelerator_namespace_prefix: space-racer-macos accelerator_enable_download: true accelerator_enable_upload: true compression: none -
05 packages
Decide what is allowed to leave the building.
removestrips junk.forbidfails the run.mode: failmakes that the default posture rather than a warning nobody reads.packages: steam-release: path: Builds/Windows mode: fail remove: - "**/.DS_Store" - "**/Thumbs.db" - "**/*.tmp" - "**/*.log" forbid: - "**/InternalOnly/**" - "**/*Development*.json" manifest: true checksum: sha256
Existing CI
Keep the orchestrator. Delete the glue.
In connected use, the Jenkinsfile and the workflow YAML still exist; they just get thinner, because the defensive scripting moves into commands that are versioned and tested. Below are the two integrations we document today.
pipeline {
agent any
stages {
stage('Preflight') {
steps { sh 'horu init --engine unity --platform windows macos' }
}
stage('Build Matrix') {
steps { sh 'horu build --platform windows --configuration release' }
}
stage('Build') {
steps { sh 'horu run --stage build --profile unity-il2cpp -- ./ci/build.sh' }
}
stage('Package') {
steps {
sh 'horu package --path ./Builds/Windows --profile steam-release \
--sanitize --manifest --validate'
}
}
}
post {
always {
sh 'horu report --format text,json,html --out ./out/horu'
archiveArtifacts artifacts: 'out/horu/**'
}
}
}
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- run: horu init --engine unity --platform windows macos
- run: horu build --platform windows --configuration release
- run: horu run --stage build --profile unity-il2cpp -- ./ci/build.ps1
- run: |
horu package --path ./Builds/Windows --profile steam-release \
--sanitize --manifest --validate
- if: always()
run: horu report --format github,json,html --out ./out/horu
TeamCity and GitLab follow the same shape: the orchestrator supplies the agent and the trigger, Horu CLI supplies the build behaviour and the records. On GitHub the github report format puts the failure anchor on the run as an annotation.
Cache behaviour
The defaults, stated plainly.
Caching is where build tools most often trade correctness for a graph that looks good. These are the choices we made and the reasoning behind each, so you can disagree with them on purpose rather than discover them in an incident.
- on by default
- Restore and save are attempted whenever cache profiles are present in the config.
horu build --cleandisables both for a single run, which is how you take a cold baseline. - path-split entries
- One entry per configured path instead of a single monolithic archive, so a change in one tree does not invalidate the rest.
- warm-workspace no-op
- Restore is skipped when the paths already exist and are compatible. Save is skipped when that key is already stored. Doing nothing correctly is the fastest available option.
- compression: none
- The default, because archive and extract overhead frequently exceeds the transfer saved on a large Unity
Librarytree. - exclude patterns
- High-churn directories such as
Library/Beeare kept out deliberately. Caching a directory that always changes is a cost with no return. - Accelerator hybrid
- An Accelerator endpoint and Horu caching are not mutually exclusive. When the endpoint is configured, Horu passes the Accelerator flags through and still owns restore and save.
- workspace_affinity_key
- Printed on every build so an external scheduler can route a repeat job back to a machine that already holds a warm workspace.
Output contract
Every command writes into one report directory.
This is the part we treat as an interface rather than an implementation detail, because pipelines and later products depend on it.
Required, always
- A console summary short enough to read in a CI log
- JSON records suitable for upload
- Stable exit codes
- Artifact and package manifest files
Optional, on request
- A local HTML report
- GitHub Actions annotations
- Upload of the same records to Horu CI
Non-negotiable
The CLI stays useful when there is no Horu CI server in the picture. Upload is an additive capability, never a prerequisite. A studio that runs Horu CLI for a year and never opens an account should still have got the value it evaluated.
Boundary
What the CLI owns, and what it refuses to.
Horu CLI owns
- Local build instrumentation
- Profile execution and environment verification
- Rule-pack behaviour for logs and packages
- Report generation in every supported format
Horu CI owns
- Orchestration, runners and scheduling
- Build history and dashboards
- Artifact storage and retention
- Team-level analytics and release gates
Existing CI systems stay valid launchers throughout, and existing source control stays authoritative unless a narrow slice of source context is needed for a build record.
Bring us your worst build lane.
Not the tidy one. The lane where failures are noisy, cache behaviour is a mystery, and the workflow file has a comment in it that says do not touch. We onboard a small number of studios at a time, and early reports are read by the people who wrote the tool.