⚠️ Work in Progress
You are viewing development documentation built from the latest commit on main. APIs and features are subject to change.

Development Guide

Development Guide

This guide covers development workflows and tooling for the Kure project.

Quick Start

# Get help with all available commands
make help

# Run all standard development tasks
make all

# Quick development cycle
make check

Contributing Workflow

The main branch is protected — all changes must go through pull requests.

Branch Workflow

  1. Create a feature branch from main:

    git checkout -b feat/my-feature main

    Use branch prefixes: feat/, fix/, docs/, chore/

  2. Develop and test locally:

    make check       # Quick validation
    make precommit   # Full pre-commit checks
  3. Push and create a pull request:

    git push -u origin feat/my-feature
    gh pr create

    Fill out the PR template (.github/PULL_REQUEST_TEMPLATE.md).

  4. Pass required CI checks: lint, test, build, pr-review / AI Code Review

  5. Resolve all conversations (no approving review is required)

  6. Merge (linear history required — rebase, no merge commits)

Branch Protection Rules

Enforced via the main-protection repository ruleset :

  • Required status checks: lint, test, build, pr-review / AI Code Review
  • Merge queue: merging goes through a GitHub merge queue (rebase method) that rebases and tests the merged result before landing — no manual rebasing, no auto-rebase force-pushes
  • Pull requests required: all changes must go through a PR
  • Conversation resolution: all review threads must be resolved
  • Linear history: enforced (rebase only, no merge commits)
  • Force pushes: disabled
  • Branch deletion: disabled
  • Bypass actors: kure-release-bot (GitHub App) — allowed to push release commits directly

Release branches (release/vX.Y) have their own release-protection ruleset with the same rules, required checks and bot bypass, but no merge queue: a pull request must be up to date with the branch before it merges. See Releasing .

Development Workflow

1. Initial Setup

Prerequisites:

  • go and mise — required
  • make, git — required
  • bash 4+ — required
  • yq — required by scripts/sync-versions.sh; pinned in mise.toml
  • mktemp — required by scripts/sync-versions.sh and scripts/vendor-guard.sh, and also by the scripts/test/ harness itself (lib.sh’s new_fixture/_run). An external coreutils utility, not a bash builtin, but present on virtually every host with coreutils installed; the two scripts name the failure clearly on the rare host without it.
  • timeout (or macOS Homebrew’s gtimeout) from GNU coreutils — split by scope, not one blanket status:
    • optional for scripts/sync-versions.sh check (production runtime): the two bounded probes it uses degrade gracefully to running unbounded, with a startup warning, when neither binary is found.
    • required to run the full local gate (mise run verify / scripts/test/run-tests.sh): harness cases 09-mvs-floor-hang-timeout.sh (pre-existing) and 36-timeout-present-no-warning.sh (new) both need a real timeout/gtimeout to pass — see docs/dependency-updates.md’s harness section for the caveat.
# Install dependencies
make deps

# Install development tools
make tools

2. Development Cycle

# Format code
make fmt

# Run quick checks (lint, vet, short tests)
make check

# Run all tests
make test

# Run tests with coverage
make test-coverage

3. Testing

# Run all tests
make test

# Run tests with verbose output
make test-verbose

# Run tests with race detection
make test-race

# Run only short tests (good for quick feedback)
make test-short

# Run tests with coverage report
make test-coverage

# Run benchmark tests
make test-benchmark

# Run integration tests (when available)
make test-integration

Golden fixtures and schema validation

The builder families under pkg/kubernetes keep their expected output as golden fixtures in testdata/. internal/kuretest.Golden validates an object against the pinned CustomResourceDefinition schema (and the API server’s ObjectMeta rules) before comparing it with its fixture, and rewrites the fixture under -update only when it validates:

GOWORK=off go test ./pkg/kubernetes/cnpg/ -run TestGolden -update

A fixture the pinned schema rejects is a defect in the test or the builder, not a fixture to update. The validator reads each pinned module’s definitions from the module cache through go list, so it needs the module graph as go.mod states it: under a parent go.work that replaces a pinned module, the version check fails and its message names GOWORK=off (the Makefile exports it already), and a replace directive that changes a pinned module’s path or version is refused the same way. The same validation runs on the Flux bootstrap output of pkg/stack/fluxcd and on everything examples/getting-started writes, so a generator that drifts from the pinned schema fails make test, not a cluster. Which kinds are held to a schema and which get ObjectMeta validation only is listed in pkg/kubernetes/README.md § Schema validation test.

