Agent-work Queue (in-operator) Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Fold a durable, ordered admission queue into tatara-operator so every agent-spawning path enqueues a QueuedEvent instead of creating a Task directly; a dispatcher admits them in strict global seq FIFO under capacity N with a reserved alert pool M.
Architecture: A new QueuedEvent CRD is the durable buffer in etcd. Producers (SCM + Grafana webhooks, cron scans) write a QueuedEvent carrying a Task blueprint in its Payload. A new dispatcher controller drains the alert pool to M then the normal pool to N in seq order, building the Task from the blueprint on admission and freeing the slot when that Task reaches terminal. The execution-time atConcurrencyCap, the maxOpenTasks creation budget, and the per-repo laneOccupancy/selectPerRepo/selectCandidates selection layer are removed.
Tech Stack: Go (controller-runtime / kubebuilder), envtest, Prometheus client_golang, chi (webhook router), mise toolchain.
Global Constraints¶
- Newest stable Go; pin exact minor in
go.mod(do not change the pin in this plan). - KISS; no premature abstraction. NEVER introduce tech-debt; if forced, record in
MEMORY.md. - JSON logs via stdlib
log/slog. Log every business action at INFO with structured fields. - Metrics for everything that counts/fails: counters/gauges via the existing
internal/obspattern; exposed on the existing/metrics. - Charts cluster-agnostic; all new tunables are camelCase scalars on the Project CRD (no lists/plain-envs in values).
- New CRD fields are NOT auto-upgraded by Helm: deploy runbook must
kubectl applythe regenerated CRDs (operator-CRD-gap memory). - Single leader-elected active operator is assumed (seq allocation relies on it).
- Build/test through mise:
mise exec -- go test ./...,mise run test,make manifests. - Conventional commits:
type: short imperative. Branch offmainin a worktree; never build/deploy from a worktree. - TDD: failing test first, minimal impl, green, commit. Frequent commits.
File Structure¶
api/v1alpha1/queuedevent_types.go(create) -QueuedEvent/QueuedEventList,QueuedEventSpec,QueuedEventPayload,QueuedEventStatus, kind/class constants,ValidateQueuedEventSpec. Owns the queue data model.api/v1alpha1/queuedevent_types_test.go(create) - validation tests.api/v1alpha1/zz_generated.deepcopy.go(modify, viamake generate) - deepcopy for the new types.api/v1alpha1/groupversion_info.goor*_types.goinit()(modify) - registerQueuedEventin the scheme builder (kubebuilderSchemeBuilder.Register).api/v1alpha1/project_types.go(modify) - addQueue *QueueSpectoProjectSpec;QueueSpectype;QueueCapacity/AlertCapacity/QueuedAutonomousCapdefault helpers.api/v1alpha1/project_types_test.go(modify/create) - default helper tests.internal/queue/allocator.go(create) -SeqAllocator+Recover/Next;SeqRecoverermanager Runnable.internal/queue/allocator_test.go(create) - monotonic + recovery tests.internal/controller/queue_enqueue.go(create) -EnqueueEventhelper +buildTaskFromQueuedEvent+ dedup query. Shared by webhook + crons + dispatcher.internal/controller/queue_enqueue_test.go(create) - enqueue/build/dedup tests.internal/controller/queue_controller.go(create) -DispatcherReconciler(inflight, Done-marking, admission, migration, SetupWithManager).internal/controller/queue_controller_test.go(create) - dispatcher unit + envtest.internal/controller/projectscan.go(modify) - crons enqueue instead of create; deleteselectCandidates/selectPerRepo/laneOccupancy/maxOpenTasks/openTaskCount/budget threading;createScanTask/createBrainstormTask/createHealthCheckTaskbecome enqueue.internal/controller/task_controller.go(modify) - deleteatConcurrencyCap+ callsite.internal/controller/lifecycle.go(modify) - delete theatConcurrencyCapcallsite.internal/webhook/server.go(modify) -handleWorkItem,createLifecycleTaskAtTriage,createIncidentTask,reactivateTaskenqueue instead of create;incidentDedupmoves into the enqueue dedup.internal/webhook/server.goConfig(modify) - addSeq *queue.SeqAllocator.internal/obs/operator_metrics.go(modify) - queue depth/inflight/admission metrics.cmd/manager/wire.go(modify) - constructSeqAllocator, registerSeqRecoverer+DispatcherReconciler, inject allocator intoProjectReconciler+ webhookConfig.config/crd/...+charts/tatara-operator/crds/...(modify, viamake manifests) - regenerated CRD manifests.
Task 1: QueuedEvent CRD types + validation¶
Files: - Create: api/v1alpha1/queuedevent_types.go - Create: api/v1alpha1/queuedevent_types_test.go - Modify: api/v1alpha1/zz_generated.deepcopy.go (via make generate)
Interfaces: - Produces: QueuedEvent, QueuedEventList, QueuedEventSpec{Seq int64; Class string; Kind string; Autonomous bool; ProjectRef string; RepositoryRef string; DedupKey string; Payload QueuedEventPayload}, QueuedEventPayload{Goal string; Kind string; RepositoryRef string; Source *TaskSource; Labels map[string]string; Annotations map[string]string; GenerateName string; Name string; Provider string; PodRepo string}, QueuedEventStatus{State string; TaskRef string; AdmittedAt *metav1.Time}, consts QueueClassNormal="normal", QueueClassAlert="alert", QueueStateQueued="Queued", QueueStateAdmitted="Admitted", QueueStateDone="Done", func ValidateQueuedEventSpec(spec QueuedEventSpec) error.
- Step 1: Write the failing test
// api/v1alpha1/queuedevent_types_test.go
package v1alpha1
import "testing"
func TestValidateQueuedEventSpec(t *testing.T) {
tests := []struct {
name string
spec QueuedEventSpec
wantErr bool
}{
{"valid normal repo-scoped", QueuedEventSpec{Seq: 1, Class: QueueClassNormal, Kind: "issueLifecycle", ProjectRef: "p", RepositoryRef: "r", Payload: QueuedEventPayload{Kind: "issueLifecycle"}}, false},
{"valid alert project-scoped", QueuedEventSpec{Seq: 2, Class: QueueClassAlert, Kind: "incident", ProjectRef: "p", Payload: QueuedEventPayload{Kind: "incident"}}, false},
{"bad class", QueuedEventSpec{Seq: 1, Class: "urgent", Kind: "incident", ProjectRef: "p"}, true},
{"bad kind", QueuedEventSpec{Seq: 1, Class: QueueClassNormal, Kind: "nope", ProjectRef: "p"}, true},
{"missing projectRef", QueuedEventSpec{Seq: 1, Class: QueueClassNormal, Kind: "review"}, true},
{"project-scoped kind with repoRef", QueuedEventSpec{Seq: 1, Class: QueueClassAlert, Kind: "incident", ProjectRef: "p", RepositoryRef: "r"}, true},
{"repo-scoped kind without repoRef", QueuedEventSpec{Seq: 1, Class: QueueClassNormal, Kind: "issueLifecycle", ProjectRef: "p"}, true},
{"zero seq", QueuedEventSpec{Seq: 0, Class: QueueClassNormal, Kind: "review", ProjectRef: "p", RepositoryRef: "r"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateQueuedEventSpec(tt.spec)
if (err != nil) != tt.wantErr {
t.Fatalf("err=%v wantErr=%v", err, tt.wantErr)
}
})
}
}
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./api/v1alpha1/ -run TestValidateQueuedEventSpec -v Expected: FAIL (undefined: QueuedEventSpec / ValidateQueuedEventSpec).
- Step 3: Write minimal implementation
// api/v1alpha1/queuedevent_types.go
package v1alpha1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
QueueClassNormal = "normal"
QueueClassAlert = "alert"
QueueStateQueued = "Queued"
QueueStateAdmitted = "Admitted"
QueueStateDone = "Done"
)
// QueuedEventPayload is the Task blueprint a producer stashes; the dispatcher
// rebuilds the Task from it verbatim on admission. Producers keep ownership of
// label/goal/source construction so the dispatcher stays generic.
type QueuedEventPayload struct {
Goal string `json:"goal,omitempty"`
Kind string `json:"kind"`
RepositoryRef string `json:"repositoryRef,omitempty"`
Source *TaskSource `json:"source,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
// GenerateName is used when Name is empty; Name is a fixed deterministic
// Task name (issueLifecycle) that makes admission idempotent.
GenerateName string `json:"generateName,omitempty"`
Name string `json:"name,omitempty"`
// Provider + PodRepo feed agent.StampPodName on the rebuilt Task.
Provider string `json:"provider,omitempty"`
PodRepo string `json:"podRepo,omitempty"`
}
type QueuedEventSpec struct {
Seq int64 `json:"seq"`
Class string `json:"class"`
Kind string `json:"kind"`
Autonomous bool `json:"autonomous,omitempty"`
ProjectRef string `json:"projectRef"`
RepositoryRef string `json:"repositoryRef,omitempty"`
DedupKey string `json:"dedupKey,omitempty"`
Payload QueuedEventPayload `json:"payload"`
}
type QueuedEventStatus struct {
State string `json:"state,omitempty"`
TaskRef string `json:"taskRef,omitempty"`
AdmittedAt *metav1.Time `json:"admittedAt,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Seq",type=integer,JSONPath=`.spec.seq`
// +kubebuilder:printcolumn:name="Class",type=string,JSONPath=`.spec.class`
// +kubebuilder:printcolumn:name="Kind",type=string,JSONPath=`.spec.kind`
// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state`
type QueuedEvent struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec QueuedEventSpec `json:"spec,omitempty"`
Status QueuedEventStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type QueuedEventList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []QueuedEvent `json:"items"`
}
func init() {
SchemeBuilder.Register(&QueuedEvent{}, &QueuedEventList{})
}
// ValidateQueuedEventSpec mirrors ValidateTaskSpec's kind/repo-scoping rules.
func ValidateQueuedEventSpec(spec QueuedEventSpec) error {
if spec.Seq <= 0 {
return fmt.Errorf("queuedevent: seq must be positive")
}
if spec.Class != QueueClassNormal && spec.Class != QueueClassAlert {
return fmt.Errorf("queuedevent: invalid class %q", spec.Class)
}
if spec.ProjectRef == "" {
return fmt.Errorf("queuedevent: projectRef required")
}
if !repoScopedKinds[spec.Kind] && !projectScopedKinds[spec.Kind] {
return fmt.Errorf("queuedevent: invalid kind %q", spec.Kind)
}
if projectScopedKinds[spec.Kind] && spec.RepositoryRef != "" {
return fmt.Errorf("queuedevent: kind %q is project-scoped, repositoryRef must be empty", spec.Kind)
}
if repoScopedKinds[spec.Kind] && spec.RepositoryRef == "" {
return fmt.Errorf("queuedevent: kind %q requires repositoryRef", spec.Kind)
}
return nil
}
Note: repoScopedKinds/projectScopedKinds already exist in task_types.go (same package). issueLifecycle/review/mrScan/issueScan must be present in repoScopedKinds; if any cron kind is missing there, add it in this step (boy-scout) and assert via the test's "valid normal repo-scoped" case.
- Step 4: Generate deepcopy + run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- make generate && mise exec -- go test ./api/v1alpha1/ -run TestValidateQueuedEventSpec -v Expected: make generate updates zz_generated.deepcopy.go; test PASS.
- Step 5: Commit
git add api/v1alpha1/queuedevent_types.go api/v1alpha1/queuedevent_types_test.go api/v1alpha1/zz_generated.deepcopy.go
git commit -m "feat: QueuedEvent CRD types + validation"
Task 2: QueueSpec on ProjectSpec + default helpers¶
Files: - Modify: api/v1alpha1/project_types.go (add Queue *QueueSpec to ProjectSpec near Scm/Memory/Grafana; add QueueSpec) - Modify: api/v1alpha1/project_types_test.go
Interfaces: - Consumes: ProjectSpec (Task 0/existing), QueuedEvent consts (Task 1). - Produces: type QueueSpec struct{ Capacity int; AlertCapacity int; QueuedAutonomousCap int }, methods func (p *Project) QueueCapacity() int, func (p *Project) AlertCapacity() int, func (p *Project) QueuedAutonomousCap() int.
- Step 1: Write the failing test
// api/v1alpha1/project_types_test.go (add)
func TestQueueDefaults(t *testing.T) {
// nil Queue: capacity falls back to MaxConcurrentTasks, cap to MaxOpenTasks, alert to 1.
p := &Project{Spec: ProjectSpec{MaxConcurrentTasks: 5, MaxOpenTasks: 6}}
if got := p.QueueCapacity(); got != 5 {
t.Fatalf("QueueCapacity nil-queue = %d, want 5", got)
}
if got := p.QueuedAutonomousCap(); got != 6 {
t.Fatalf("QueuedAutonomousCap nil-queue = %d, want 6", got)
}
if got := p.AlertCapacity(); got != 1 {
t.Fatalf("AlertCapacity nil-queue = %d, want 1", got)
}
// explicit Queue overrides.
p2 := &Project{Spec: ProjectSpec{MaxConcurrentTasks: 5, Queue: &QueueSpec{Capacity: 2, AlertCapacity: 3, QueuedAutonomousCap: 10}}}
if p2.QueueCapacity() != 2 || p2.AlertCapacity() != 3 || p2.QueuedAutonomousCap() != 10 {
t.Fatalf("explicit queue not honoured: %d/%d/%d", p2.QueueCapacity(), p2.AlertCapacity(), p2.QueuedAutonomousCap())
}
// hard floor when nothing set anywhere.
p3 := &Project{}
if p3.QueueCapacity() != 3 || p3.QueuedAutonomousCap() != 3 || p3.AlertCapacity() != 1 {
t.Fatalf("hard floors wrong: %d/%d/%d", p3.QueueCapacity(), p3.QueuedAutonomousCap(), p3.AlertCapacity())
}
}
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./api/v1alpha1/ -run TestQueueDefaults -v Expected: FAIL (undefined: QueueSpec / QueueCapacity).
- Step 3: Write minimal implementation
// api/v1alpha1/project_types.go
// in ProjectSpec, sibling of Grafana:
// // +optional
// Queue *QueueSpec `json:"queue,omitempty"`
// QueueSpec configures the in-operator agent-work admission queue.
type QueueSpec struct {
// Capacity N: max concurrently-admitted normal-class events (defaults to
// MaxConcurrentTasks, else 3).
// +optional
Capacity int `json:"capacity,omitempty"`
// AlertCapacity M: reserved concurrent slots for alert-class events (default 1).
// +optional
AlertCapacity int `json:"alertCapacity,omitempty"`
// QueuedAutonomousCap K: max Queued autonomous (cron) events; crons stop
// enqueuing past it (defaults to MaxOpenTasks, else 3). Webhooks/alerts exempt.
// +optional
QueuedAutonomousCap int `json:"queuedAutonomousCap,omitempty"`
}
func (p *Project) QueueCapacity() int {
if p.Spec.Queue != nil && p.Spec.Queue.Capacity > 0 {
return p.Spec.Queue.Capacity
}
if p.Spec.MaxConcurrentTasks > 0 {
return p.Spec.MaxConcurrentTasks
}
return 3
}
func (p *Project) AlertCapacity() int {
if p.Spec.Queue != nil && p.Spec.Queue.AlertCapacity > 0 {
return p.Spec.Queue.AlertCapacity
}
return 1
}
func (p *Project) QueuedAutonomousCap() int {
if p.Spec.Queue != nil && p.Spec.Queue.QueuedAutonomousCap > 0 {
return p.Spec.Queue.QueuedAutonomousCap
}
if p.Spec.MaxOpenTasks > 0 {
return p.Spec.MaxOpenTasks
}
return 3
}
- Step 4: Run tests + regenerate deepcopy
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- make generate && mise exec -- go test ./api/v1alpha1/ -run TestQueueDefaults -v Expected: PASS.
- Step 5: Commit
git add api/v1alpha1/project_types.go api/v1alpha1/project_types_test.go api/v1alpha1/zz_generated.deepcopy.go
git commit -m "feat: QueueSpec on ProjectSpec with capacity/alert/cap defaults"
Task 3: SeqAllocator + boot recovery¶
Files: - Create: internal/queue/allocator.go - Create: internal/queue/allocator_test.go
Interfaces: - Produces: type SeqAllocator struct{...}, func NewSeqAllocator() *SeqAllocator, func (a *SeqAllocator) Recover(maxSeq int64), func (a *SeqAllocator) Next() int64, type SeqRecoverer struct{ Client client.Client; Alloc *SeqAllocator; Namespace string } implementing Start(ctx context.Context) error (manager.Runnable). - Consumes: QueuedEventList (Task 1).
- Step 1: Write the failing test
// internal/queue/allocator_test.go
package queue
import (
"sync"
"testing"
)
func TestSeqAllocator_MonotonicFromZero(t *testing.T) {
a := NewSeqAllocator()
if a.Next() != 1 || a.Next() != 2 || a.Next() != 3 {
t.Fatal("expected 1,2,3 from a fresh allocator")
}
}
func TestSeqAllocator_RecoverMaxPlusOne(t *testing.T) {
a := NewSeqAllocator()
a.Recover(41)
if got := a.Next(); got != 42 {
t.Fatalf("Next after Recover(41) = %d, want 42", got)
}
}
func TestSeqAllocator_ConcurrentUnique(t *testing.T) {
a := NewSeqAllocator()
const n = 1000
seen := make([]int64, n)
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) { defer wg.Done(); seen[i] = a.Next() }(i)
}
wg.Wait()
set := map[int64]bool{}
for _, v := range seen {
if set[v] {
t.Fatalf("duplicate seq %d", v)
}
set[v] = true
}
}
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/queue/ -v Expected: FAIL (no package / undefined SeqAllocator).
- Step 3: Write minimal implementation
// internal/queue/allocator.go
package queue
import (
"context"
"sync"
tatarav1alpha1 "github.com/szymonrychu/tatara-operator/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
// SeqAllocator hands out a strictly increasing int64 sequence. Correctness
// relies on a single leader-elected active operator (one allocator instance).
type SeqAllocator struct {
mu sync.Mutex
next int64
}
func NewSeqAllocator() *SeqAllocator { return &SeqAllocator{next: 0} }
// Recover sets the counter so the next allocation is maxSeq+1. Call once at boot
// with the max Seq of existing QueuedEvents.
func (a *SeqAllocator) Recover(maxSeq int64) {
a.mu.Lock()
defer a.mu.Unlock()
if maxSeq > a.next {
a.next = maxSeq
}
}
func (a *SeqAllocator) Next() int64 {
a.mu.Lock()
defer a.mu.Unlock()
a.next++
return a.next
}
// SeqRecoverer is a manager.Runnable that recovers the allocator high-water mark
// from existing QueuedEvents after the cache syncs.
type SeqRecoverer struct {
Client client.Client
Alloc *SeqAllocator
Namespace string
}
func (s *SeqRecoverer) Start(ctx context.Context) error {
var list tatarav1alpha1.QueuedEventList
if err := s.Client.List(ctx, &list, client.InNamespace(s.Namespace)); err != nil {
return err
}
var max int64
for i := range list.Items {
if list.Items[i].Spec.Seq > max {
max = list.Items[i].Spec.Seq
}
}
s.Alloc.Recover(max)
log.FromContext(ctx).Info("queue: seq recovered", "action", "seq_recover", "max", max)
return nil
}
- Step 4: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/queue/ -v Expected: PASS.
- Step 5: Commit
git add internal/queue/
git commit -m "feat: SeqAllocator + boot recovery runnable for the work queue"
Task 4: Enqueue helper + Task builder + dedup¶
Files: - Create: internal/controller/queue_enqueue.go - Create: internal/controller/queue_enqueue_test.go
Interfaces: - Consumes: SeqAllocator (Task 3), QueuedEvent* (Task 1), existing agent.StampPodName(task, project, provider, repo), existing Task/TaskSource. - Produces: - const LabelQueuedEvent = "tatara.dev/queued-event", const LabelDedupKey = "tatara.dev/dedup-key" - func EnqueueEvent(ctx context.Context, c client.Client, alloc *queue.SeqAllocator, proj *tatarav1alpha1.Project, class string, autonomous bool, dedupKey string, payload tatarav1alpha1.QueuedEventPayload) (*tatarav1alpha1.QueuedEvent, bool, error) - returns (event, created, error); created=false on dedup skip. - func buildTaskFromQueuedEvent(qe *tatarav1alpha1.QueuedEvent, proj *tatarav1alpha1.Project, scheme *runtime.Scheme) (*tatarav1alpha1.Task, error) - func dedupExists(ctx context.Context, c client.Client, ns, projectRef, dedupKey string) (bool, error)
- Step 1: Write the failing test
// internal/controller/queue_enqueue_test.go
package controller
import (
"context"
"testing"
tatarav1alpha1 "github.com/szymonrychu/tatara-operator/api/v1alpha1"
"github.com/szymonrychu/tatara-operator/internal/queue"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func testProject(name, ns string) *tatarav1alpha1.Project {
return &tatarav1alpha1.Project{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns}}
}
func TestEnqueueEvent_AssignsSeqAndFields(t *testing.T) {
scheme := newTestScheme(t) // helper already present in suite_test.go / export_test.go
c := fake.NewClientBuilder().WithScheme(scheme).Build()
alloc := queue.NewSeqAllocator()
proj := testProject("p", "tatara")
pl := tatarav1alpha1.QueuedEventPayload{Kind: "incident", GenerateName: "incident-"}
qe, created, err := EnqueueEvent(context.Background(), c, alloc, proj, tatarav1alpha1.QueueClassAlert, false, "grp1", pl)
if err != nil || !created {
t.Fatalf("created=%v err=%v", created, err)
}
if qe.Spec.Seq != 1 || qe.Spec.Class != tatarav1alpha1.QueueClassAlert || qe.Spec.Kind != "incident" {
t.Fatalf("bad spec: %+v", qe.Spec)
}
if qe.Labels[LabelDedupKey] != "grp1" || qe.Status.State != tatarav1alpha1.QueueStateQueued {
t.Fatalf("bad labels/state: %v %q", qe.Labels, qe.Status.State)
}
}
func TestEnqueueEvent_DedupSkips(t *testing.T) {
scheme := newTestScheme(t)
c := fake.NewClientBuilder().WithScheme(scheme).Build()
alloc := queue.NewSeqAllocator()
proj := testProject("p", "tatara")
pl := tatarav1alpha1.QueuedEventPayload{Kind: "incident", GenerateName: "incident-"}
if _, created, _ := EnqueueEvent(context.Background(), c, alloc, proj, tatarav1alpha1.QueueClassAlert, false, "grp1", pl); !created {
t.Fatal("first enqueue should create")
}
_, created, err := EnqueueEvent(context.Background(), c, alloc, proj, tatarav1alpha1.QueueClassAlert, false, "grp1", pl)
if err != nil {
t.Fatal(err)
}
if created {
t.Fatal("duplicate dedupKey should skip")
}
}
func TestBuildTaskFromQueuedEvent(t *testing.T) {
scheme := newTestScheme(t)
proj := testProject("p", "tatara")
qe := &tatarav1alpha1.QueuedEvent{
ObjectMeta: metav1.ObjectMeta{Name: "qe-1", Namespace: "tatara"},
Spec: tatarav1alpha1.QueuedEventSpec{
Seq: 1, Class: tatarav1alpha1.QueueClassNormal, Kind: "review", ProjectRef: "p", RepositoryRef: "r",
Payload: tatarav1alpha1.QueuedEventPayload{
Kind: "review", RepositoryRef: "r", Goal: "g", GenerateName: "scan-",
Labels: map[string]string{"x": "y"}, Provider: "github", PodRepo: "r",
},
},
}
task, err := buildTaskFromQueuedEvent(qe, proj, scheme)
if err != nil {
t.Fatal(err)
}
if task.Spec.Kind != "review" || task.Spec.Goal != "g" || task.Spec.RepositoryRef != "r" {
t.Fatalf("bad task spec: %+v", task.Spec)
}
if task.Labels[LabelQueuedEvent] != "qe-1" || task.Labels["x"] != "y" {
t.Fatalf("missing labels: %v", task.Labels)
}
if task.GenerateName != "scan-" {
t.Fatalf("bad generateName: %q", task.GenerateName)
}
}
If newTestScheme does not exist yet, add a tiny helper in export_test.go that builds a scheme with tatarav1alpha1.AddToScheme.
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run 'TestEnqueueEvent|TestBuildTaskFromQueuedEvent' -v Expected: FAIL (undefined EnqueueEvent/buildTaskFromQueuedEvent/LabelQueuedEvent).
- Step 3: Write minimal implementation
// internal/controller/queue_enqueue.go
package controller
import (
"context"
"fmt"
tatarav1alpha1 "github.com/szymonrychu/tatara-operator/api/v1alpha1"
"github.com/szymonrychu/tatara-operator/internal/agent"
"github.com/szymonrychu/tatara-operator/internal/queue"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
const (
LabelQueuedEvent = "tatara.dev/queued-event"
LabelDedupKey = "tatara.dev/dedup-key"
)
// dedupExists reports whether a non-Done QueuedEvent or a non-terminal Task with
// dedupKey already exists for the project.
func dedupExists(ctx context.Context, c client.Client, ns, projectRef, dedupKey string) (bool, error) {
if dedupKey == "" {
return false, nil
}
var qel tatarav1alpha1.QueuedEventList
if err := c.List(ctx, &qel, client.InNamespace(ns), client.MatchingLabels{LabelDedupKey: dedupKey}); err != nil {
return false, err
}
for i := range qel.Items {
if qel.Items[i].Spec.ProjectRef == projectRef && qel.Items[i].Status.State != tatarav1alpha1.QueueStateDone {
return true, nil
}
}
var tl tatarav1alpha1.TaskList
if err := c.List(ctx, &tl, client.InNamespace(ns), client.MatchingLabels{LabelDedupKey: dedupKey}); err != nil {
return false, err
}
for i := range tl.Items {
if tl.Items[i].Spec.ProjectRef == projectRef && !tatarav1alpha1.TaskTerminal(&tl.Items[i]) {
return true, nil
}
}
return false, nil
}
// EnqueueEvent writes a QueuedEvent (seq-assigned, owned by Project, state=Queued).
// Returns created=false when dedupKey already has live work.
func EnqueueEvent(ctx context.Context, c client.Client, alloc *queue.SeqAllocator, proj *tatarav1alpha1.Project,
class string, autonomous bool, dedupKey string, payload tatarav1alpha1.QueuedEventPayload) (*tatarav1alpha1.QueuedEvent, bool, error) {
dup, err := dedupExists(ctx, c, proj.Namespace, proj.Name, dedupKey)
if err != nil {
return nil, false, err
}
if dup {
return nil, false, nil
}
labels := map[string]string{}
if dedupKey != "" {
labels[LabelDedupKey] = dedupKey
}
qe := &tatarav1alpha1.QueuedEvent{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "qe-",
Namespace: proj.Namespace,
Labels: labels,
},
Spec: tatarav1alpha1.QueuedEventSpec{
Seq: alloc.Next(),
Class: class,
Kind: payload.Kind,
Autonomous: autonomous,
ProjectRef: proj.Name,
RepositoryRef: payload.RepositoryRef,
DedupKey: dedupKey,
Payload: payload,
},
}
if err := controllerutil.SetControllerReference(proj, qe, c.Scheme()); err != nil {
return nil, false, fmt.Errorf("enqueue: set ownerref: %w", err)
}
if err := c.Create(ctx, qe); err != nil {
return nil, false, fmt.Errorf("enqueue: create queuedevent: %w", err)
}
qe.Status.State = tatarav1alpha1.QueueStateQueued
if err := c.Status().Update(ctx, qe); err != nil {
return nil, false, fmt.Errorf("enqueue: set state: %w", err)
}
return qe, true, nil
}
// buildTaskFromQueuedEvent reconstructs the Task the producer described, labelled
// with the QueuedEvent name (dispatcher completion mapping) and dedup key.
func buildTaskFromQueuedEvent(qe *tatarav1alpha1.QueuedEvent, proj *tatarav1alpha1.Project, scheme *runtime.Scheme) (*tatarav1alpha1.Task, error) {
p := qe.Spec.Payload
labels := map[string]string{}
for k, v := range p.Labels {
labels[k] = v
}
labels[LabelQueuedEvent] = qe.Name
if qe.Spec.DedupKey != "" {
labels[LabelDedupKey] = qe.Spec.DedupKey
}
om := metav1.ObjectMeta{
Namespace: qe.Namespace,
Labels: labels,
Annotations: p.Annotations,
}
if p.Name != "" {
om.Name = p.Name
} else {
om.GenerateName = p.GenerateName
}
task := &tatarav1alpha1.Task{
ObjectMeta: om,
Spec: tatarav1alpha1.TaskSpec{
ProjectRef: proj.Name,
RepositoryRef: p.RepositoryRef,
Goal: p.Goal,
Kind: p.Kind,
Source: p.Source,
},
}
agent.StampPodName(task, proj.Name, p.Provider, p.PodRepo)
if err := controllerutil.SetControllerReference(proj, task, scheme); err != nil {
return nil, fmt.Errorf("build task: set ownerref: %w", err)
}
return task, nil
}
- Step 4: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run 'TestEnqueueEvent|TestBuildTaskFromQueuedEvent' -v Expected: PASS.
- Step 5: Commit
git add internal/controller/queue_enqueue.go internal/controller/queue_enqueue_test.go internal/controller/export_test.go
git commit -m "feat: enqueue helper, Task blueprint builder, dedup query"
Task 5: Dispatcher - inflight counts + Done marking¶
Files: - Create: internal/controller/queue_controller.go - Create: internal/controller/queue_controller_test.go
Interfaces: - Consumes: QueuedEvent* (Task 1), LabelQueuedEvent (Task 4), TaskTerminal (existing). - Produces: - type DispatcherReconciler struct{ client.Client; Scheme *runtime.Scheme; Alloc *queue.SeqAllocator; Metrics *obs.OperatorMetrics } - func (r *DispatcherReconciler) poolInflight(qes []tatarav1alpha1.QueuedEvent, tasks []tatarav1alpha1.Task, class string) int - func (r *DispatcherReconciler) reconcileDone(ctx, qes, tasks) (changed bool, err error) (marks Admitted whose Task is terminal/missing -> Done) - helper taskByName(tasks []tatarav1alpha1.Task, name string) *tatarav1alpha1.Task
- Step 1: Write the failing test
// internal/controller/queue_controller_test.go
package controller
import (
"testing"
tatarav1alpha1 "github.com/szymonrychu/tatara-operator/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func qe(name, class, state, taskRef string) tatarav1alpha1.QueuedEvent {
return tatarav1alpha1.QueuedEvent{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "tatara"},
Spec: tatarav1alpha1.QueuedEventSpec{Class: class, ProjectRef: "p"},
Status: tatarav1alpha1.QueuedEventStatus{State: state, TaskRef: taskRef},
}
}
func tk(name, phase, lifecycle, queuedEvent string) tatarav1alpha1.Task {
return tatarav1alpha1.Task{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "tatara", Labels: map[string]string{LabelQueuedEvent: queuedEvent}},
Spec: tatarav1alpha1.TaskSpec{ProjectRef: "p"},
Status: tatarav1alpha1.TaskStatus{Phase: phase, LifecycleState: lifecycle},
}
}
func TestPoolInflight_CountsAdmittedNonTerminal(t *testing.T) {
r := &DispatcherReconciler{}
qes := []tatarav1alpha1.QueuedEvent{
qe("a", tatarav1alpha1.QueueClassNormal, tatarav1alpha1.QueueStateAdmitted, "t-a"), // running -> counts
qe("b", tatarav1alpha1.QueueClassNormal, tatarav1alpha1.QueueStateAdmitted, "t-b"), // terminal -> not
qe("c", tatarav1alpha1.QueueClassAlert, tatarav1alpha1.QueueStateAdmitted, "t-c"), // alert running
qe("d", tatarav1alpha1.QueueClassNormal, tatarav1alpha1.QueueStateQueued, ""), // queued -> not
}
tasks := []tatarav1alpha1.Task{
tk("t-a", "Running", "", "a"),
tk("t-b", "Succeeded", "", "b"),
tk("t-c", "Running", "", "c"),
}
if got := r.poolInflight(qes, tasks, tatarav1alpha1.QueueClassNormal); got != 1 {
t.Fatalf("normal inflight = %d, want 1", got)
}
if got := r.poolInflight(qes, tasks, tatarav1alpha1.QueueClassAlert); got != 1 {
t.Fatalf("alert inflight = %d, want 1", got)
}
}
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestPoolInflight -v Expected: FAIL (undefined DispatcherReconciler).
- Step 3: Write minimal implementation
// internal/controller/queue_controller.go
package controller
import (
"context"
tatarav1alpha1 "github.com/szymonrychu/tatara-operator/api/v1alpha1"
"github.com/szymonrychu/tatara-operator/internal/obs"
"github.com/szymonrychu/tatara-operator/internal/queue"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
type DispatcherReconciler struct {
client.Client
Scheme *runtime.Scheme
Alloc *queue.SeqAllocator
Metrics *obs.OperatorMetrics
}
func taskByName(tasks []tatarav1alpha1.Task, name string) *tatarav1alpha1.Task {
for i := range tasks {
if tasks[i].Name == name {
return &tasks[i]
}
}
return nil
}
// poolInflight counts Admitted QueuedEvents of class whose Task is still
// non-terminal. (Migration of unlabelled pre-queue Tasks is added in Task 7.)
func (r *DispatcherReconciler) poolInflight(qes []tatarav1alpha1.QueuedEvent, tasks []tatarav1alpha1.Task, class string) int {
n := 0
for i := range qes {
q := &qes[i]
if q.Spec.Class != class || q.Status.State != tatarav1alpha1.QueueStateAdmitted {
continue
}
t := taskByName(tasks, q.Status.TaskRef)
if t != nil && !tatarav1alpha1.TaskTerminal(t) {
n++
}
}
return n
}
// reconcileDone flips Admitted events whose Task is terminal or gone to Done.
func (r *DispatcherReconciler) reconcileDone(ctx context.Context, qes []tatarav1alpha1.QueuedEvent, tasks []tatarav1alpha1.Task) (bool, error) {
changed := false
for i := range qes {
q := &qes[i]
if q.Status.State != tatarav1alpha1.QueueStateAdmitted {
continue
}
t := taskByName(tasks, q.Status.TaskRef)
if t == nil || tatarav1alpha1.TaskTerminal(t) {
q.Status.State = tatarav1alpha1.QueueStateDone
if err := r.Status().Update(ctx, q); err != nil {
return changed, err
}
changed = true
log.FromContext(ctx).Info("queue: event done", "action", "queue_done", "resource_id", q.Name, "class", q.Spec.Class)
}
}
return changed, nil
}
- Step 4: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestPoolInflight -v Expected: PASS.
- Step 5: Commit
git add internal/controller/queue_controller.go internal/controller/queue_controller_test.go
git commit -m "feat: dispatcher inflight counting + Done marking"
Task 6: Dispatcher - admission (alert pool then normal, FIFO, capacity)¶
Files: - Modify: internal/controller/queue_controller.go - Modify: internal/controller/queue_controller_test.go
Interfaces: - Consumes: poolInflight/reconcileDone (Task 5), buildTaskFromQueuedEvent (Task 4), QueueCapacity/AlertCapacity (Task 2). - Produces: func (r *DispatcherReconciler) admit(ctx context.Context, proj *tatarav1alpha1.Project, qes []tatarav1alpha1.QueuedEvent, tasks []tatarav1alpha1.Task) error.
- Step 1: Write the failing test (envtest-backed)
// internal/controller/queue_controller_test.go (add; uses the envtest k8sClient from suite_test.go)
func TestAdmit_AlertBeforeNormal_AndCapacity(t *testing.T) {
ctx := context.Background()
ns := "tatara"
proj := &tatarav1alpha1.Project{
ObjectMeta: metav1.ObjectMeta{Name: "p-admit", Namespace: ns},
Spec: tatarav1alpha1.ProjectSpec{Queue: &tatarav1alpha1.QueueSpec{Capacity: 1, AlertCapacity: 1}},
}
mustCreate(t, ctx, proj) // helper: k8sClient.Create + cleanup
mkQE := func(seq int64, class string) *tatarav1alpha1.QueuedEvent {
q := &tatarav1alpha1.QueuedEvent{
ObjectMeta: metav1.ObjectMeta{GenerateName: "qe-", Namespace: ns},
Spec: tatarav1alpha1.QueuedEventSpec{
Seq: seq, Class: class, Kind: "incident", ProjectRef: proj.Name,
Payload: tatarav1alpha1.QueuedEventPayload{Kind: "incident", GenerateName: "x-"},
},
}
mustCreate(t, ctx, q)
q.Status.State = tatarav1alpha1.QueueStateQueued
mustStatusUpdate(t, ctx, q)
return q
}
normalQE := mkQE(1, tatarav1alpha1.QueueClassNormal) // older
alertQE := mkQE(2, tatarav1alpha1.QueueClassAlert) // newer but priority pool
r := &DispatcherReconciler{Client: k8sClient, Scheme: k8sClient.Scheme()}
qes, tasks := listQEsTasks(t, ctx, proj.Name) // helper
if err := r.admit(ctx, proj, qes, tasks); err != nil {
t.Fatal(err)
}
// Both pools have capacity 1: one alert + one normal admitted.
got := refreshQE(t, ctx, alertQE)
if got.Status.State != tatarav1alpha1.QueueStateAdmitted || got.Status.TaskRef == "" {
t.Fatalf("alert not admitted: %+v", got.Status)
}
gotN := refreshQE(t, ctx, normalQE)
if gotN.Status.State != tatarav1alpha1.QueueStateAdmitted {
t.Fatalf("normal not admitted: %+v", gotN.Status)
}
}
Add the small test helpers (mustCreate, mustStatusUpdate, listQEsTasks, refreshQE) to export_test.go if not present, following the existing envtest helper style in suite_test.go.
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestAdmit -v Expected: FAIL (undefined admit).
- Step 3: Write minimal implementation
// internal/controller/queue_controller.go (add)
import "sort" // add to the import block
// admit drains the alert pool to AlertCapacity, then the normal pool to
// QueueCapacity, each in strict ascending seq order (pure global FIFO within a
// pool; head-of-line blocking accepted).
func (r *DispatcherReconciler) admit(ctx context.Context, proj *tatarav1alpha1.Project,
qes []tatarav1alpha1.QueuedEvent, tasks []tatarav1alpha1.Task) error {
admitPool := func(class string, cap int) error {
inflight := r.poolInflight(qes, tasks, class)
queued := make([]*tatarav1alpha1.QueuedEvent, 0)
for i := range qes {
if qes[i].Spec.Class == class && qes[i].Status.State == tatarav1alpha1.QueueStateQueued {
queued = append(queued, &qes[i])
}
}
sort.Slice(queued, func(i, j int) bool { return queued[i].Spec.Seq < queued[j].Spec.Seq })
for _, q := range queued {
if inflight >= cap {
break
}
task, err := buildTaskFromQueuedEvent(q, proj, r.Scheme)
if err != nil {
return err
}
if err := r.Create(ctx, task); err != nil {
// Leave Queued; requeue. Slot not consumed (inflight derives from Admitted).
return err
}
q.Status.State = tatarav1alpha1.QueueStateAdmitted
q.Status.TaskRef = task.Name
now := metav1.Now()
q.Status.AdmittedAt = &now
if err := r.Status().Update(ctx, q); err != nil {
return err
}
inflight++
if r.Metrics != nil {
r.Metrics.QueueAdmitted(class, q.Spec.Kind)
}
log.FromContext(ctx).Info("queue: admitted",
"action", "queue_admit", "resource_id", q.Name, "task", task.Name,
"class", class, "seq", q.Spec.Seq, "kind", q.Spec.Kind)
}
return nil
}
if err := admitPool(tatarav1alpha1.QueueClassAlert, proj.AlertCapacity()); err != nil {
return err
}
return admitPool(tatarav1alpha1.QueueClassNormal, proj.QueueCapacity())
}
Add metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" to the imports. r.Metrics.QueueAdmitted is added in Task 12; guard with the r.Metrics != nil check shown so this task compiles and tests pass before metrics exist (the method is referenced only behind the nil-guard — define a no-op stub now in obs if the compiler requires the symbol; see note).
Compile note: Go needs the symbol to exist. In this task add a minimal func (m *OperatorMetrics) QueueAdmitted(class, kind string) {} stub to internal/obs/operator_metrics.go (real counter wired in Task 12).
- Step 4: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestAdmit -v Expected: PASS.
- Step 5: Commit
git add internal/controller/queue_controller.go internal/controller/queue_controller_test.go internal/controller/export_test.go internal/obs/operator_metrics.go
git commit -m "feat: dispatcher admission (alert pool first, normal pool, seq FIFO, capacity)"
Task 7: Dispatcher - migration count for pre-queue Tasks¶
Files: - Modify: internal/controller/queue_controller.go (extend poolInflight) - Modify: internal/controller/queue_controller_test.go
Interfaces: - Consumes/Produces: poolInflight gains migration counting; signature unchanged.
- Step 1: Write the failing test
// internal/controller/queue_controller_test.go (add)
func TestPoolInflight_CountsUnlabelledPreQueueTasks(t *testing.T) {
r := &DispatcherReconciler{}
var qes []tatarav1alpha1.QueuedEvent
tasks := []tatarav1alpha1.Task{
preQueueTask("old-normal", "Running", "review", ""), // no queued-event label -> normal pool
preQueueTask("old-incident", "Running", "incident", ""), // -> alert pool
preQueueTask("old-done", "Succeeded", "review", ""), // terminal -> not counted
}
if got := r.poolInflight(qes, tasks, tatarav1alpha1.QueueClassNormal); got != 1 {
t.Fatalf("normal pre-queue inflight = %d, want 1", got)
}
if got := r.poolInflight(qes, tasks, tatarav1alpha1.QueueClassAlert); got != 1 {
t.Fatalf("alert pre-queue inflight = %d, want 1", got)
}
}
// preQueueTask: a Task with NO LabelQueuedEvent (created before the queue existed).
func preQueueTask(name, phase, kind, _ string) tatarav1alpha1.Task {
return tatarav1alpha1.Task{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "tatara"}, // no labels
Spec: tatarav1alpha1.TaskSpec{ProjectRef: "p", Kind: kind},
Status: tatarav1alpha1.TaskStatus{Phase: phase},
}
}
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestPoolInflight_CountsUnlabelledPreQueueTasks -v Expected: FAIL (count 0, want 1 — migration not counted yet).
- Step 3: Write minimal implementation
// internal/controller/queue_controller.go — replace poolInflight with:
func (r *DispatcherReconciler) poolInflight(qes []tatarav1alpha1.QueuedEvent, tasks []tatarav1alpha1.Task, class string) int {
n := 0
for i := range qes {
q := &qes[i]
if q.Spec.Class != class || q.Status.State != tatarav1alpha1.QueueStateAdmitted {
continue
}
t := taskByName(tasks, q.Status.TaskRef)
if t != nil && !tatarav1alpha1.TaskTerminal(t) {
n++
}
}
// Migration: non-terminal Tasks created before the queue (no queued-event
// label) count toward their pool so capacity is not over-admitted at cutover.
for i := range tasks {
t := &tasks[i]
if _, queued := t.Labels[LabelQueuedEvent]; queued {
continue
}
if tatarav1alpha1.TaskTerminal(t) {
continue
}
taskClass := tatarav1alpha1.QueueClassNormal
if t.Spec.Kind == "incident" {
taskClass = tatarav1alpha1.QueueClassAlert
}
if taskClass == class {
n++
}
}
return n
}
- Step 4: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestPoolInflight -v Expected: PASS (both pool tests).
- Step 5: Commit
git add internal/controller/queue_controller.go internal/controller/queue_controller_test.go
git commit -m "feat: dispatcher counts pre-queue Tasks toward capacity at cutover"
Task 8: Dispatcher Reconcile + SetupWithManager (Task watch)¶
Files: - Modify: internal/controller/queue_controller.go (add Reconcile, SetupWithManager) - Modify: internal/controller/queue_controller_test.go (envtest end-to-end)
Interfaces: - Consumes: reconcileDone (Task 5), admit (Task 6). - Produces: func (r *DispatcherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error), func (r *DispatcherReconciler) SetupWithManager(mgr ctrl.Manager) error.
- Step 1: Write the failing test
// internal/controller/queue_controller_test.go (add)
func TestDispatcherReconcile_AdmitsThenFreesOnTerminal(t *testing.T) {
ctx := context.Background()
ns := "tatara"
proj := &tatarav1alpha1.Project{
ObjectMeta: metav1.ObjectMeta{Name: "p-disp", Namespace: ns},
Spec: tatarav1alpha1.ProjectSpec{Queue: &tatarav1alpha1.QueueSpec{Capacity: 1, AlertCapacity: 1}},
}
mustCreate(t, ctx, proj)
mk := func(seq int64) *tatarav1alpha1.QueuedEvent {
q := &tatarav1alpha1.QueuedEvent{
ObjectMeta: metav1.ObjectMeta{GenerateName: "qe-", Namespace: ns},
Spec: tatarav1alpha1.QueuedEventSpec{Seq: seq, Class: tatarav1alpha1.QueueClassNormal, Kind: "incident", ProjectRef: proj.Name,
Payload: tatarav1alpha1.QueuedEventPayload{Kind: "incident", GenerateName: "x-"}},
}
mustCreate(t, ctx, q)
q.Status.State = tatarav1alpha1.QueueStateQueued
mustStatusUpdate(t, ctx, q)
return q
}
q1 := mk(1)
q2 := mk(2)
r := &DispatcherReconciler{Client: k8sClient, Scheme: k8sClient.Scheme()}
if _, err := r.Reconcile(ctx, reqFor(q1)); err != nil { // helper: ctrl.Request for an object
t.Fatal(err)
}
// capacity 1: q1 admitted, q2 still queued (head-of-line).
if refreshQE(t, ctx, q1).Status.State != tatarav1alpha1.QueueStateAdmitted {
t.Fatal("q1 should be admitted")
}
if refreshQE(t, ctx, q2).Status.State != tatarav1alpha1.QueueStateQueued {
t.Fatal("q2 should still be queued (capacity 1)")
}
// Drive q1's task terminal, reconcile -> q1 Done, q2 admitted.
task := taskForQE(t, ctx, refreshQE(t, ctx, q1)) // helper: get Task by LabelQueuedEvent
task.Status.Phase = "Succeeded"
mustStatusUpdate(t, ctx, task)
if _, err := r.Reconcile(ctx, reqFor(q1)); err != nil {
t.Fatal(err)
}
if refreshQE(t, ctx, q1).Status.State != tatarav1alpha1.QueueStateDone {
t.Fatal("q1 should be Done")
}
if refreshQE(t, ctx, q2).Status.State != tatarav1alpha1.QueueStateAdmitted {
t.Fatal("q2 should now be admitted")
}
}
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestDispatcherReconcile -v Expected: FAIL (undefined Reconcile).
- Step 3: Write minimal implementation
// internal/controller/queue_controller.go (add)
import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"k8s.io/apimachinery/pkg/types"
"apimachinery/pkg/apierrors" // placeholder: use k8s.io/apimachinery/pkg/api/errors as apierrors
)
func (r *DispatcherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var qe tatarav1alpha1.QueuedEvent
if err := r.Get(ctx, req.NamespacedName, &qe); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
var proj tatarav1alpha1.Project
if err := r.Get(ctx, types.NamespacedName{Namespace: qe.Namespace, Name: qe.Spec.ProjectRef}, &proj); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Project-wide pass: list this project's QueuedEvents + Tasks.
var qel tatarav1alpha1.QueuedEventList
if err := r.List(ctx, &qel, client.InNamespace(qe.Namespace)); err != nil {
return ctrl.Result{}, err
}
var tl tatarav1alpha1.TaskList
if err := r.List(ctx, &tl, client.InNamespace(qe.Namespace)); err != nil {
return ctrl.Result{}, err
}
qes := filterQEsByProject(qel.Items, proj.Name)
tasks := filterTasksByProject(tl.Items, proj.Name)
if _, err := r.reconcileDone(ctx, qes, tasks); err != nil {
return ctrl.Result{}, err
}
// Re-list after Done mutations so admission sees fresh state.
if err := r.List(ctx, &qel, client.InNamespace(qe.Namespace)); err != nil {
return ctrl.Result{}, err
}
qes = filterQEsByProject(qel.Items, proj.Name)
if err := r.admit(ctx, &proj, qes, tasks); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func filterQEsByProject(in []tatarav1alpha1.QueuedEvent, project string) []tatarav1alpha1.QueuedEvent {
out := in[:0:0]
for i := range in {
if in[i].Spec.ProjectRef == project {
out = append(out, in[i])
}
}
return out
}
func filterTasksByProject(in []tatarav1alpha1.Task, project string) []tatarav1alpha1.Task {
out := in[:0:0]
for i := range in {
if in[i].Spec.ProjectRef == project {
out = append(out, in[i])
}
}
return out
}
func (r *DispatcherReconciler) SetupWithManager(mgr ctrl.Manager) error {
mapTaskToQE := func(ctx context.Context, obj client.Object) []reconcile.Request {
qeName := obj.GetLabels()[LabelQueuedEvent]
if qeName == "" {
return nil
}
return []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: obj.GetNamespace(), Name: qeName}}}
}
return ctrl.NewControllerManagedBy(mgr).
For(&tatarav1alpha1.QueuedEvent{}).
Watches(&tatarav1alpha1.Task{}, handler.EnqueueRequestsFromMapFunc(mapTaskToQE)).
Complete(r)
}
Fix the import placeholder: use apierrors "k8s.io/apimachinery/pkg/api/errors" only if referenced; the code above uses client.IgnoreNotFound, so drop the apierrors import. Keep imports the compiler needs.
- Step 4: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestDispatcherReconcile -v Expected: PASS.
- Step 5: Commit
git add internal/controller/queue_controller.go internal/controller/queue_controller_test.go internal/controller/export_test.go
git commit -m "feat: dispatcher Reconcile (project pass) + Task-watch wiring"
Task 9: Webhook producers enqueue instead of create¶
Files: - Modify: internal/webhook/server.go (Config add Seq *queue.SeqAllocator; handleWorkItem, createLifecycleTaskAtTriage, createIncidentTask, reactivateTask enqueue; delete incidentDedup (folded into enqueue dedup); createIncidentTask builds a Payload) - Modify: internal/webhook/server_test.go (or the relevant existing webhook test files)
Interfaces: - Consumes: EnqueueEvent, LabelDedupKey (Task 4); controller package (import the controller package for EnqueueEvent, or relocate EnqueueEvent to a neutral package both can import — see note). - Produces: webhook handlers create QueuedEvents.
Note on package boundaries: EnqueueEvent/buildTaskFromQueuedEvent are in internal/controller but the webhook is internal/webhook. To avoid an import cycle, move EnqueueEvent, buildTaskFromQueuedEvent, dedupExists, and the LabelQueuedEvent/LabelDedupKey consts into a new neutral package internal/queue (alongside the allocator) in this task, updating Task ⅘/6/8 references from EnqueueEvent to queue.EnqueueEvent. Do this relocation as Step 1 here (mechanical move + import fixups), then run the full controller suite to confirm green before changing the webhook.
- Step 1: Relocate enqueue helpers to internal/queue, fix imports, verify green
Move the four symbols + two consts from internal/controller/queue_enqueue.go into internal/queue/enqueue.go (package queue). Update internal/controller callsites (dispatcher) to queue.EnqueueEvent, queue.LabelQueuedEvent, etc. buildTaskFromQueuedEvent takes *runtime.Scheme already, so it has no controller dependency.
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go build ./... && mise exec -- go test ./internal/controller/ ./internal/queue/ -v Expected: PASS (pure move, no behavior change).
- Step 2: Write the failing webhook test
// internal/webhook/server_test.go (add) - assert enqueue, not Task create.
func TestHandleGrafanaAlert_EnqueuesAlertEvent(t *testing.T) {
// Build a Server whose cfg.Client is a fake with a Grafana-enabled Project,
// cfg.Seq = queue.NewSeqAllocator(). POST a firing alert.
// Assert: exactly one QueuedEvent, Class=alert, Kind=incident, DedupKey=groupHash,
// and NO Task created directly.
// (Mirror the existing grafana handler test setup in grafana_handler_test.go.)
}
Fill the body following the existing grafana_handler_test.go setup (it already builds a Server + fake client + firing payload). Assert QueuedEventList length 1 with the fields above and TaskList length 0.
- Step 3: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/webhook/ -run TestHandleGrafanaAlert_EnqueuesAlertEvent -v Expected: FAIL (still creates a Task / no QueuedEvent).
- Step 4: Rewrite the webhook producers
Add to Config: Seq *queue.SeqAllocator. Replace createIncidentTask body:
func (s *Server) createIncidentTask(ctx context.Context, proj *tatarav1.Project, alert GrafanaAlert, groupHash string) error {
slugs := projectRepoSlugs(ctx, s.cfg.Client, s.cfg.Namespace, proj.Name)
alertCtx := renderAlertContext(alert)
goal := incident.GoalProject(alertCtx, slugs)
payload := tatarav1.QueuedEventPayload{
Kind: "incident",
Goal: goal,
GenerateName: "incident-",
Labels: map[string]string{tatarav1.LabelActivity: "incident", tatarav1.LabelAlertGroup: groupHash},
Annotations: map[string]string{tatarav1.AnnGrafanaAlert: alertCtx},
Provider: "",
PodRepo: "",
}
_, _, err := queue.EnqueueEvent(ctx, s.cfg.Client, s.cfg.Seq, proj, tatarav1.QueueClassAlert, false, groupHash, payload)
return err
}
Delete incidentDedup and its callsite in handleGrafanaAlert (the EnqueueEvent dedup on dedupKey=groupHash now covers the in-flight case). The cooldown window is intentionally dropped to keep one dedup mechanism; if a cooldown is still wanted, record the decision in MEMORY and add a Done-within-window check to dedupExists — but default for this plan: in-flight dedup only (a resolved+refiring group reopens once the prior incident is Done). Note this behavior change in the commit body.
For handleWorkItem (the issue/MR -> issueLifecycle path, lines ~404-429): replace the Task{...} + r.Create with a Payload carrying the same Labels, Annotations, RepositoryRef, Goal, Kind, Source, and a fixed Name: issueLifecycleTaskName(...) (deterministic), then queue.EnqueueEvent(..., class=normal, autonomous=false, dedupKey=issueLifecycleTaskName(...), payload). The deterministic Name + dedupKey preserve today's idempotency. Set Payload.Provider from the resolved provider and Payload.PodRepo from the repo name (matching the old agent.StampPodName args at the create site).
For createLifecycleTaskAtTriage (lines ~559-622) and reactivateTask: route through queue.EnqueueEvent the same way (normal class, dedupKey = the deterministic lifecycle name). Reactivation that targets an existing Parked Task stays as-is if it mutates an existing Task rather than creating one — only the create arms move to enqueue.
- Step 5: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/webhook/ -v Expected: PASS (the new enqueue test + existing webhook tests adjusted to assert QueuedEvent where they previously asserted Task).
- Step 6: Commit
git add internal/webhook/ internal/queue/ internal/controller/
git commit -m "feat: webhook producers enqueue QueuedEvents (incident=alert, issueLifecycle=normal); drop incidentDedup cooldown"
Task 10: Crons enqueue + delete the selection layer¶
Files: - Modify: internal/controller/projectscan.go (createScanTask/createBrainstormTask/createHealthCheckTask enqueue; runScans drops the budget; mrScan/issueScan/brainstorm/healthCheck enqueue under the K cap; delete selectCandidates, selectPerRepo, laneOccupancy, maxOpenTasks, openTaskCount) - Modify: the affected projectscan_*_test.go files - Modify: internal/controller/queue_controller.go add func queuedAutonomousCount(qes []tatarav1alpha1.QueuedEvent) int
Interfaces: - Consumes: queue.EnqueueEvent (Task 9), QueuedAutonomousCap (Task 2), ProjectReconciler gains Alloc *queue.SeqAllocator. - Produces: crons emit QueuedEvents; selection layer removed.
- Step 1: Write the failing test
// internal/controller/projectscan_run_test.go (add or adapt)
func TestRunScans_EnqueuesUnderAutonomousCap(t *testing.T) {
// Seed a Project with Queue.QueuedAutonomousCap=2 and >2 eligible issues across repos.
// Stub the SCM reader to return several open issues.
// Call r.runScans(ctx, proj).
// Assert: exactly 2 QueuedEvents created with Autonomous=true; NO Tasks created
// directly by the scan (Tasks appear only after the dispatcher admits).
}
Fill using the existing projectscan_run_test.go reader-stub + seeding helpers. Assert QueuedEventList count == 2 and all Spec.Autonomous == true, TaskList count == 0.
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestRunScans_EnqueuesUnderAutonomousCap -v Expected: FAIL (still creates Tasks / no cap on QueuedEvents).
- Step 3: Implement
Add Alloc *queue.SeqAllocator to ProjectReconciler (struct + wire in Task 12). Add:
// internal/controller/queue_controller.go
func queuedAutonomousCount(qes []tatarav1alpha1.QueuedEvent) int {
n := 0
for i := range qes {
if qes[i].Spec.Autonomous && qes[i].Status.State == tatarav1alpha1.QueueStateQueued {
n++
}
}
return n
}
Rewrite the three create-helpers to enqueue. Example for createScanTask (replace the Task{...}+ownerref+r.Create+StampPodName block, lines ~416-441) with a Payload build + enqueue. The dedupKey for a scan candidate is kind + "\x00" + src.IssueRef:
payload := tatarav1alpha1.QueuedEventPayload{
Kind: kind,
RepositoryRef: repo.Name,
Goal: goal,
Source: src,
Labels: scanTaskLabels(labelCand, activity, kind),
GenerateName: "scan-",
Provider: provider,
PodRepo: repo.Name,
}
dedupKey := kind + "\x00" + src.IssueRef
_, created, err := queue.EnqueueEvent(ctx, r.Client, r.Alloc, proj, tatarav1alpha1.QueueClassNormal, true, dedupKey, payload)
if err != nil {
return nil, fmt.Errorf("scan: enqueue: %w", err)
}
if created {
r.Metrics.ScanTaskCreated(activity, kind) // keep the existing metric name; it now counts enqueues
}
return nil, nil // callers no longer use the returned *Task
Adjust createScanTask's signature/return if callers consumed the *Task (they used it only for logging/budget — remove that usage). Apply the same transform to createBrainstormTask (class=normal, autonomous=true, dedupKey="brainstorm-"+proj.Name+cycle marker; Labels {labelActivity:"brainstorm"}, Annotations {AnnBrainstormSources:...}, GenerateName "brainstorm-", Kind "brainstorm") and createHealthCheckTask (same, activity "healthCheck", GenerateName "healthcheck-").
In runScans (lines ~1532-1636): delete budget := maxOpenTasks(proj) - openTaskCount(existing) and the &budget threading. Replace with a K-gate the scans honor:
var qel tatarav1alpha1.QueuedEventList
if err := r.List(ctx, &qel, client.InNamespace(proj.Namespace)); err != nil {
return 0, err
}
qes := filterQEsByProject(qel.Items, proj.Name)
remaining := proj.QueuedAutonomousCap() - queuedAutonomousCount(qes)
Thread &remaining where &budget was threaded; each scan enqueues while remaining > 0, decrementing on created. The webhook/alert paths never consult remaining.
In mrScan/issueScan: replace selectPerRepo(eligible, priorityLabel, maxPerRepo, occ) with a flat eligible list (all candidates), enqueue each via createScanTask while *remaining > 0 and the candidate is not already deduped (the enqueue dedup handles the per-work-item skip; the old laneOccupancy per-repo cap is gone). Delete selectCandidates, selectPerRepo, laneOccupancy, maxOpenTasks, openTaskCount, taskOpen, the candidate.updatedAt sort usage, and priorityLabel plumbing. (Keep candidate struct + candidatesFromPRs/candidatesFromIssues — still used to shape eligible items.)
- Step 4: Run tests
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -v Expected: PASS. Delete or rewrite the now-obsolete projectscan_select_test.go, projectscan_backlog_test.go (budget), and lane tests; assert the new enqueue behavior instead. Ensure no references to deleted functions remain (mise exec -- go build ./...).
- Step 5: Commit
git add internal/controller/
git commit -m "refactor: crons enqueue under autonomous cap; delete per-repo selection + maxOpenTasks budget"
Task 11: Delete atConcurrencyCap (execution gate superseded by admission)¶
Files: - Modify: internal/controller/task_controller.go (delete atConcurrencyCap func + callsite ~176; delete taskActive if it has no other callers, else keep) - Modify: internal/controller/lifecycle.go (delete callsite ~533) - Modify: affected tests (task_controller_test.go, any concurrency-cap test)
Interfaces: - Removes: atConcurrencyCap. No replacement (admission is the only gate now).
- Step 1: Write/adjust the failing test
// internal/controller/task_controller_test.go (add)
func TestReconcile_NoConcurrencyGate(t *testing.T) {
// Seed a Project with MaxConcurrentTasks=1 and 2 active (Running) Tasks both
// carrying the LabelQueuedEvent (admitted). Reconcile the second Task.
// Assert it is NOT requeued/blocked by a concurrency cap (it proceeds to its
// normal phase handling). Previously atConcurrencyCap would have gated it.
}
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/controller/ -run TestReconcile_NoConcurrencyGate -v Expected: FAIL (cap still gates).
-
Step 3: Implement (delete the gate)
-
Remove
func (r *TaskReconciler) atConcurrencyCap(...)(task_controller.go:329). - Remove its callsite in
Reconcile(task_controller.go:176) and inreconcileLifecycle(lifecycle.go:533), including the now-deadself/cap branches and thetasksInflight-from-cap bookkeeping if it lived there. - If
taskActive(task_controller.go:101) andisActivelose all callers, delete them too (boy-scout). Keep any still referenced by the inflight gauge. -
Delete the old concurrency-cap test(s) (
TestTaskActive_ExcludesTerminalLifecycle, the cap-deadlock regression test) or repurpose them to assert the gate is gone. -
Step 4: Run tests + build
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go build ./... && mise exec -- go test ./internal/controller/ -v Expected: PASS; no references to atConcurrencyCap.
- Step 5: Commit
git add internal/controller/
git commit -m "refactor: remove atConcurrencyCap; admission queue is the only concurrency gate"
Task 12: Wire manager + metrics + manifests + full suite¶
Files: - Modify: cmd/manager/wire.go (construct SeqAllocator; register SeqRecoverer via mgr.Add; construct + SetupWithManager the DispatcherReconciler; inject Alloc into ProjectReconciler; set webhook Config.Seq) - Modify: internal/obs/operator_metrics.go (real queue metrics) - Modify: CRD manifests via make manifests (config/crd/... and charts/tatara-operator/crds/...)
Interfaces: - Consumes: everything above. - Produces: func (m *OperatorMetrics) QueueAdmitted(class, kind string), func (m *OperatorMetrics) SetQueueDepth(class string, n int), func (m *OperatorMetrics) SetQueueInflight(class string, n int).
- Step 1: Write the failing test
// internal/obs/operator_metrics_test.go (add)
func TestQueueMetrics_RegisterAndObserve(t *testing.T) {
reg := prometheus.NewRegistry()
m := NewOperatorMetrics(reg)
m.QueueAdmitted("alert", "incident")
m.SetQueueDepth("normal", 3)
m.SetQueueInflight("alert", 1)
// gather + assert the three series exist with expected values
mfs, err := reg.Gather()
if err != nil {
t.Fatal(err)
}
if !hasMetric(mfs, "operator_queue_admitted_total") ||
!hasMetric(mfs, "operator_queue_depth") ||
!hasMetric(mfs, "operator_queue_inflight") {
t.Fatal("queue metrics not registered")
}
}
hasMetric is a tiny helper over []*dto.MetricFamily (add if absent).
- Step 2: Run test to verify it fails
Run: cd /Users/szymonri/Documents/tatara/tatara-operator && mise exec -- go test ./internal/obs/ -run TestQueueMetrics -v Expected: FAIL (methods undefined / not registered).
- Step 3: Implement metrics + wiring
In internal/obs/operator_metrics.go replace the Task-6 no-op QueueAdmitted stub with real instruments registered in NewOperatorMetrics:
// fields on OperatorMetrics
queueAdmittedTotal *prometheus.CounterVec // labels: class, kind
queueDepth *prometheus.GaugeVec // label: class
queueInflight *prometheus.GaugeVec // label: class
// in NewOperatorMetrics, construct with prometheus.NewCounterVec/NewGaugeVec and reg.MustRegister(...)
func (m *OperatorMetrics) QueueAdmitted(class, kind string) { m.queueAdmittedTotal.WithLabelValues(class, kind).Inc() }
func (m *OperatorMetrics) SetQueueDepth(class string, n int) { m.queueDepth.WithLabelValues(class).Set(float64(n)) }
func (m *OperatorMetrics) SetQueueInflight(class string, n int) { m.queueInflight.WithLabelValues(class).Set(float64(n)) }
In the dispatcher Reconcile, after the admission pass, set the gauges from the current snapshot (depth = Queued per class, inflight = poolInflight per class) so they recompute from cluster state (no deltas), mirroring the lifecycle-gauge recompute precedent.
In cmd/manager/wire.go:
seqAlloc := queue.NewSeqAllocator()
if err := mgr.Add(&queue.SeqRecoverer{Client: mgr.GetClient(), Alloc: seqAlloc, Namespace: cfg.Namespace}); err != nil {
return err
}
// ProjectReconciler: add Alloc: seqAlloc to the struct literal (cmd/manager/wire.go:151-169)
// webhook Config: set Seq: seqAlloc (where the webhook.Config is built, ~cmd/manager/wire.go:88)
if err := (&controller.DispatcherReconciler{
Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Alloc: seqAlloc, Metrics: metrics,
}).SetupWithManager(mgr); err != nil {
return err
}
- Step 4: Regenerate manifests + full suite
Run:
cd /Users/szymonri/Documents/tatara/tatara-operator
mise exec -- make manifests generate
mise exec -- go build ./...
mise exec -- golangci-lint run
mise exec -- go test ./...
QueuedEvent + the queue field on Project appear under config/crd/bases/ and charts/tatara-operator/crds/; build clean; lint clean; ALL tests PASS. - Step 5: Commit
git add cmd/manager/wire.go internal/obs/ config/crd/ charts/tatara-operator/crds/ api/v1alpha1/
git commit -m "feat: wire seq allocator + dispatcher + queue metrics; regenerate CRDs"
Self-Review¶
Spec coverage: - Unit A (QueuedEvent CRD) -> Task 1. Unit B (seq) -> Task 3. Unit C (producers) -> Tasks 9, 10. Unit D (dispatcher) -> Tasks 5-8. Unit E (migration) -> Task 7. Unit F (QueueSpec config + supersession) -> Tasks 2, 10, 11. - Strict global FIFO + alert-pool-first -> Task 6. Reserved alert capacity -> Tasks 2, 6. HOL blocking accepted -> demonstrated in Task 8 test. Queued-depth cap K -> Task 10. Dedup/cooldown move to enqueue -> Tasks 4, 9 (cooldown dropped to in-flight-dedup; flagged for MEMORY). Deletions (atConcurrencyCap/maxOpenTasks/lane selection) -> Tasks 10, 11. Metrics -> Task 12. CRD-apply runbook -> Task 12 + Deploy section.
Placeholder scan: One deliberate plan-time decision remains (incident cooldown vs in-flight-only dedup) - resolved to in-flight-only with a MEMORY note; not a placeholder. Test bodies for Tasks 9 and 10 reference "fill using existing helper setup" - acceptable because they reuse concrete existing test scaffolding (grafana_handler_test.go, projectscan_run_test.go); the assertions are fully specified.
Type consistency: EnqueueEvent/buildTaskFromQueuedEvent/dedupExists relocate to package queue in Task 9; all later references use queue. prefix. LabelQueuedEvent/LabelDedupKey move with them. QueueClass*/QueueState* consts consistent across tasks. poolInflight signature stable across Tasks 5/7. r.Metrics.QueueAdmitted stubbed in Task 6, made real in Task 12 (compiles throughout).
Deploy (post-implementation)¶
- tatara-operator: merge to
main-> CI builds operator image.kubectl applythe regenerated CRDs (QueuedEvent+ Projectqueuefield) - Helm skips CRD upgrades. - tatara-helmfile: bump operator chart version + pinned
image.tag; optionally set per-projectspec.queue {capacity, alertCapacity, queuedAutonomousCap}(defaults match today's throughput). Diff -> apply. - Cutover: pre-queue in-flight Tasks drain under the Task 7 migration rule; new work flows through the queue. Enabling the (already-inert) Grafana incident webhook lights up the reserved alert lane.