Contributions are welcome — code, docs, bug reports, and ideas.
For security-sensitive reports, please use the private reporting path in SECURITY.md instead of opening a public issue.
Use the DevContainer. It includes Go, kubectl, k3d, Helm, and all required tools. Everything below assumes you are running inside it.
See .devcontainer/README.md for setup instructions.
An optional repo-root .env file can provide read-only gh CLI access inside the devcontainer for
checking PRs, Actions runs, and CI logs. Keep it local-only; the existing .gitignore already
covers .env.
You can access common services as part of the e2e cluster
- gitea: http://localhost:13000/ (giteaadmin/giteapassword123)
- redis://default:e2e-valkey-password@127.0.0.1:16379/0
For small fixes, docs improvements, and obvious bug fixes, feel free to open a PR directly.
For larger changes, new features, architectural shifts, or anything likely to take significant effort, please open an issue or discussion first so direction can be aligned before you spend a lot of time on implementation.
As a rough rule: if the change is likely to take more than about 30-60 minutes of focused work, or more than a few thousand AI tokens / a substantial coding session to prepare, check first.
task lint # must pass
task test # must pass
task test-e2e # must pass (requires Docker running)task testThis regenerates manifests and codegen, runs go fmt and go vet, downloads envtest
assets, and runs all packages except test/e2e. Coverage is written to cover.out:
go tool cover -html=cover.outtask test also enforces a coverage ratchet (task cover-check): it fails if total coverage drops
more than ~0.5% below .coverage-baseline, a committed high-water mark. When your change improves
coverage the baseline auto-raises — commit the updated .coverage-baseline alongside your
change so the floor advances. On PRs, Codecov
reports the merged unit + e2e coverage.
Run a single package or test:
go test ./internal/controller -v
go test ./internal/git -run TestName -vThe project fuzzes the code that parses untrusted or semi-structured input — manifest
editing (internal/git/manifestedit) and audit/admission request decoding
(internal/webhook) — so hostile input can never crash the controller. task test
already replays every fuzz seed and committed crash reproducer as ordinary regression
cases, so you get that coverage for free on every run.
Before a major production release, run a longer discovery pass:
task fuzz # release-time discovery run, per target, under the race detector
task fuzz-smoke # short active-fuzz smoke of each targetIf fuzzing finds a crash, Go writes the reproducer under
<package>/testdata/fuzz/<Target>/. Commit it: it fixes the input as a permanent
regression case, replayed by task test thereafter. See
docs/design/dynamic-analysis-fuzzing-plan.md.
task test-e2eE2E runs against a real k3d cluster with Gitea and Prometheus. The cluster is intentionally not torn down on failure — this makes debugging easier and reruns faster. Tests are written to append to existing state rather than require a clean slate.
For deeper debugging, see test/e2e/E2E_DEBUGGING.md.
To measure how much of the controller the e2e suite exercises, build an instrumented image and flush coverage from the running pod after the suite:
E2E_COVERAGE=1 task test-e2e
task e2e-coverage-collect # writes e2e-cover.outCI does this automatically on the full matrix and uploads it to Codecov under the e2e flag.
For controller iteration against the shared e2e cluster:
tilt upThe Tilt UI keeps build/deploy work on the existing task prepare-e2e path, then adds a small
playground group of manual resources:
playground-bootstrapcreates a reusable example repo, thetilt-playgroundnamespace, the Git credentials, and thesops-age-keySecret- the starter
GitProvider,GitTarget, andWatchRulelive intest/playground/and are auto-applied by Tilt via kustomize once the bootstrap has run playground-cleanupremoves the fixed playground namespace, repo, and local artifactsplayground-upsert-*/playground-delete-*mutate watched resources so you can quickly confirm create, update, delete, and Secret encryption flowsplayground-statusprints the current starter resources and the recent repo commit log
The design note for the playground flow lives in
docs/design/tilt-playground-plan.md.
Troubleshooting:
- envtest errors: run
task setup-envtestthen retry - Docker errors: ensure the Docker daemon is running
- Permission issues: see
docs/ci/go-module-permissions.md - Windows: see
docs/ci/windows-devcontainer.md
This project uses Conventional Commits. The type determines whether a release is triggered and what version bump applies:
| Type | Effect |
|---|---|
feat |
minor bump |
fix, perf, revert |
patch bump |
docs, test, chore, ci, refactor, style, build |
no bump |
feat! or BREAKING CHANGE: footer |
major bump |
Examples: feat(controller): add SSH key rotation, fix(webhook): prevent queue race
See .github/RELEASES.md for how releases and changelogs are automated.
Repo-specific guidance for AI coding assistants lives in AGENTS.md.
- Branch from
main - Make changes, follow the validation steps above
- Open a PR against
main— CI runs automatically
External contributions come from a fork, and GitHub gives fork PRs a read-only
GITHUB_TOKEN — it cannot push to the org's packages and has no access to repository
secrets. CI adapts automatically so that fork PRs still run the full pipeline
(lint, unit, e2e, image scan) without needing any write access:
- Images are built but never pushed. The CI base container and the project image
are built from your PR's own code in the PR run — a PR that changes
.devcontainer/Dockerfileis validated against its own toolchain. - Images move between jobs as artifacts, not via the registry. They are
docker saved, uploaded, and later jobsdocker loadthem; the e2e job imports the project image straight into k3d (IMAGE_DELIVERY_MODE=load) instead of pulling it. - Same checks as maintainers. PRs and pushes to
mainrun the same jobs from the same workflow file (ci.yml); only the image delivery differs.
Nothing is published from a PR regardless of origin: image and chart publishing only
happen on push to main via release.yml, after the
full pipeline passed. If this is your first contribution, a maintainer needs to
approve the workflow run before it starts — that's a GitHub safety default, not a
distrust of your patch.
The full design (trust zones, signing, verification) is described in docs/ci-overview.md.
Thank you for contributing!