This guide describes the local workflow for contributing to the Hive Go codebase on the v5 branch.
- Git and GitHub CLI (
gh) for normal issue and PR workflows. - Go
1.26.6, as declared bysrc/go.mod— when the two disagree, go.mod wins. - Docker or Podman if you are exercising containerized contributor relay or deployment paths.
tmuxfor local agent/contributor workflows that attach CLIs to terminal sessions.justif you use the repository's helper recipes (brew install juston macOS, or install from thejustproject for your platform).- Optional agent CLIs depending on what you test locally: Claude Code, GitHub Copilot/
gh, Bob, Goose, Codex, Pi, or Antigravity.
git clone https://github.com/hivecommons/hive.git
cd hive
git switch -c <topic-branch> origin/v5Use v5 as the PR base for ordinary Hive development (v4 is feature-frozen and takes security/critical fixes only — see CONTRIBUTING.md). Rebase or recreate your branch from a fresh origin/v5 before opening or updating a PR.
src/ is a two-module Go workspace (src/go.work): the root module
github.com/hivecommons/hive and the nested Crux agent-runtime module
github.com/hivecommons/hive/crux at src/crux/ (see
src/docs/design/crux.md). In workspace mode
./... covers the root module only; the Crux module is addressed from
src/ as ./crux/.... Build every package in both modules:
cd src
go build ./... ./crux/...To build only the main Hive binary during quick iteration:
cd src
go build ./cmd/hiveRun both modules' tests from src/:
cd src
go test ./... ./crux/...(cd src/crux && go test ./... is the same Crux run from inside the module;
GOWORK=off from src/ builds the root module with the replace in
src/go.mod and is what the container images do.)
The src/test/ package holds the inception e2e/regression suite. Those tests
talk to a live hive over the network and sit behind the integration build
tag, so the command above compiles the package but runs none of them — a plain
go test ./... will never exercise this suite, and a green run says nothing
about it. To actually run it:
cd src
HIVE_URL=http://<host>:<port> HIVE_TOKEN=<token> go test -tags integration ./test/...The suite skips itself (exit 0) when HIVE_URL is unset or the endpoint does
not answer a fast TCP dial, so a passing run without those variables means
"skipped", not "verified". Run it when you touch inception code; the normal
go test ./pkg/... loop is enough otherwise. See src/test/doc.go for the
package's own description.
If a local environment dependency prevents a full run, include the failing package and error summary in the PR and still run the narrower package tests affected by your change.
Useful narrower loops:
cd src
go test ./pkg/...
go test ./crux/...
go test ./cmd/hivego test ./... is not what CI runs. Every shard in
.github/workflows/v2-tests.yml — the
workflow that publishes the required test check — uses the same three flags:
cd src
go test ./pkg/hub -short -race -count=1 -run '<1/4 slice>' # test (hub i/4)
go test ./pkg/dashboard -short -race -count=1 -run '<1/4 slice>' # test (dashboard i/4)
go test ./pkg/scheduler -short -race -count=1 -run '<1/3 slice>' # test (scheduler i/3)
go test ./crux/agent -short -race -count=1 -run '<1/5 slice>' # test (agent i/5)
go test $PKGS -short -race -count=1 # test (rest i/4)The workflow shards for wall-clock only: pkg/hub, pkg/dashboard,
pkg/scheduler and crux/agent are each sliced across several jobs by test
function (they are single packages too slow to run whole), and everything
else from
go list ./pkg/... ./cmd/... ./internal/... ./crux/... is
partitioned into balanced buckets. Every shard also runs with -v and posts
its slowest test functions to the job's step summary — look there before
reaching for a local profile when the gate feels slow. Scheduled runs add
-shuffle=on (the seed is in the log); the PR gate does not, so an
order-dependent test shows up as a filed issue rather than as a red PR. The union of the shards is the whole of ./pkg/...,
./cmd/..., ./internal/... (the shared test helpers in
internal/testutil) and ./crux/... (the Crux module), so what changes
between your loop and the gate is the flags, not the coverage. To reproduce
the gate locally in one unsharded run:
cd src
go test ./pkg/... ./cmd/... ./internal/... ./crux/... -short -race -count=1What each flag changes:
-raceis the one most likely to catch a bug you would otherwise ship. A data race or a lock-ordering mistake usually passes a non-race run every time and only surfaces under load in production. This repository has repeatedly paid for that: see thewriteMu/WriteControlreasoning insrc/pkg/dashboard/contribute_ws.go, which exists because concurrent writers to a single WebSocket produced real mutex re-entrancy deadlocks. If you run only one thing before pushing, run the race build of the packages you touched.-shortsetstesting.Short(), so any test guarded byif testing.Short() { t.Skip(...) }does not run in CI. This cuts both ways, and both directions bite. A slow test you write without aShortguard runs on every shard and adds to the gate's wall clock. A test you write behind aShortguard is never executed by the gate at all — a green required check says nothing about it, exactly as a plain run says nothing about theintegrationsuite above. Guard slow setup, not the assertion that proves your fix.-count=1disables the test result cache. Without it, an unchanged package reports its previous verdict instead of re-running, which is precisely what you do not want when you are trying to reproduce a failure or chase a flake.
Two flags that appear in CI but are not part of the PR gate:
-timeout 600sis used only by the hourly coverage cron (.github/workflows/coverage-hourly.yml), which runs the suite unsharded. The PR shards pass no-timeout, so they take Go's default of 10 minutes per shard binary. The practical bound on a shard is therefore the same 10 minutes, applied to a much smaller slice of the suite.-coverprofileis added by each shard to feed a per-package coverage report. Coverage is scored, but it is not the required merge gate; a failing test is.
The v2 CI workflow runs Go build/vet, deployment and shell contracts, and
Node/runtime contracts as three independent lanes. The protected
build-and-test check aggregates all three and fails if any lane fails,
is cancelled, or unexpectedly skips. This changes scheduling, not which
contracts run. The separate race/shuffle matrix and per-package coverage
checks remain required and unchanged.
As before, a transient build scope-preflight failure falls back to executing
the full actual build/contract/runtime lanes. Scope failure is not itself a
new build gate: all three actual lanes must succeed, and skipped or cancelled
lanes cannot create a green aggregate.
The Podman cleanup contract reads every tracked file once, checking all
forbidden literal patterns together rather than spawning a grep for each
pattern in each file. Go security CI caches the version-pinned gosec binary;
the scan itself still runs every time. Gosec's recursive discovery includes
nested modules, so its ./... scan covers Crux without scanning it twice.
Other Go tools still need both ./... and ./crux/....
Gosec retains the four package-loading workers established by #16045 rather
than the host CPU count (which can exceed a runner container's CPU quota).
The structural contract checks this explicit bound against the runner limit.
The coverage scorer only lists packages and reads shard profiles; it does not
archive shared Go caches in a serial post-step after scoring.
Package discovery runs alongside the scope preflight; test shards still wait
for both, and neither test nor coverage can pass a failed scope verdict.
Out-of-scope PRs may perform discovery but still execute no test shards.
The Docker PR freshness reload also keeps the matrix platform, so an ARM64
job reuses and probes its ARM64 build instead of rebuilding a host-architecture
image.
Public PR image builds default to the free ubuntu-24.04-arm native runner
already used by the Podman ARM64 lane; an explicit HIVE_ARM64_RUNNER override
still wins. Private repositories and release/push events retain their existing
runner selection, and QEMU is installed only when the selected host needs it.
Measure PR turnaround from workflow creation to the last required check completing, including runner queues and aggregate/coverage jobs, not just the slowest test shard's execution. Cache misses and shared runner load can change this substantially; a short shard alone is not a five-minute pipeline.
On a pull request the gate does not always run every package. The
list-packages job diffs the PR merge commit against its base parent and runs
.github/scripts/affected-go-packages.sh,
which selects:
- the packages containing a changed
.gofile; - their reverse dependencies, including test-only imports (from
go list -test), for changes to non-test files — a_test.gochange is never compiled into importers; - packages whose tests name a changed file or directory as a string literal (tests that read another package's source as text);
./internal/...,pkg/configandpkg/rotation, whose tests scan the whole source tree.
It falls back to the full suite for anything it cannot reason about: a
change outside src/ other than docs/, changelog.d/ and CHANGELOG.md
(workflows, .github/scripts/, bin/, config/, Justfile, Makefile,
dashboard/openapi.json); src/go.mod/src/go.sum; src/scripts/; the
shared harness under src/internal/; any non-Go file under src/ (embedded
static assets, docs, policies, fixtures — several are read across packages);
any testdata/ file; a .go file outside a module package; and any git or
go list failure. The repository variable HIVE_AFFECTED_TEST_SELECTION=off
forces the full suite on PRs without a code change.
Every shard job still runs: a hub, agent, dashboard-shuffle or rest lane with
nothing selected logs why and succeeds without testing, so the required test
and coverage contexts never wait on a missing check. On a partial run the
coverage job scores only the selected packages, each against its unchanged
floor, fails if a selected package produced no result, labels the ./pkg/...
total as selection-only, and takes the no-test-files gate from the whole tree.
It never writes floors or baselines. The safety net is unchanged: every push
to a release branch and every scheduled run executes the full suite, and
coverage-hourly.yml scores it.
Neither the PR gate nor the cron runs ./test/...: both enumerate
./pkg/... ./cmd/... ./internal/... explicitly, and the integration suite additionally needs
the integration build tag and a live hive, as described above.
Run gofmt on Go files you edit:
gofmt -w path/to/file.goThe v5 CI workflow runs go vet ./... ./crux/... after building the Hive binary. Reproduce that check locally from the Go workspace:
cd src
go vet ./... ./crux/...NOTICE lists every Go module compiled into the shipped binaries. It is
generated, not hand-edited, so any change to src/go.mod or src/go.sum
— including a Dependabot version bump — makes it stale and fails the
notice-drift job ("NOTICE matches the module graph") in
.github/workflows/go-security-analysis.yml.
Regenerate and commit it:
bash src/scripts/generate-notice.sh # writes NOTICE at the repo rootThree things that will otherwise cost you a CI round trip:
- Commit the output verbatim. The check is byte-exact. Do not reformat it, do not strip trailing whitespace — several dependency licences contain trailing spaces on their own lines, and removing them produces a permanent diff against what CI generates.
- The generator needs the module's Go toolchain.
src/go.modpins a specific version; running under an oldergomakesgo-licensesfail to resolve stdlib packages and abort before writing anything. SetGOTOOLCHAINto the pinned version if your defaultgois older. - A red
notice-driftis not always yours. BecauseNOTICElives on the branch, a dependency bump merged without regenerating it leavesv5itself stale — and then every open PR inherits the failure, including docs-only ones. Check whetherv5is clean before assuming your change caused it.
A FORBIDDEN result is a different problem: the module graph contains a
licence the project cannot ship (this is how an AGPL-3.0 dependency was caught
in #5016). That needs the dependency removed or replaced, not a regeneration.
There is no public just lint recipe in the current root Justfile; use go vet ./... for the repository's documented local lint-equivalent check, plus gofmt, go build, and targeted go test for the files you change.
The quickest operator path remains Docker Compose; see the full quick start in
src/docs/quickstart-docker-compose.md:
cp src/hive.yaml.example src/hive.yaml
# src/.env, NOT ./.env — `-f src/docker-compose.yaml` makes `src/` the project
# directory, so that is the `.env` Compose reads. A root `.env` is ignored.
install -m 600 /dev/null src/.env # holds live tokens — create it 0600, never world-readable
echo "HIVE_GITHUB_TOKEN=ghp_..." >> src/.env
# REQUIRED: the dashboard's auth proxy refuses to start without it.
printf 'HIVE_DASHBOARD_TOKEN=%s\n' "$(openssl rand -hex 32)" >> src/.env
docker compose -f src/docker-compose.yaml up -dFor source-level debugging, build with go build ./cmd/hive from src/ and run the generated binary with a local src/hive.yaml. Keep real tokens in your shell or local ignored .env files, never in commits.
The root Justfile is the discoverable entry point for contributor relay automation. List the public recipes with:
just --listCurrent public recipes are centered on the contribute workflow:
just contribute-check <backend>— read-only preflight for an agent backend CLI.just contribute-setup <backend>— one-time setup for GitHub auth, hub registration, and backend readiness.just contribute-hive [backend] [mode]— start contributing work to a hive, using a container by default or local mode when requested.just contribute-status,just contribute-browse, andjust contribute-stop— inspect, discover, or stop contributor relay activity.just contribute-move [backend]— move a legacy positional contributor relay to another machine by reissuing its credential instead of hand-copyingcontributor.env; see contributor-relay.md.just contribute-k8s [namespace] [outfile] [image_tag]— print Kubernetes manifests for a headless contributor workload; it prints or writes the manifest you request and does not apply it.- Optional
HIVE_K8S_RUNTIME_CLASS=<name>(e.g.gvisor,kata) setsruntimeClassNameon the contributor pod; unset emits no field. The RuntimeClass must already exist in the cluster.
- Optional
just hive-api <endpoint>andjust hive-api-docs— inspect hub API endpoints for the configured hive.just backend-smoke [backends]— opt-in live smoke of the contributor CLI integration (bin/test_backend_smoke.sh) with this machine's own credentials; keyless machines still run the drift and stub wire-contract checks. See backend-smoke.md.
Deployment and development tasks that are not listed by just --list are not public recipes today. Use the Go, Docker Compose, and Kubernetes commands documented in the README and src/docs/ for those workflows.
- Rebase on the latest
origin/v5. - Run the build and tests that match your change.
- Commit with DCO sign-off:
git commit -s. - Open a PR against
v5with an emoji-prefixed title, testing notes, andFixes #...lines for closing issues.