Repository navigation
Let Slack TaskSpawners create TaskPipelines - #1854
knechtionscoding wants to merge 2 commits into
Conversation
A stage can now declare conditions that gate it on earlier stages' results. Each condition renders a Go template input with .Stages, trims whitespace, and compares it against a value list with In or NotIn. A stage whose conditions are not met is Skipped: it creates no Tasks and counts as complete for the stages after it and for pipeline success. This lets one pipeline pick a branch at runtime, for example a routing stage whose agent answer selects which of several stages runs, with a NotIn stage as the fallback. Pipeline templates also gain b64dec, so prompts and conditions can read the base64-encoded response result directly.
A TaskSpawner can now set spec.pipelineTemplate instead of spec.taskTemplate. For each matching Slack message it creates a TaskPipeline with the template's stages, so a pipeline with conditional stages can act as a front door that routes each request. TaskPipeline gains spec.params, an immutable string map exposed to stage prompt, branch, and condition templates as .Params. The spawner fills it from the work item's string template variables. The created TaskPipeline is named and deduplicated the way the Task would have been, carries the spawner label, owner reference, and Slack reporting annotations, and counts toward status.activeTasks and maxConcurrency. taskTemplate becomes optional, with CEL requiring exactly one of the two templates, so existing manifests are unaffected. pipelineTemplate is limited to the slack source and rejects spec.credentials; manual runs return an error for such spawners. A v1alpha1 round-trip keeps it in a preservation annotation.
There was a problem hiding this comment.
12 issues found across 24 files
You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/slack/pipeline_test.go">
<violation number="1" location="internal/slack/pipeline_test.go:88">
P3: This Slack-to-TaskPipeline integration test leaves `ID`, `Title`, and `Kind` unverified even though they are part of the message parameter contract. Assert those fields here so regressions in the creation path are caught.</violation>
</file>
<file name="internal/taskbuilder/pipeline_test.go">
<violation number="1" location="internal/taskbuilder/pipeline_test.go:64">
P2: This stage assertion does not verify the copied `TaskTemplate`, so a regression that loses a prompt or `WorkerPoolRef` still passes. Assert representative prompt and worker-pool values for both stages before mutating the condition slice.</violation>
<violation number="2" location="internal/taskbuilder/pipeline_test.go:74">
P2: This assertion can pass when `BuildTaskPipeline` omits the controller owner reference because `ObjectBelongsToSpawner` falls back to the label. Assert the owner reference’s UID, name, kind, API version, and controller bit directly so ownership regressions fail the test.
(Based on your team's feedback about adding tests for new behavior.)</violation>
</file>
<file name="internal/controller/taskpipeline_conditions_test.go">
<violation number="1" location="internal/controller/taskpipeline_conditions_test.go:93">
P2: Assert that `StageStatuses` contains every expected stage before checking phases; otherwise this test can pass without verifying that skipped stages were recorded at all.</violation>
<violation number="2" location="internal/controller/taskpipeline_conditions_test.go:319">
P2: Scope this helper to the pipeline as well as the stage; otherwise tests with multiple pipelines can update or inspect an unrelated Task and produce misleading results. Include the `taskPipelineLabel` selector using `pipelineLabelValue(pipeline.Name)`.</violation>
</file>
<file name="internal/slack/handler.go">
<violation number="1" location="internal/slack/handler.go:441">
P1: This records Slack reporting metadata on the `TaskPipeline`, but the Slack reporter lists only `Task` objects and pipeline-generated Tasks do not inherit this metadata. Pipeline-template requests therefore create work without sending Slack status or completion replies; propagate the reporting metadata to child Tasks or make the reporter handle TaskPipelines.</violation>
</file>
<file name="internal/conversion/taskspawner.go">
<violation number="1" location="internal/conversion/taskspawner.go:143">
P2: Pipeline spawners are not writable through v1alpha1: conversion returns empty required legacy fields, which the v1alpha1 schema rejects on write-back. Provide a schema-valid fallback that is removed on restoration, or explicitly reject this conversion path.</violation>
<violation number="2" location="internal/conversion/taskspawner.go:164">
P2: This stores the entire pipeline template in one annotation, so valid large v1alpha2 templates can exceed Kubernetes' 256 KiB annotation limit and fail v1alpha1 conversion. Bound the template to the annotation budget or use a preservation mechanism that is not limited by annotation size.</violation>
<violation number="3" location="internal/conversion/taskspawner.go:177">
P2: This restore path accepts semantically invalid pipeline templates. Validate the complete `TaskSpawnerPipelineTemplate` before assigning it, or ignore the annotation when any stage violates the v1alpha2 constraints.</violation>
<violation number="4" location="internal/conversion/taskspawner.go:187">
P2: A v1alpha1 edit to the placeholder task template leaves both templates after restoration, so the v1alpha2 exactly-one validation rejects the write-back. Clear the legacy task template unconditionally when restoring a preserved pipeline template.</violation>
</file>
<file name="examples/07-task-pipeline/README.md">
<violation number="1" location="examples/07-task-pipeline/README.md:23">
P3: The README now documents three pipeline examples but still says “Delete both examples.” Change that wording to “Delete all examples” so the cleanup instructions match the directory contents.</violation>
</file>
<file name="examples/07-task-pipeline/conditional-pipeline.yaml">
<violation number="1" location="examples/07-task-pipeline/conditional-pipeline.yaml:18">
P3: The classifier asks for the default branch, but this example's Workspace is pinned to `main`; repositories using another default branch will classify and review the wrong commit. Refer to the checked-out branch here, or document that users must change `workspace.yaml.spec.ref` to their default branch.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| spawnerRef *taskbuilder.SpawnerRef, | ||
| ) error { | ||
| pipeline := h.taskBuilder.BuildTaskPipeline(name, spawner.Namespace, spawner.Spec.PipelineTemplate, templateVars, spawnerRef) | ||
| setSlackReporting(pipeline, msg) |
There was a problem hiding this comment.
P1: This records Slack reporting metadata on the TaskPipeline, but the Slack reporter lists only Task objects and pipeline-generated Tasks do not inherit this metadata. Pipeline-template requests therefore create work without sending Slack status or completion replies; propagate the reporting metadata to child Tasks or make the reporter handle TaskPipelines.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/slack/handler.go, line 441:
<comment>This records Slack reporting metadata on the `TaskPipeline`, but the Slack reporter lists only `Task` objects and pipeline-generated Tasks do not inherit this metadata. Pipeline-template requests therefore create work without sending Slack status or completion replies; propagate the reporting metadata to child Tasks or make the reporter handle TaskPipelines.</comment>
<file context>
@@ -441,6 +425,70 @@ func (h *SlackHandler) createTask(ctx context.Context, spawner *kelos.TaskSpawne
+ spawnerRef *taskbuilder.SpawnerRef,
+) error {
+ pipeline := h.taskBuilder.BuildTaskPipeline(name, spawner.Namespace, spawner.Spec.PipelineTemplate, templateVars, spawnerRef)
+ setSlackReporting(pipeline, msg)
+
+ if err := h.client.Create(ctx, pipeline); err != nil {
</file context>
| if len(pipeline.Spec.Stages) != 2 || pipeline.Spec.Stages[1].Conditions[0].Values[0] != "docs" { | ||
| t.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages) | ||
| } |
There was a problem hiding this comment.
P2: This stage assertion does not verify the copied TaskTemplate, so a regression that loses a prompt or WorkerPoolRef still passes. Assert representative prompt and worker-pool values for both stages before mutating the condition slice.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/taskbuilder/pipeline_test.go, line 64:
<comment>This stage assertion does not verify the copied `TaskTemplate`, so a regression that loses a prompt or `WorkerPoolRef` still passes. Assert representative prompt and worker-pool values for both stages before mutating the condition slice.</comment>
<file context>
@@ -0,0 +1,104 @@
+ t.Errorf("params[%q] = %q, want %q", key, pipeline.Spec.Params[key], value)
+ }
+ }
+ if len(pipeline.Spec.Stages) != 2 || pipeline.Spec.Stages[1].Conditions[0].Values[0] != "docs" {
+ t.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages)
+ }
</file context>
| if len(pipeline.Spec.Stages) != 2 || pipeline.Spec.Stages[1].Conditions[0].Values[0] != "docs" { | |
| t.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages) | |
| } | |
| +\tif len(pipeline.Spec.Stages) != 2 { | |
| +\t\tt.Fatalf("stages = %#v, want the template stages", pipeline.Spec.Stages) | |
| +\t} | |
| +\tif got := pipeline.Spec.Stages[0].TaskTemplate.Prompt; got != "Route {{.Params.Body}}" { | |
| +\t\tt.Errorf("stage 0 prompt = %q, want %q", got, "Route {{.Params.Body}}") | |
| +\t} | |
| +\tif ref := pipeline.Spec.Stages[0].TaskTemplate.WorkerPoolRef; ref == nil || ref.Name != "router" { | |
| +\t\tt.Errorf("stage 0 worker pool = %#v, want router", ref) | |
| +\t} | |
| +\tif got := pipeline.Spec.Stages[1].TaskTemplate.Prompt; got != "{{.Params.Body}}" { | |
| +\t\tt.Errorf("stage 1 prompt = %q, want %q", got, "{{.Params.Body}}") | |
| +\t} | |
| +\tif ref := pipeline.Spec.Stages[1].TaskTemplate.WorkerPoolRef; ref == nil || ref.Name != "docs" { | |
| +\t\tt.Errorf("stage 1 worker pool = %#v, want docs", ref) | |
| +\t} | |
| +\tif got := pipeline.Spec.Stages[1].Conditions[0].Values[0]; got != "docs" { | |
| +\t\tt.Fatalf("stage 1 condition value = %q, want docs", got) | |
| +\t} |
| if pipeline.Labels[SpawnerLabel] != "chat" { | ||
| t.Errorf("spawner label = %q, want chat", pipeline.Labels[SpawnerLabel]) | ||
| } | ||
| if !ObjectBelongsToSpawner(pipeline, "chat", "spawner-uid") { |
There was a problem hiding this comment.
P2: This assertion can pass when BuildTaskPipeline omits the controller owner reference because ObjectBelongsToSpawner falls back to the label. Assert the owner reference’s UID, name, kind, API version, and controller bit directly so ownership regressions fail the test.
(Based on your team's feedback about adding tests for new behavior.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/taskbuilder/pipeline_test.go, line 74:
<comment>This assertion can pass when `BuildTaskPipeline` omits the controller owner reference because `ObjectBelongsToSpawner` falls back to the label. Assert the owner reference’s UID, name, kind, API version, and controller bit directly so ownership regressions fail the test.
(Based on your team's feedback about adding tests for new behavior.) </comment>
<file context>
@@ -0,0 +1,104 @@
+ if pipeline.Labels[SpawnerLabel] != "chat" {
+ t.Errorf("spawner label = %q, want chat", pipeline.Labels[SpawnerLabel])
+ }
+ if !ObjectBelongsToSpawner(pipeline, "chat", "spawner-uid") {
+ t.Error("built TaskPipeline does not belong to its spawner")
+ }
</file context>
| "incident": kelos.PipelineStagePhaseSkipped, | ||
| "fallback": kelos.PipelineStagePhaseSkipped, | ||
| } | ||
| for _, status := range updated.Status.StageStatuses { |
There was a problem hiding this comment.
P2: Assert that StageStatuses contains every expected stage before checking phases; otherwise this test can pass without verifying that skipped stages were recorded at all.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/controller/taskpipeline_conditions_test.go, line 93:
<comment>Assert that `StageStatuses` contains every expected stage before checking phases; otherwise this test can pass without verifying that skipped stages were recorded at all.</comment>
<file context>
@@ -0,0 +1,357 @@
+ "incident": kelos.PipelineStagePhaseSkipped,
+ "fallback": kelos.PipelineStagePhaseSkipped,
+ }
+ for _, status := range updated.Status.StageStatuses {
+ if status.Phase != wantPhases[status.Name] {
+ t.Errorf("stage %q phase = %q, want %q", status.Name, status.Phase, wantPhases[status.Name])
</file context>
| var tasks kelos.TaskList | ||
| if err := k8sClient.List(context.Background(), &tasks, | ||
| client.InNamespace(pipeline.Namespace), | ||
| client.MatchingLabels{taskPipelineStageLabel: stage}, |
There was a problem hiding this comment.
P2: Scope this helper to the pipeline as well as the stage; otherwise tests with multiple pipelines can update or inspect an unrelated Task and produce misleading results. Include the taskPipelineLabel selector using pipelineLabelValue(pipeline.Name).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/controller/taskpipeline_conditions_test.go, line 319:
<comment>Scope this helper to the pipeline as well as the stage; otherwise tests with multiple pipelines can update or inspect an unrelated Task and produce misleading results. Include the `taskPipelineLabel` selector using `pipelineLabelValue(pipeline.Name)`.</comment>
<file context>
@@ -0,0 +1,357 @@
+ var tasks kelos.TaskList
+ if err := k8sClient.List(context.Background(), &tasks,
+ client.InNamespace(pipeline.Namespace),
+ client.MatchingLabels{taskPipelineStageLabel: stage},
+ ); err != nil {
+ t.Fatal(err)
</file context>
| client.MatchingLabels{taskPipelineStageLabel: stage}, | |
| client.MatchingLabels{taskPipelineLabel: pipelineLabelValue(pipeline.Name), taskPipelineStageLabel: stage}, |
| if emptyTaskTemplate(dst.TaskTemplate) { | ||
| dst.TaskTemplate = v1alpha2.TaskTemplate{} | ||
| } |
There was a problem hiding this comment.
P2: A v1alpha1 edit to the placeholder task template leaves both templates after restoration, so the v1alpha2 exactly-one validation rejects the write-back. Clear the legacy task template unconditionally when restoring a preserved pipeline template.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/conversion/taskspawner.go, line 187:
<comment>A v1alpha1 edit to the placeholder task template leaves both templates after restoration, so the v1alpha2 exactly-one validation rejects the write-back. Clear the legacy task template unconditionally when restoring a preserved pipeline template.</comment>
<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
+ // v1alpha1 always serializes taskTemplate, including its non-pointer
+ // credentials, so a pipeline spawner comes back with an empty template.
+ // Drop it so the hub object sets exactly one of the two templates.
+ if emptyTaskTemplate(dst.TaskTemplate) {
+ dst.TaskTemplate = v1alpha2.TaskTemplate{}
+ }
</file context>
| if emptyTaskTemplate(dst.TaskTemplate) { | |
| dst.TaskTemplate = v1alpha2.TaskTemplate{} | |
| } | |
| dst.TaskTemplate = v1alpha2.TaskTemplate{} |
| if dst.Annotations == nil { | ||
| dst.Annotations = map[string]string{} | ||
| } | ||
| dst.Annotations[preservedPipelineTemplateAnnotation] = string(data) |
There was a problem hiding this comment.
P2: This stores the entire pipeline template in one annotation, so valid large v1alpha2 templates can exceed Kubernetes' 256 KiB annotation limit and fail v1alpha1 conversion. Bound the template to the annotation budget or use a preservation mechanism that is not limited by annotation size.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/conversion/taskspawner.go, line 164:
<comment>This stores the entire pipeline template in one annotation, so valid large v1alpha2 templates can exceed Kubernetes' 256 KiB annotation limit and fail v1alpha1 conversion. Bound the template to the annotation budget or use a preservation mechanism that is not limited by annotation size.</comment>
<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
+ if dst.Annotations == nil {
+ dst.Annotations = map[string]string{}
+ }
+ dst.Annotations[preservedPipelineTemplateAnnotation] = string(data)
+ return nil
+}
</file context>
| if got := pipeline.Spec.Params["URL"]; got != msg.Permalink { | ||
| t.Errorf("params.URL = %q, want %q", got, msg.Permalink) | ||
| } |
There was a problem hiding this comment.
P3: This Slack-to-TaskPipeline integration test leaves ID, Title, and Kind unverified even though they are part of the message parameter contract. Assert those fields here so regressions in the creation path are caught.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/slack/pipeline_test.go, line 88:
<comment>This Slack-to-TaskPipeline integration test leaves `ID`, `Title`, and `Kind` unverified even though they are part of the message parameter contract. Assert those fields here so regressions in the creation path are caught.</comment>
<file context>
@@ -0,0 +1,154 @@
+ if got := pipeline.Spec.Params["Body"]; got != msg.Body {
+ t.Errorf("params.Body = %q, want %q", got, msg.Body)
+ }
+ if got := pipeline.Spec.Params["URL"]; got != msg.Permalink {
+ t.Errorf("params.URL = %q, want %q", got, msg.Permalink)
+ }
</file context>
| if got := pipeline.Spec.Params["URL"]; got != msg.Permalink { | |
| t.Errorf("params.URL = %q, want %q", got, msg.Permalink) | |
| } | |
| if got := pipeline.Spec.Params["URL"]; got != msg.Permalink { | |
| t.Errorf("params.URL = %q, want %q", got, msg.Permalink) | |
| } | |
| if got := pipeline.Spec.Params["ID"]; got != msg.Timestamp { | |
| t.Errorf("params.ID = %q, want %q", got, msg.Timestamp) | |
| } | |
| if got := pipeline.Spec.Params["Title"]; got != msg.Text { | |
| t.Errorf("params.Title = %q, want %q", got, msg.Text) | |
| } | |
| if got := pipeline.Spec.Params["Kind"]; got != "SlackMessage" { | |
| t.Errorf("params.Kind = %q, want %q", got, "SlackMessage") | |
| } |
| | `workspace.yaml` | Workspace | Git repository to clone | | ||
| | `pipeline.yaml` | TaskPipeline | Three-stage feature development workflow | | ||
| | `matrix-pipeline.yaml` | TaskPipeline | Matrix fan-out followed by a summary stage | | ||
| | `conditional-pipeline.yaml` | TaskPipeline | Classification stage that selects one of two review stages | |
There was a problem hiding this comment.
P3: The README now documents three pipeline examples but still says “Delete both examples.” Change that wording to “Delete all examples” so the cleanup instructions match the directory contents.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At examples/07-task-pipeline/README.md, line 23:
<comment>The README now documents three pipeline examples but still says “Delete both examples.” Change that wording to “Delete all examples” so the cleanup instructions match the directory contents.</comment>
<file context>
@@ -19,6 +20,7 @@ The pipeline status summarizes progress with bounded per-stage counts.
| `workspace.yaml` | Workspace | Git repository to clone |
| `pipeline.yaml` | TaskPipeline | Three-stage feature development workflow |
| `matrix-pipeline.yaml` | TaskPipeline | Matrix fan-out followed by a summary stage |
+| `conditional-pipeline.yaml` | TaskPipeline | Classification stage that selects one of two review stages |
## Sequential Pipeline
</file context>
| workspaceRef: | ||
| name: my-workspace | ||
| prompt: | | ||
| Read the most recent commit on the default branch. Do not modify the repository. |
There was a problem hiding this comment.
P3: The classifier asks for the default branch, but this example's Workspace is pinned to main; repositories using another default branch will classify and review the wrong commit. Refer to the checked-out branch here, or document that users must change workspace.yaml.spec.ref to their default branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At examples/07-task-pipeline/conditional-pipeline.yaml, line 18:
<comment>The classifier asks for the default branch, but this example's Workspace is pinned to `main`; repositories using another default branch will classify and review the wrong commit. Refer to the checked-out branch here, or document that users must change `workspace.yaml.spec.ref` to their default branch.</comment>
<file context>
@@ -0,0 +1,57 @@
+ workspaceRef:
+ name: my-workspace
+ prompt: |
+ Read the most recent commit on the default branch. Do not modify the repository.
+ Reply with exactly one word and no other text:
+ - docs if the commit only changes documentation
</file context>
What type of PR is this?
/kind api
What this PR does / why we need it:
Stacked on #1853. Only the top commit is new; review that one. It lets a
Slack TaskSpawner start a TaskPipeline. Combined with #1853's conditional
stages, one Slack front door can route each request to the right agent without
a new CRD, as discussed on #1761.
TaskSpawner.spec.pipelineTemplate.stagescreates a TaskPipeline for eachmatching message instead of a Task.
reporting annotations.
status.activeTasksandmaxConcurrency. TheTaskSpawner controller watches TaskPipelines so the count updates.
TaskPipeline.spec.paramsis an immutablemap[string]string, exposedas
.Paramsto stage prompt, branch, and condition templates. The spawnerfills it from the work item's string template variables (for Slack:
ID,Title,Body,URL,Kind). This sidesteps a template clash: thespawner can't fill in request fields at creation time while leaving
{{.Stages...}}for the pipeline to fill in later.taskTemplatebecomes optional. CEL requires exactly one oftaskTemplateorpipelineTemplate, so every existing manifest stillapplies unchanged.
This is the second of three stacked PRs:
pipelineTemplate.Which issue(s) this PR is related to:
Part of #1760. Depends on #1853.
Special notes for your reviewer:
pipelineTemplateis rejected with anyother source and with
spec.credentials. Each stage sets its ownworker/workerPoolRef.Task-creation and dedupe logic. Wiring them is follow-up work, and lifting
the restriction later only loosens validation.
kelos run --fromand Console manual runs return an error naming thespawner instead of building a broken Task.
taskTemplatebecame optional. I usedjson:"taskTemplate,omitzero"with
+optional, keeping the value type. Converting it to a pointer wouldtouch about 98 references. With
omitzero, a typed Go client omits a zerotaskTemplate, which the integration specs exercise with the typed client.The three existing spec-level rules that read
self.taskTemplateare nowguarded with
!has(self.taskTemplate) || .... One spec checks that theinline-credentials rule still fires for a
taskTemplatespawner.pipelineTemplateis kept in akelos.dev/v1alpha2-pipeline-templateannotation, following the existingpreservation pattern. v1alpha1 always serializes
taskTemplate, includingits non-pointer
credentials. On the way back up, that empty template isdropped so the exactly-one rule still holds.
PipelineStageembeds the fullWorkerSpecschema, so theTaskSpawner CRD grows from about 844 KB to about 1.09 MB of compact JSON.
That is under etcd's 1.5 MiB object limit, but worth knowing.
TestHelmReleaseSecretFitsKubernetesLimitpasses.create/getontaskpipelines.Testing:
make verify,make test, andmake test-integrationare clean(179/179 specs, plus 12/12 install specs).
annotations is created, and no Task is created;
BuildTaskPipeline, including a deep-copy check;.Paramsin prompts and conditions;credentialsrule;paramsimmutability.Does this PR introduce a user-facing change?
🤖 Generated with Claude Code
Summary by cubic
Lets Slack TaskSpawners create TaskPipelines instead of Tasks, so a single Slack front door can route each message to the right agent using conditional stages.
spec.pipelineTemplate.stagesdefines the pipeline for each matching message; the pipeline keeps Task naming, dedupe, spawner label, owner reference, and Slack reporting annotations, and counts towardmaxConcurrencyandstatus.activeTasks.spec.params, an immutable string map exposed to stage templates as.Params; the spawner fills it from the message's string template variables.spec.taskTemplateis now optional — exactly one oftaskTemplateorpipelineTemplateis required via CEL, so existing manifests still apply unchanged.pipelineTemplateis Slack-only (rejected with other sources and withspec.credentials); manual runs (kelos run --from, Console) return an error naming the spawner.conditionsthat render an earlier result and compare withIn/NotIn; templates gainb64decto decode base64 results.Notes
pipelineTemplatesurvives a v1alpha1 round-trip via a preservation annotation; v1alpha1 always serializes ataskTemplate, which is dropped on conversion back so the exactly-one rule holds.create/getontaskpipelines.Written for commit ee7d947. Summary will update on new commits.