Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a478b38
ci: validate every pull request, not only those based on main
sunib Jul 20, 2026
a63532a
test(e2e): read the author mode from the Deployment, not from a log grep
sunib Jul 20, 2026
d41e392
chore(dev): make the e2e task surface hard to misuse
sunib Jul 20, 2026
46cbfed
diag(watch): name declared streams, and record the attribution invest…
sunib Jul 20, 2026
c55c33b
docs(debug): consolidate the attribution investigation into docs/debug/
sunib Jul 20, 2026
db3c136
docs(design): propose an explicit author for unattributed commits
sunib Jul 20, 2026
3715d94
docs(design): drop the flag, and say "attribution failed" rather than…
sunib Jul 20, 2026
613d57b
docs(design): carry attribution outcome explicitly, not in the author…
sunib Jul 20, 2026
ee140aa
feat(attribution): carry the outcome explicitly, and name unresolved …
sunib Jul 20, 2026
2f29c39
fix(giteaclient): follow pagination when listing a user's keys
sunib Jul 20, 2026
be2f9d3
refactor(watch): drop the dead snapshot-resolution path
sunib Jul 20, 2026
608ed1b
fix(attribution): let a CommitRequest attach when the two sides disag…
sunib Jul 20, 2026
4a3780e
fix(giteaclient): terminate pagination on an empty page, not a short …
sunib Jul 20, 2026
9a9d76a
fix(e2e): initialize the Prometheus client before the attribution report
sunib Jul 20, 2026
87a8a8a
chore(renovate): track the second devcontainer ENV block
sunib Jul 20, 2026
0740971
docs: clarify attribution outcomes and prune stale notes
sunib Jul 20, 2026
1334d92
fix(attribution): address review feedback
sunib Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coverage-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
76.6
76.7
32 changes: 32 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,38 @@
"matchStrings": ["COSIGN_VERSION=(?<currentValue>v[0-9.]+)"],
"datasourceTemplate": "github-releases",
"depNameTemplate": "sigstore/cosign"
},
{
"customType": "regex",
"description": "devcontainer: tilt (second ENV block; release asset download)",
"managerFilePatterns": ["/^\\.devcontainer/Dockerfile$/"],
"matchStrings": ["TILT_VERSION=(?<currentValue>v[0-9.]+)"],
"datasourceTemplate": "github-releases",
"depNameTemplate": "tilt-dev/tilt"
},
{
"customType": "regex",
"description": "devcontainer: delve (go install github.com/go-delve/delve/cmd/dlv)",
"managerFilePatterns": ["/^\\.devcontainer/Dockerfile$/"],
"matchStrings": ["DLV_VERSION=(?<currentValue>v[0-9.]+)"],
"datasourceTemplate": "go",
"depNameTemplate": "github.com/go-delve/delve"
},
{
"customType": "regex",
"description": "devcontainer: gopls (go install golang.org/x/tools/gopls; its own module, versioned apart from x/tools)",
"managerFilePatterns": ["/^\\.devcontainer/Dockerfile$/"],
"matchStrings": ["GOPLS_VERSION=(?<currentValue>v[0-9.]+)"],
"datasourceTemplate": "go",
"depNameTemplate": "golang.org/x/tools/gopls"
},
{
"customType": "regex",
"description": "devcontainer: staticcheck (go install honnef.co/go/tools/cmd/staticcheck)",
"managerFilePatterns": ["/^\\.devcontainer/Dockerfile$/"],
"matchStrings": ["STATICCHECK_VERSION=(?<currentValue>v[0-9.]+)"],
"datasourceTemplate": "go",
"depNameTemplate": "honnef.co/go/tools"
}
]
}
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@
name: CI

