Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2a993b3
docs(rbac): note that Secret refs are namespace-local by design
sunib Jul 10, 2026
9fbb869
test(fixtures): add a corpus of real-world GitOps layouts
sunib Jul 10, 2026
f1ab8db
test(fixtures): make the Helm charts real, fix four layout bugs, add …
sunib Jul 10, 2026
848d25c
docs(gitops-api): the orchestrator knowledge boundary, and ownership …
sunib Jul 10, 2026
f682b47
docs(pkg): stop promising compatibility we have not earned
sunib Jul 10, 2026
871ac1f
docs(gitops-api): write-only encrypted secrets — describe the ciphert…
sunib Jul 10, 2026
10b1a13
docs: fix four mermaid sequence diagrams broken by a semicolon
sunib Jul 10, 2026
c273abd
docs(gitops-api): capability model, SealedSecrets/ExternalSecrets, in…
sunib Jul 10, 2026
da9dc24
docs(gitops-api): correct the hydration doc — a triggered reconciler …
sunib Jul 10, 2026
4c3d433
docs(gitops-api): yes, Flux can skip SOPS documents — tested against …
sunib Jul 10, 2026
a988bb9
docs(gitops-api): the expansion boundary, and a triage plan for the d…
sunib Jul 11, 2026
2cd3d01
docs: split the tree by lifecycle, and fix every dangling citation
sunib Jul 11, 2026
e45145a
docs: add the index, the support contract, and lifecycle banners
sunib Jul 11, 2026
03c28fc
docs: make the spec/ folder definition accurate
sunib Jul 11, 2026
60f664c
chore: cleaning links, and automating checks for dead doc links
sunib Jul 13, 2026
fe22fb6
feat(gitops-api): layout fixture corpus, provenance markers, and a do…
sunib Jul 13, 2026
63bbcf4
fix(doccheck): check YAML and shell citations, and stop fingerprintin…
sunib Jul 13, 2026
7a6ebb8
docs: keep the support boundary public, take the product roadmap private
sunib Jul 14, 2026
2b227c6
test(e2e): drop the ladder shorthand from the e2e labels and fixtures
sunib Jul 14, 2026
1574ed4
fix(doccheck): resolve references against git, not against this disk
sunib Jul 14, 2026
0e51aee
fix(ci): the lint cache assertion contradicted the un-fingerprinted d…
sunib Jul 14, 2026
bbc5380
docs: distinguish planned support boundary
sunib Jul 14, 2026
f550b5d
fix(ci): quote lint cache assertion safely
sunib Jul 14, 2026
34fffc9
fix(ci): avoid quotes inside lint shell block
sunib Jul 14, 2026
cf80137
fix(ci): the stale-baseline annotation ran the task instead of naming it
sunib Jul 14, 2026
1cbc0ae
fix(doccheck): check the citations that do not live under docs/
sunib Jul 14, 2026
89662b0
docs(upgrading): record the overlay fan-out refusal-code rename
sunib Jul 14, 2026
5eca263
fix(baseline): escape pipes in the generated Markdown table cells
sunib Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 7 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
},
"runArgs": [
"--group-add=docker",
"--add-host=host.docker.internal:host-gateway"
"--add-host=host.docker.internal:host-gateway",
// Cap the devcontainer so a runaway build/lint/test is OOM-killed inside the
// container instead of starving the remote host. Note this bounds only what runs
// *in here*: docker-outside-of-docker means e2e's k3d containers are siblings on
// the host daemon and are not covered by this limit.
"--memory=12g",
"--memory-swap=16g"
],
"forwardPorts": [
13000,
Expand Down
55 changes: 50 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,57 @@ jobs:
fi
done

if grep -Eq "^task: \[[^]]+\]" "${cache_log}"; then
echo "Expected second task lint dry-run to be fully cached, but a command would run" >&2
# lint-docs is exempt on purpose. It carries no `sources:` (see
# Taskfile-build.yml), because its real input is "every tracked .md, .go,
# .yml and .sh" — which Task cannot express: an unrooted `**` glob walks
# external-sources/ and has OOMed the host, and a hand-maintained file list
# silently drifts out of sync with what doccheck scans. A stale "up to date"
# there means the check never runs, so it always runs. It costs ~0.2s.
#
# awk, not `grep -qv`: the CI image ships ugrep as grep, whose -q -v returns
# 1 even when -c reports a selected line. That would make this assertion
# silently pass whatever it was handed.
uncached="$(awk "/^task: \\[/ && !/^task: \\[lint-docs\\]/" "${cache_log}")"
if [ -n "${uncached}" ]; then
echo "Expected second task lint dry-run to be fully cached apart from lint-docs, but these would run:" >&2
printf "%s\n" "${uncached}" >&2
exit 1
fi
'

- name: GitOps layout baseline is current
# test/fixtures/gitops-layouts/support-today.md is generated from a live
# manifest-analyzer scan of the corpus. Regenerating it here and failing on a
# diff is what makes a change to the acceptance boundary show up in review as
# "exactly which fixtures moved, and in which direction". Without this check the
# baseline silently goes stale, and the corpus stops being evidence of anything.
#
# A failure is not necessarily a bug: if the boundary moved on purpose, run
# `task gitops-layouts-baseline` and commit the regenerated file. Review the diff.
#
# Kept out of `task lint` deliberately: the lint step above asserts that a second
# dry-run is fully cached, and this task's fingerprint would have to be threaded
# through that assertion for no benefit.
run: |
docker run --rm \
-v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \
-w "${{ env.CI_WORKDIR }}" \
"${CI_CONTAINER}" \
bash -lc '
set -e
git config --global --add safe.directory "$PWD"
task gitops-layouts-baseline --force
if ! git diff --quiet -- test/fixtures/gitops-layouts/support-today.md; then
# No backticks in the message below. It is a double-quoted string inside a
# bash -lc block, where backticks are command substitution: the echo would
# RUN the task a second time and splice its stdout into the annotation.
echo "::error file=test/fixtures/gitops-layouts/support-today.md::The GitOps layout baseline is stale: the analyzer now reports something different for the corpus. If the acceptance boundary moved on purpose, run task gitops-layouts-baseline and commit the result."
git --no-pager diff -- test/fixtures/gitops-layouts/support-today.md
exit 1
fi
echo "baseline is current"
'

test:
name: Unit tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -717,7 +762,7 @@ jobs:
# equals what `full` ran — so no spec is added or dropped, the work is
# just split across two clusters. The membership is a provisional
# first cut; rebalance from the e2e-ginkgo-reports-* per-spec timings
# (docs/design/e2e-ci-runner-sharding-plan.md, "balance from real data").
# (docs/finished/e2e-ci-runner-sharding-plan.md, "balance from real data").
# Each shard runs config-dir installs (GOCOVERDIR overlay) and collects
# coverage; Codecov unions same-flag `e2e` uploads, so merged coverage
# is the union of both shards — the non-regression ratchet still holds.
Expand All @@ -729,7 +774,7 @@ jobs:
coverage: "1"
# Bumped 1 -> 4: the full suite was serialized to avoid the
# cross-target rule-change snapshot coupling, now fixed (per-target
# effective watch plan — docs/design/gittarget-isolation-on-rule-change.md).
# effective watch plan — docs/spec/gittarget-isolation-on-rule-change.md).
e2e_ginkgo_procs: "4"
k3d_agent_count: "0"
- name: full-core
Expand All @@ -747,7 +792,7 @@ jobs:
# only place gitops-reverser shares a path with a foreign GitOps engine.
# Its own runner (hence its own cluster) is what keeps the Argo CD
# install off the other three legs.
# docs/design/e2e-bi-directional-corner.md.
# docs/spec/e2e-bi-directional-corner.md.
#
# `task test-e2e-bi-directional` pins its own label filter and Ginkgo
# procs (BI_DIRECTIONAL_GINKGO_PROCS), so e2e_label_filter and
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
*.dylib
bin/*
Dockerfile.cross
# `go build ./hack/doccheck` drops the binary here by default, and a 4 MB ELF was
# committed once already. Root-anchored on purpose: a bare `doccheck` would also
# match the hack/doccheck/ SOURCE directory and untrack it.
/doccheck
*.env
# The GitOps layout fixtures check in a kustomize secretGenerator env file on purpose.
# Its contents are placeholder text, never a real credential.
!test/fixtures/gitops-layouts/**/*.env

# Test binary, built with `go test -c`
*.test
Expand Down
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ linters:
# traffic generation, and operator-supplied target URLs.
- path: 'test/loadtest/.*\.go'
linters: [depguard, forbidigo, gochecknoglobals, gosec, mnd]
# The doc-link checker is a standalone dev CLI: printing its report to the console
# is its entire purpose (forbidigo), and it reads every git-tracked file by a path
# it computed rather than a constant (gosec G304). It only ever reads them — it
# opens nothing it was handed by a reference, and writes nothing at all.
- path: 'hack/doccheck/.*\.go'
linters: [forbidigo, gosec]
# Allow fmt.Println in metrics exporter (initialization logs)
- path: 'internal/telemetry/.*\.go'
linters: [forbidigo]
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ task fuzz-smoke # short active-fuzz smoke of each target
If 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`](docs/design/dynamic-analysis-fuzzing-plan.md).
[`docs/finished/dynamic-analysis-fuzzing-plan.md`](docs/finished/dynamic-analysis-fuzzing-plan.md).

## E2E tests

Expand Down Expand Up @@ -133,7 +133,7 @@ The Tilt UI keeps build/deploy work on the existing `task prepare-e2e` path, the
- `playground-status` prints 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`](docs/design/tilt-playground-plan.md).
[`docs/finished/tilt-playground-plan.md`](docs/finished/tilt-playground-plan.md).

**Troubleshooting:**
- envtest errors: run `task setup-envtest` then retry
Expand Down
50 changes: 47 additions & 3 deletions Taskfile-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ vars:
ENVTEST_STAMP: '.stamps/envtest-{{.ENVTEST_K8S_VERSION}}.ready'
# Fuzzing (dynamic analysis) targets, one "package:FuzzTarget" pair per line.
# `go test -fuzz` fuzzes exactly one target per invocation, so the fuzz tasks
# loop over these. See docs/design/dynamic-analysis-fuzzing-plan.md.
# loop over these. See docs/finished/dynamic-analysis-fuzzing-plan.md.
FUZZ_TARGETS: >-
./internal/git/manifestedit:FuzzManifestEdit
./internal/webhook:FuzzDecodeEventList
Expand Down Expand Up @@ -184,7 +184,7 @@ tasks:
desc: >-
Release-time dynamic analysis — longer fuzz discovery run per target under the race
detector. Crash reproducers land under <pkg>/testdata/fuzz/<Target>/ (commit them).
See docs/design/dynamic-analysis-fuzzing-plan.md.
See docs/finished/dynamic-analysis-fuzzing-plan.md.
cmds:
- |
set -eu
Expand All @@ -195,12 +195,39 @@ tasks:
done

lint:
desc: Run all linters (Go, Dockerfiles, workflows, Helm chart); deps run in parallel where safe
desc: Run all linters (Go, Dockerfiles, workflows, Helm chart, doc links); deps run in parallel where safe
deps:
- lint-golang
- lint-dockerfiles
- lint-actions
- lint-helm
- lint-docs

lint-docs:
desc: Check that every doc reference resolves — markdown links, and docs/**.md cited in Go comments
# Re-run whenever a doc moves or a Go comment changes. Task fingerprints these
# under .task/ and skips the run otherwise.
#
# This exists because nothing off the shelf covers the second surface: markdownlint
# does not check links, and link checkers (lychee, markdown-link-check) extract URLs
# from markdown -- they never look inside a Go comment for a repo-relative doc path.
# Those are the references that rot silently, and seventeen of them were dangling
# before this check was written.
# Deliberately un-fingerprinted: this task has no `sources:`, so it always runs.
#
# Its real input is "every git-tracked .md, .go, .yml, .yaml and .sh", which Task
# cannot express. Approximating it with globs went wrong twice. An unrooted
# '**/*.go' makes Task walk every path under the repo root -- including the 1.2 GB
# of upstream checkouts in external-sources/ -- and fingerprinting that costs tens
# of GB inside the Task process, enough to OOM the host. (`exclude:` does not help:
# Task expands the glob first and filters afterwards, so the walk still happens.)
# A hand-maintained per-tree list avoids that, but then silently drifts out of sync
# with the file types doccheck scans, and a stale "up to date" means the check
# never runs -- which is how a non-compiling file reached a commit here once.
#
# The whole check takes ~0.2s. That is not worth a cache that can be wrong.
cmds:
- go run ./hack/doccheck

lint-golang:
desc: Run golangci-lint
Expand All @@ -214,7 +241,9 @@ tasks:
sources:
- api/**/*.go
- cmd/**/*.go
- hack/**/*.go
- internal/**/*.go
- pkg/**/*.go
- test/**/*.go
- .golangci.yml
- go.mod
Expand Down Expand Up @@ -276,6 +305,21 @@ tasks:
cmds:
- '{{.SCC}} --exclude-dir external-sources,vendor,bin,dist {{.CLI_ARGS}}'

gitops-layouts-baseline:
desc: Regenerate the GitOps layout corpus behavioural baseline (test/fixtures/gitops-layouts/support-today.md)
# The baseline is generated, never hand-edited, so that a change to the
# acceptance boundary shows up in review as a diff of exactly which fixtures
# moved and in which direction. See test/fixtures/gitops-layouts/README.md.
sources:
- test/fixtures/gitops-layouts/**/*
- internal/manifestanalyzer/**/*.go
- cmd/manifest-analyzer/**/*.go
- hack/gitops-layouts-baseline.sh
generates:
- test/fixtures/gitops-layouts/support-today.md
cmds:
- bash hack/gitops-layouts-baseline.sh

build:
desc: Build manager binary
deps:
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha3/gittarget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type GitTargetSpec struct {
// GitTargetPlacementSpec declares where NEW resources are written when no document
// for their identity exists yet in Git — one exact-type map plus a fallback
// default template (Option B2 of
// docs/design/manifest/version2/gittarget-new-file-placement-rules.md). There is
// docs/spec/gittarget-new-file-placement-rules.md). There is
// deliberately no separate "sensitive" placement block: sensitivity is a
// write-safety classification the controller owns (encrypt the content, keep the
// path identity-complete, never append or co-mingle), not a second placement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# wait. Narrow by construction: one rule per command kind. failurePolicy is Ignore and
# the handler always allows, so a user's CommitRequest never depends on this webhook —
# a miss degrades to a committer-authored commit (AuthorAttributed=False).
# See docs/design/commitrequest-admission-authorship.md §4.
# See docs/spec/commitrequest-admission-authorship.md §4.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
Expand Down
2 changes: 1 addition & 1 deletion charts/gitops-reverser/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ servers:
# attribution governs *mirrored-resource* authorship (from audit), this governs
# *command* authorship (from admission). Off → CommitRequests commit as the configured
# committer and report AuthorAttributed=False. See
# docs/design/commitrequest-admission-authorship.md.
# docs/spec/commitrequest-admission-authorship.md.
#
# Command-author capture is Redis-backed: the captured author is written to Redis at admission and
# read back at reconcile. Enabled by default so the webhook is always installed, but it is a no-op
Expand Down
4 changes: 2 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ func main() {
// lives in its own Redis corner (author:v1:command), independent of
// --author-attribution (which governs mirrored-resource attribution). It is wired
// whenever the admission server is on; the controller reads the captured submitter
// back with no wait (docs/design/commitrequest-admission-authorship.md §2, §6).
// back with no wait (docs/spec/commitrequest-admission-authorship.md §2, §6).
//
// AuthorLookup must be a nil interface — not a non-nil interface wrapping a nil
// *CommandAuthorStore — when the webhook is off, so the controller's nil check
Expand Down Expand Up @@ -898,7 +898,7 @@ func newManager(
// value in memory. Typed Secret Get/List go straight to the API server instead,
// which also keeps credential/age-key rotation reads fresh. Mirrored Secrets
// selected by a WatchRule use the separate dynamic-watch path, not this cache.
// See docs/future/secret-value-retention-plan.md.
// See docs/rbac.md.
Client: client.Options{
Cache: &client.CacheOptions{
DisableFor: []client.Object{&corev1.Secret{}},
Expand Down
4 changes: 2 additions & 2 deletions cmd/manifest-analyzer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// manifest-analyzer is a standalone, read-only CLI that analyzes a folder of
// Kubernetes manifests. It is the proof-of-concept consumer of the
// internal/manifestanalyzer library described in
// docs/design/manifest/current-manifest-support-review.md. It writes nothing; it
// docs/spec/current-manifest-support-review.md. It writes nothing; it
// only reports what it finds.
//
// Usage:
Expand Down Expand Up @@ -277,7 +277,7 @@ func scanExitCode(policy string, accepted bool) int {
return exitOK
}

// runScanRepo runs the F8 whole-repo onboarding scan: walk every folder, enumerate
// runScanRepo runs the whole-repo onboarding scan: walk every folder, enumerate
// candidate GitTarget subtrees, classify each one's layout and acceptance, and emit the
// report. It is read-only and needs no cluster. Exit codes stay simple for this cut
// (exitOK, or exitUsage on an I/O error); the repo-level --policy refuse gate is
Expand Down
2 changes: 1 addition & 1 deletion cmd/manifest-analyzer/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestRun_ScanRepoText(t *testing.T) {
if code := run([]string{"--mode", "scan-repo", scanRepoFixture(t)}, &out, &errBuf); code != 0 {
t.Fatalf("exit = %d, want 0 (stderr=%s)", code, errBuf.String())
}
for _, want := range []string{"candidates:", "kustomize-overlay", "overlay-fan-out-needs-f2"} {
for _, want := range []string{"candidates:", "kustomize-overlay", "overlay-fan-out-unsupported"} {
if !strings.Contains(out.String(), want) {
t.Errorf("scan-repo text missing %q:\n%s", want, out.String())
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/mutation-capture-lab/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// of recorders (native watch, audit webhook, validating admission webhook) whose
// output is a versioned corpus of the exact structures Kubernetes emits. It is
// deliberately NOT a second GitOps Reverser; see
// docs/design/mutation-capture-lab-design.md.
// docs/spec/mutation-capture-lab-design.md.
//
// It deliberately serves the SAME webhook URLs as the product
// (/validate-all, /audit-webhook) so a lab deployment can swap the product image
Expand Down
6 changes: 3 additions & 3 deletions cmd/readiness.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Liveness and readiness wiring for the controller. Liveness (/healthz) stays a bare process
// ping; readiness (/readyz) reflects the audit-serving preconditions so the kube-apiserver — which
// dials this pod through a Service — only routes audit events here once the pod can actually
// receive and enqueue them. See docs/design/audit-readiness-probe-plan.md.
// receive and enqueue them. See docs/spec/audit-readiness-probe-plan.md.

package main

Expand Down Expand Up @@ -46,7 +46,7 @@ type redisPinger interface {
// endpoints — but once it has connected, transient Redis blips are handled at the request layer
// (the audit handler returns HTTP 500 and the apiserver retries the batch), NOT by flapping
// readiness, which would yank every replica out of endpoints at once. See
// docs/design/audit-readiness-probe-plan.md §4.
// docs/spec/audit-readiness-probe-plan.md §4.
type redisReadinessGate struct {
pinger redisPinger
timeout time.Duration
Expand Down Expand Up @@ -152,7 +152,7 @@ func combineReadyChecks(checks ...healthz.Checker) healthz.Checker {
// reflects the locally-checkable audit-serving preconditions (listener up, TLS cert loaded, first
// Redis connection made) so the kube-apiserver, which dials this pod through a Service, only routes
// audit events here once the pod can actually receive and enqueue them. See
// docs/design/audit-readiness-probe-plan.md.
// docs/spec/audit-readiness-probe-plan.md.
func addHealthChecks(
mgr ctrl.Manager,
auditProbe auditReadinessProbe,
Expand Down
2 changes: 1 addition & 1 deletion config/webhook/validating-webhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ metadata:
# kinds (a CommitRequest today) at admission. This one IS product behavior and is
# packaged in the chart (charts/.../templates/validate-operator-types-webhook.yaml); the
# entry here is the e2e SUT's copy. See
# docs/design/commitrequest-admission-authorship.md §4.
# docs/spec/commitrequest-admission-authorship.md §4.
#
# The cert-manager.io/inject-ca-from annotation injects the admission server CA bundle
# for every webhook in this configuration.
Expand Down
Loading