Skip to content

Latest commit

 

History

History
332 lines (266 loc) · 16.6 KB

development.md

File metadata and controls

332 lines (266 loc) · 16.6 KB

Local development

This guide describes the local workflow for contributing to the Hive Go codebase on the v5 branch.

Prerequisites

  • Git and GitHub CLI (gh) for normal issue and PR workflows.
  • Go 1.26.6, as declared by src/go.mod — when the two disagree, go.mod wins.
  • Docker or Podman if you are exercising containerized contributor relay or deployment paths.
  • tmux for local agent/contributor workflows that attach CLIs to terminal sessions.
  • just if you use the repository's helper recipes (brew install just on macOS, or install from the just project for your platform).
  • Optional agent CLIs depending on what you test locally: Claude Code, GitHub Copilot/gh, Bob, Goose, Codex, Pi, or Antigravity.

Clone and branch

git clone https://github.com/hivecommons/hive.git
cd hive
git switch -c <topic-branch> origin/v5

Use 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.

Build

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/hive

Test

Run 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/hive

The plain command above is weaker than the gate

go 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=1

What each flag changes:

  • -race is 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 the writeMu / WriteControl reasoning in src/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.
  • -short sets testing.Short(), so any test guarded by if testing.Short() { t.Skip(...) } does not run in CI. This cuts both ways, and both directions bite. A slow test you write without a Short guard runs on every shard and adds to the gate's wall clock. A test you write behind a Short guard is never executed by the gate at all — a green required check says nothing about it, exactly as a plain run says nothing about the integration suite above. Guard slow setup, not the assertion that proves your fix.
  • -count=1 disables 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 600s is 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.
  • -coverprofile is 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.

CI wall clock and required checks

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.

Affected-package selection on pull requests

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 .go file;
  • their reverse dependencies, including test-only imports (from go list -test), for changes to non-test files — a _test.go change 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/config and pkg/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.

Format and lint expectations

Run gofmt on Go files you edit:

gofmt -w path/to/file.go

The 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/...

If CI says "NOTICE is out of date"

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 root

Three 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.mod pins a specific version; running under an older go makes go-licenses fail to resolve stdlib packages and abort before writing anything. Set GOTOOLCHAIN to the pinned version if your default go is older.
  • A red notice-drift is not always yours. Because NOTICE lives on the branch, a dependency bump merged without regenerating it leaves v5 itself stale — and then every open PR inherits the failure, including docs-only ones. Check whether v5 is 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.

Running Hive locally

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 -d

For 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.

Just recipes

The root Justfile is the discoverable entry point for contributor relay automation. List the public recipes with:

just --list

Current 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, and just 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-copying contributor.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) sets runtimeClassName on the contributor pod; unset emits no field. The RuntimeClass must already exist in the cluster.
  • just hive-api <endpoint> and just 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.

Before opening a PR

  1. Rebase on the latest origin/v5.
  2. Run the build and tests that match your change.
  3. Commit with DCO sign-off: git commit -s.
  4. Open a PR against v5 with an emoji-prefixed title, testing notes, and Fixes #... lines for closing issues.