on:
# Every pull request, whatever its base — deliberately NOT filtered to `branches: [main]`.
# A stacked PR (PR N based on PR N-1's branch) never matches a main-only filter, so it
# merged with no lint, no unit tests, and no e2e: PR #255 ran only the external review
# bots. Retargeting does not rescue it either — when the base branch merges, GitHub
# retargets the open PR onto main and fires `pull_request` with action `edited`, which
# is not in the default trigger set (opened/synchronize/reopened), so nothing re-runs.
# Validating every base costs a little runner time and is what makes a stack reviewable
# one PR at a time. See docs/design/watchrule-source-namespace/README.md for the stack
# this was found on.
pull_request:
branches: [main]
workflow_call:
outputs:
ci-image:
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
- **Skip conversational phrases** - No "Great!", "Certainly!", "Okay!" - get straight to the point
- **One clear message per concept** - Don't repeat the same information in different ways

## PULL REQUESTS

- Use a concise Conventional Commit-style PR title: `type(scope): imperative summary`, or omit the scope
when none adds useful context.
- Describe the final branch state, not the sequence of commits used to reach it.

## MANDATORY PRE-COMPLETION VALIDATION

**CRITICAL**: These commands MUST pass before any implementation is considered complete:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ actor — user, service account, or CI identity — while the committer never mo
kube-apiserver audit delivery (managed control planes like EKS/GKE/AKS generally do not expose it)
plus Valkey/Redis: see the [attribution setup guide](docs/attribution-setup-guide.md).

If attribution is enabled but a live change has no usable audit fact, its author is explicitly
`unknown (attribution unresolved)`, not the configured committer. For a change that should have a named
actor, treat that identity as a signal to verify the audit policy, webhook route, source identity, and
Redis connectivity.

**Valkey/Redis is optional but advised.** Without it the default mode works fine; adding it unlocks
warm-restart cursors, `CommitRequest` author capture, and attribution.

Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ includes:
dir: .
flatten: true
e2e:
taskfile: ./test/e2e/Taskfile.yml
taskfile: ./test/e2e/Taskfile-e2e.yml
dir: .
flatten: true