A second pass holds the objects to each other rather than to a schema: AssertConsistent, AssertConsistentYAML and AssertConsistentDir fail when a Flux sourceRef, chartRef or dependsOn names nothing in the tree, or an object or a Kustomization’s targetNamespace names a namespace the tree does not create — unless the test declares it with External or Namespace, or it is one the cluster makes itself. On a written directory, AssertConsistentDir reads every .yaml and .yml file but one a kustomization.yaml names as data only, and every file one lists as a resource whatever its name, as kustomize applies it, and a .yaml or .yml one named as data under a Flux Kustomization’s spec.path that has no kustomization.yaml, but not in a subdirectory with its own, since Flux’s generator lists it there and takes such a subdirectory whole; it also fails when a Kustomization’s spec.path or an entry of a kustomization.yaml names nothing in the tree, or an entry of a kustomization.yaml or of a Kustomization’s spec.components leaves it, and builds what each Flux Kustomization applies the way kustomize-controller builds it, on a copy of the tree — nothing, once an entry leaves the tree.

4. Code Quality

# Run all linting
make lint

# Format code
make fmt

# Run go vet
make vet

# Tidy modules
make tidy

# Run Qodana static analysis (requires Docker)
make qodana

Pre-commit Workflow

Before committing changes, run:

make precommit

This will:

  • Format code with go fmt
  • Tidy modules
  • Run linters
  • Run go vet
  • Run all tests

CI/CD Pipeline

The project uses several GitHub Actions workflows:

Main CI Pipeline (.github/workflows/ci.yml)

  • Triggers: Push to main/develop, PRs
  • Jobs:
    • Test (unit, race, coverage)
    • Lint and format check
    • Integration tests (main branch only)
    • Security scanning
    • Dependency vulnerability checks

Qodana Code Quality (.github/workflows/code_quality.yml)

  • Triggers: Push, PRs
  • Purpose: Static analysis with JetBrains Qodana
  • Uses: make deps for setup

Release (.github/workflows/release.yml) and Release / Publish (.github/workflows/release-publish.yml)

Run Release from the Actions tab: pick the branch (main, or release/vX.Y for a patch), what to do (release by default), and tick Dry run for a preview. It makes the release commits and the tag, and Release / Publish then runs by itself on the tag. Nothing is tagged by hand. The Releasing page (docs/releasing.md, shared by every go-kure repository) explains every option, release branches, and what to do when a release fails. kure’s Publish creates the GitHub release object only: kure is a library, so a complete release carries no assets and the tag is the artifact.

PR Checks (.github/workflows/pr-checks.yml)

  • Triggers: PR events
  • Jobs:
    • Quick validation with make check
    • Security and dependency checks
    • Test coverage validation
    • Changed files analysis
    • Performance benchmarks (when labeled)
    • Documentation validation

Renovate Management

Dependency updates come from Renovate (renovate.json, extending the shared go-kure/.github preset). The Dependency Dashboard issue is the control surface:

  • Gated updates (every major, all Go-toolchain updates, Flux minors) sit under Pending Approval — tick the checkbox to let Renovate open the PR. Nothing gated is ever proposed on its own.
  • Deferring an update: leave its dashboard checkbox unticked; there is nothing to close. To reopen a closed/ignored update, tick its checkbox on the dashboard.
  • Rebasing a PR: tick the “rebase/retry” checkbox in the PR body, or the per-PR entry on the dashboard. Renovate also rebases automatically when the PR falls behind the base branch.
  • Closing a PR: closing it normally tells Renovate not to recreate that version; the dashboard lists it under Closed/Ignored.

Renovate regenerates docs/compatibility.md on its own branch after gomod or mise bumps (postUpgradeTasks running ./scripts/sync-versions.sh generate), so its PRs pass the validate drift check without manual help. The same postUpgradeTasks rule also runs sh scripts/sync-tool-versions.sh, which keeps the golangci-lint pin in Makefile, .github/workflows/ci.yml and docs/github-workflows.md in step with mise.toml, and sh scripts/sync-go-version.sh, which keeps go.mod’s own go directive in step with mise.toml’s go pin — a bot PR touching those files, or a check-tool-versions/check-go-version failure on one, is this same automation. The rule’s last command, ./scripts/gen-builders.sh generate, regenerates the per-kind constructor wrappers under pkg/kubernetes (zz_generated_*.go) from the registered scheme, so an API-module bump that adds or drops a kind arrives with its wrapper; validate runs ./scripts/gen-builders.sh check and fails on drift. Locally: make gen-builders / make check-builders (or mise run builders:generate / builders:check).

For the full dependency update process (review, bundling, version tracking), see Dependency Updates .

Makefile Targets Reference

Development

  • help - Display help message
  • all - Run all standard development tasks
  • info - Display project information
  • clean - Clean build artifacts and caches

Dependencies

  • deps - Download and tidy Go modules
  • deps-upgrade - Upgrade all dependencies
  • tools - Install development tools
  • outdated - Check for outdated dependencies