Expand Down
19 changes: 10 additions & 9 deletions api/v1alpha3/commitrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ type CommitRequestSpec struct {
Message string `json:"message,omitempty"`

// CloseDelaySeconds optionally delays closing the open commit window for this
// many seconds after the CommitRequest is attributed, acting as an extra collect
// window: changes the author makes in the meantime still join the open commit
// window and are included in the resulting commit. Omitted or 0 closes the window
// as soon as the CommitRequest is attributed to its author. The window can still
// be closed earlier by another author's change or by the provider's commit window
// timer, exactly as without a CommitRequest.
// many seconds after the CommitRequest attaches to a matching open window, acting as
// an extra collect window: matching changes that arrive in the meantime still join
// that window and are included in the resulting commit. Omitted or 0 closes the
// window as soon as the CommitRequest attaches. The window can still be closed
// earlier by another author's change or by the provider's commit window timer,
// exactly as without a CommitRequest.
// +optional
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=300
Expand All @@ -55,9 +55,10 @@ type CommitRequestSpec struct {
// while finalizing; Stalled=True when the finalize failed and needs attention.
// - AuthorAttributed (domain): binary and settled immediately. True
// (AttributedFromAdmission) when the submitter captured at admission named the
// commit author; False (CommitterFallback) when no admission record exists — the
// validate-operator-types webhook is not configured — and the commit is authored by the
// configured committer. False is not a failure and does not affect Ready.
// commit author; False (CommitterFallback) when capture ran but no admission record
// exists, or False (AuthorCaptureDisabled) when capture is disabled. In either False
// case the request claims no actor. False is not a failure and does not affect Ready;
// the final Git author remains the matching watch window's author.
// - Pushed (domain): True once the commit is in the remote repository.
type CommitRequestStatus struct {
// ObservedGeneration is the most recent generation observed by the controller.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# at admission, into the command-author Redis corner the controller reads back with no
# wait. Narrow by construction: one rule per command kind. failurePolicy is Ignore and
# the handler always allows, so a user's CommitRequest never depends on this webhook —
# a miss degrades to a committer-authored commit (AuthorAttributed=False).
# See docs/spec/commitrequest-admission-authorship.md §4.
# a miss leaves the request without a claimed actor (AuthorAttributed=False).
# See docs/spec/commitrequest-admission-authorship.md.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
Expand All @@ -26,7 +26,7 @@ webhooks:
namespace: {{ .Release.Namespace }}
path: /validate-operator-types
port: {{ .Values.servers.admission.port }}
# Ignore, not Fail: a missed author capture must degrade to the committer, never
# Ignore, not Fail: a missed author capture must leave the request unnamed, never
# reject a user's CommitRequest.
failurePolicy: Ignore
matchPolicy: Equivalent
Expand Down
8 changes: 4 additions & 4 deletions charts/gitops-reverser/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,16 @@ servers:
secretNameOverride: ""

# Validating admission server hosting the validate-operator-types webhook, which captures
# the submitter of each CommitRequest at admission and names them as the commit author
# the submitter of each CommitRequest at admission and records it as the request's named actor
# (read back by the controller with no wait). Independent of `attribution` below:
# attribution governs *mirrored-resource* authorship (from audit), this governs
# *command* authorship (from admission). Off → CommitRequests commit as the configured
# committer and report AuthorAttributed=False. See
# *command* authorship (from admission). Off → CommitRequests claim no actor and report
# AuthorAttributed=False. See
# docs/spec/commitrequest-admission-authorship.md.
#
# Command-author capture is Redis-backed: the captured author is written to Redis at admission and
# read back at reconcile. Enabled by default so the webhook is always installed, but it is a no-op
# without Redis: the pod runs healthy and CommitRequests commit as the configured committer
# without Redis: the pod runs healthy and CommitRequests claim no actor
# (AuthorAttributed=False) until queue.redis.addr is set.
admission:
enabled: true
Expand Down
12 changes: 6 additions & 6 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,11 @@ func main() {
// lives in its own Redis corner (author:v1:command), independent of
// --author-attribution (which governs mirrored-resource attribution). It is wired
// whenever the admission server is on; the controller reads the captured submitter
// back with no wait (docs/spec/commitrequest-admission-authorship.md §2, §6).
// back with no wait (docs/spec/commitrequest-admission-authorship.md).
//
// AuthorLookup must be a nil interface — not a non-nil interface wrapping a nil
// *CommandAuthorStore — when the webhook is off, so the controller's nil check
// selects finalize-as-committer immediately (AuthorAttributed=False) instead of
// selects the no-actor path immediately (AuthorAttributed=False) instead of
// dereferencing a nil store.
var commandAuthorStore *queue.CommandAuthorStore
var commandAuthorLookup controller.CommandAuthorLookup
Expand All @@ -325,10 +325,10 @@ func main() {
commandAuthorStore = redisStore.CommandAuthorStore()
commandAuthorLookup = commandAuthorStore
setupLog.Info("validate-operator-types webhook enabled: command submitters are captured at admission " +
"and named as the commit author")
"and recorded as the request's named actor")
} else {
setupLog.Info("validate-operator-types webhook enabled without Redis: command author capture is a " +
"no-op; CommitRequests commit as the configured committer (AuthorAttributed=False). " +
"no-op; CommitRequests claim no actor (AuthorAttributed=False). " +
"Set --redis-addr to capture command authors.")
}
}
Expand Down Expand Up @@ -478,7 +478,7 @@ func parseFlagsWithArgs(fs *flag.FlagSet, args []string) (appConfig, error) {
"and, when author attribution is enabled, the attribution facts. Leave empty to run without "+
"Redis: watches cold-replay on restart instead of resuming. Required by "+
"--author-attribution=true. --admission-webhook still runs without it, but command-author "+
"capture is a no-op: CommitRequests commit as the configured committer.")
"capture is a no-op: CommitRequests claim no actor.")
fs.BoolVar(&cfg.authorAttribution, "author-attribution", true,
"Name the real actor (human or service account) who caused each change as the Git commit author, "+
"resolved from matching audit facts; this runs the audit webhook ingress (default true). When "+
Expand Down Expand Up @@ -661,7 +661,7 @@ func validateAdmissionWebhookConfig(cfg appConfig) error {
return nil
}
// No redis-addr requirement here: the admission webhook stays enabled without Redis and
// simply no-ops command-author capture (commits fall back to the committer). Redis is
// simply no-ops command-author capture (CommitRequests claim no actor). Redis is
// wired only when configured; see the commandAuthorStore setup.
if _, _, err := splitBindAddress(cfg.admissionWebhookBindAddress); err != nil {
return fmt.Errorf("invalid admission-webhook-bind-address %q: %w", cfg.admissionWebhookBindAddress, err)
Expand Down
12 changes: 6 additions & 6 deletions config/crd/bases/configbutler.ai_commitrequests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ spec:
closeDelaySeconds:
description: |-
CloseDelaySeconds optionally delays closing the open commit window for this
many seconds after the CommitRequest is attributed, acting as an extra collect
window: changes the author makes in the meantime still join the open commit
window and are included in the resulting commit. Omitted or 0 closes the window
as soon as the CommitRequest is attributed to its author. The window can still
be closed earlier by another author's change or by the provider's commit window
timer, exactly as without a CommitRequest.
many seconds after the CommitRequest attaches to a matching open window, acting as
an extra collect window: matching changes that arrive in the meantime still join
that window and are included in the resulting commit. Omitted or 0 closes the
window as soon as the CommitRequest attaches. The window can still be closed
earlier by another author's change or by the provider's commit window timer,
exactly as without a CommitRequest.
format: int32
maximum: 300
minimum: 0
Expand Down
10 changes: 10 additions & 0 deletions config/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ spec:
- --redis-addr=valkey.valkey-e2e.svc.cluster.local:6379
# The e2e Valkey serves plain TCP, so opt out of the now-default Redis TLS.
- --redis-insecure
# Attribution waits this long for a matching audit fact before committing with the explicit
# `unknown (attribution unresolved)` author. Widened from the 3s default to buy headroom
# over the apiserver's --audit-webhook-batch-max-wait=1s
# (test/e2e/cluster/start-cluster.sh): measured fact delivery is ~0.8-1.1s even on an
# idle cluster, so 3s leaves only ~2s of slack for a 4-proc run.
#
# This is not a substitute for correct audit attribution configuration. A resolution that
# still ends as `absent` exposes an audit policy, route, source-identity, Redis, or timing
# issue for the change being exercised; the after-suite report keeps that distinction visible.
- --author-attribution-grace=10s
# Dev/e2e only: the in-cluster Gitea SSH host key is not pinned, so permit SSH
# when no known_hosts source exists. Never set this in production.
- --insecure-allow-missing-known-hosts
Expand Down
2 changes: 1 addition & 1 deletion config/webhook/validating-webhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ metadata:
# kinds (a CommitRequest today) at admission. This one IS product behavior and is
# packaged in the chart (charts/.../templates/validate-operator-types-webhook.yaml); the
# entry here is the e2e SUT's copy. See
# docs/spec/commitrequest-admission-authorship.md §4.
# docs/spec/commitrequest-admission-authorship.md.
#
# The cert-manager.io/inject-ca-from annotation injects the admission server CA bundle
# for every webhook in this configuration.
Expand Down
11 changes: 4 additions & 7 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# Index: what to read, and what is history

> Rebuilt 2026-07-11, when the tree went from 180 documents to 117.

There are 136 tracked markdown files here. **About 35 of them bind.** This page names
those. If a document is not on this page, it is either a user guide (see
[`README.md`](README.md)) or history you can safely not read.
This page names the documents that bind the current implementation. If a document is not
listed here, it is either a user guide (see [`README.md`](README.md)) or historical context
you can safely skip.

## The four folders, and what each one means

Expand Down Expand Up @@ -53,7 +51,7 @@ misled. Full list in [`spec/README.md`](spec/README.md); the ones that carry a
| [`gitpath-foreign-content-stringency.md`](spec/gitpath-foreign-content-stringency.md) | refusing a path that shadows foreign content |
| [`unsupported-folder-refusal-plan.md`](spec/unsupported-folder-refusal-plan.md) | `GitPathAccepted`, and refusing what we cannot own |
| [`commitrequest-design.md`](spec/commitrequest-design.md) | the CommitRequest window and its conditions |
| [`commitrequest-admission-authorship.md`](spec/commitrequest-admission-authorship.md) | how a real Kubernetes user becomes a commit author |
| [`commitrequest-admission-authorship.md`](spec/commitrequest-admission-authorship.md) | how command submitters are captured and matched to commit windows |
| [`where-validation-lives.md`](spec/where-validation-lives.md) | schema → CEL → **the reconciler**; a webhook only for what exists solely at admission |
| [`e2e-serial-registry.md`](spec/e2e-serial-registry.md) | which e2e specs must run Serial, and why |

Expand All @@ -78,7 +76,6 @@ Eleven other open items:
| [`release-image-reuse-plan.md`](design/release-image-reuse-plan.md) | PRs 2–5 unstarted |
| [`e2e-coverage-gaps-and-improvements-plan.md`](design/e2e-coverage-gaps-and-improvements-plan.md) | tests A/B/C still proposals |
| [`e2e-finish-plan.md`](design/e2e-finish-plan.md) | remaining e2e harness work |
| [`residual-e2e-flakes-2026-06-19.md`](design/residual-e2e-flakes-2026-06-19.md) | Flake B still open |
| [`sensitive-resource-diagnostics-follow-up.md`](design/sensitive-resource-diagnostics-follow-up.md) | deferred diagnostics |
| [`e2e-git-server-choice.md`](design/e2e-git-server-choice.md) | stay on Gitea or move to Forgejo — the `_csrf` pin is fixable in place on both, so the migration is now a preference call, not a fix; also why we adopt no SDK either way |
| [`watchrule-source-namespace/`](design/watchrule-source-namespace/README.md) | letting a WatchRule address a differently-named namespace on its source cluster — a deny-by-default `allowedSourceNamespaces` on the **GitTarget** (so scope is per-tenant, not a provider-wide union), unlocked by a false-by-default delegation flag on the ClusterProvider. Split into five implementable PRs: three prerequisite scope fixes (the namespace-blind resync sweep that would delete other namespaces' manifests, the cluster-wide/named stream collapse, and ClusterWatchRule's unchecked GitTarget attachment), the field and its gate, and the ceiling that makes the allow-list bind ClusterWatchRule too — required because a multi-tenant deployment runs a ClusterWatchRule per tenant from day one to capture CRDs |
Expand Down
4 changes: 2 additions & 2 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ This file is meant to track the smaller current backlog, not historical notes.

- [ ] Fix recurring full e2e flakiness around WatchRule/snapshot convergence.
This has shown up more than once as timeout-based failures in manager SOPS bootstrap and
signing snapshot-message specs, then passed on rerun. Capture and mitigation notes live in
[docs/design/e2e-watchrule-cross-spec-interference.md](spec/e2e-serial-registry.md).
signing snapshot-message specs, then passed on rerun. Capture and mitigation notes live in the
[e2e serial registry](spec/e2e-serial-registry.md).
This should be addressed before the next feature that expands commit-message, snapshot, or
write-window behavior, otherwise new failures will be hard to separate from existing timing debt.

Expand Down
Loading