Testing

  • test - Run all tests
  • test-verbose - Run tests with verbose output
  • test-race - Run tests with race detection
  • test-short - Run short tests only
  • test-coverage - Run tests with coverage report
  • test-benchmark - Run benchmark tests
  • test-integration - Run integration tests

Code Quality

  • lint - Run all linters
  • lint-go - Run golangci-lint
  • fmt - Format Go code
  • vet - Run go vet
  • tidy - Tidy modules
  • qodana - Run Qodana static analysis

CI/CD

  • ci - Run CI pipeline tasks
  • ci-coverage - Run CI with coverage
  • ci-integration - Run CI with integration tests
  • check - Quick code quality check
  • precommit - Run all pre-commit checks

Release

No target releases or previews a release: run the Release workflow, with Dry run ticked for a preview (Releasing ). changelog-preview prints the unreleased changelog entries.

Utilities

  • generate - Run go generate
  • mod-graph - Display module dependency graph
  • list-packages - List all packages

Environment Variables

Set from the environment (?=, so VAR=value make <target> and an exported VAR both work):

  • GOROOT - Go root directory (default: go env GOROOT)
  • VERSION - Version string for builds (default: git describe, else dev)
  • TEST_TIMEOUT - Test timeout, applied per test binary (default: 15m, the same budget CI grants)

Assigned with :=, so the environment does not reach them — override with a make command-line assignment instead, as in make test TEST_PACKAGES=./pkg/errors/:

  • GO - Go command (default: go)
  • TEST_PACKAGES - Package pattern the test targets run (default: ./...)
  • BUILD_DIR, OUTPUT_DIR, COVERAGE_DIR - directories clean removes (bin, out, coverage); COVERAGE_DIR is also where test-coverage writes its profile and HTML report

COVERAGE_THRESHOLD (Makefile:30) is defined and never referenced — setting it changes nothing. It also disagrees with the gate that does run: it reads 80, while the coverage-check CI job hardcodes 90 for both total and per-package coverage (.github/workflows/ci.yml:768-769). Treat 90 as the number that matters.

Development Tips

Testing Strategy

  • Use make test-short for quick feedback during development
  • Use make test-coverage to check coverage before PRs
  • Use make test-race to catch concurrency issues
  • Use make check for quick pre-commit validation
  • Refresh a golden fixture with -update (Golden fixtures and schema validation, above); the helper refuses to write one the pinned schema rejects

Code Quality

  • CI gates test coverage — see the COVERAGE_THRESHOLD note above for the number that applies
  • All code must pass golangci-lint checks
  • Code must be properly formatted with go fmt
  • Modules must be tidy

Active Linters

The .golangci.yml enables these linters, aligned with the shared Go standard:

LinterCategoryPurpose
errcheckDefaultUnchecked errors
govetDefaultSuspicious constructs
ineffassignDefaultIneffectual assignments
staticcheckDefaultComprehensive static analysis (includes gosimple S* checks)
unusedDefaultUnused code
bodycloseRequiredHTTP response body closed
durationcheckRequiredtime.Duration mistakes
errorlintRequiredError wrapping issues
exhaustiveRequiredExhaustive enum switches
misspellRequiredCommon misspellings
nilerrRequiredNil error returns
unconvertRequiredUnnecessary conversions
whitespaceRequiredUnnecessary whitespace
gosecOptionalSecurity checks (kure-specific)

Formatters: gofmt, goimports (with github.com/go-kure/kure as local prefix).

Performance

  • Benchmark tests can be run with make test-benchmark
  • PR checks include performance benchmarks when labeled with performance
  • Build targets include optimized release builds with -s -w flags

Troubleshooting

Build Issues

# Clean everything and rebuild
make clean all

# Check Go installation and environment
make info

Test Failures

# Run tests with verbose output for more details
make test-verbose

# Run specific test
go test -v ./pkg/specific/package -run TestSpecific

Dependency Issues

# Update dependencies
make deps-upgrade

# Check for outdated or vulnerable dependencies
make outdated

This development guide provides a comprehensive overview of the development workflow using the Makefile and CI/CD pipeline.

Documentation Updates

When modifying a package’s public API, update documentation in the same PR:

  1. Package README — Update the README.md in the package directory (e.g., pkg/stack/README.md)
  2. Guides — Check the reverse mapping in AGENTS.md for guides that reference the changed package

To verify the docs site builds correctly:

# Check all mounted files exist
bash site/scripts/check-mounts.sh

# Build site
mise run site:build

Consumer Compatibility

Kure’s public packages are consumed by external projects. Keep public APIs stable when possible, describe integration requirements as reusable library capabilities, and follow the organization API stability contract for deprecations or breaking changes.

For local co-development, a consumer can use a Go workspace without adding a committed replace directive:

go work init
go work use ./kure ./consumer

Before publishing a Kure change needed by a consumer, verify Kure with GOWORK=off, then update and verify the consumer against the released Kure version in its own repository.