Repository navigation
Report TaskPipeline stage results to the originating Slack thread - #1855
knechtionscoding wants to merge 3 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.
Stage Tasks now inherit the TaskPipeline's Slack reporting label and annotations (kelos.dev/slack-reporting, -channel, -thread-ts, -user-id). A pipeline started from a Slack message therefore reports each stage that runs back to the thread, the same way a spawned Task does, with no change to the reporter. Skipped stages create no Task and post nothing. Only those keys are copied. The controller keeps its own copy of the keys rather than importing internal/reporting, and a test pins them to the reporter's constants. Adds examples/19-slack-pipeline-router, a Slack front door that classifies each request and runs one of several handler stages.
There was a problem hiding this comment.
5 issues found across 29 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/taskbuilder/pipeline_test.go">
<violation number="1" location="internal/taskbuilder/pipeline_test.go:74">
P2: This assertion does not verify that `BuildTaskPipeline` sets the controller owner reference: the already-asserted label makes `ObjectBelongsToSpawner` return true through its legacy fallback. Assert the owner reference’s UID, name, kind, API version, and controller flag directly so ownership regressions are caught.</violation>
</file>
<file name="docs/reference.md">
<violation number="1" location="docs/reference.md:1523">
P2: This guarantee is too broad for Slack slash-command work items: slash commands have no originating thread, and `setSlackReporting` intentionally omits the reporting label and thread annotation, so stage Tasks do not report. Limit this statement to message-triggered pipelines or explicitly document that slash-command pipelines do not receive Slack stage reporting.</violation>
</file>
<file name="internal/conversion/taskspawner.go">
<violation number="1" location="internal/conversion/taskspawner.go:143">
P1: Pipeline spawners are down-converted with an empty v1alpha1 `taskTemplate`, but v1alpha1 requires `taskTemplate.type` and `taskTemplate.credentials`. A v1alpha1 read followed by any write is rejected before the annotation can restore `pipelineTemplate`; emit a schema-valid fallback that the restore path recognizes, or do not expose pipeline spawners through v1alpha1.</violation>
<violation number="2" location="internal/conversion/taskspawner.go:164">
P2: This preservation path has no annotation-size guard. A valid v1alpha2 pipeline with large prompts or many stages can exceed Kubernetes’ 256 KiB annotation limit when converted, causing v1alpha1 conversion or write-back to fail; enforce the metadata budget or use a preservation mechanism that is not limited to annotations.</violation>
<violation number="3" location="internal/conversion/taskspawner.go:183">
P2: The restore accepts semantically invalid pipeline templates from the annotation. An edited annotation can therefore produce an invalid v1alpha2 TaskSpawner whose generated TaskPipeline is rejected or repeatedly fails; validate the restored template against its schema constraints or ignore it wholesale.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| if err := setPreservedSlackExcludeFilters(dst, src.Spec.When.Slack); err != nil { | ||
| return err | ||
| } | ||
| if err := setPreservedPipelineTemplate(dst, src.Spec.PipelineTemplate); err != nil { |
There was a problem hiding this comment.
P1: Pipeline spawners are down-converted with an empty v1alpha1 taskTemplate, but v1alpha1 requires taskTemplate.type and taskTemplate.credentials. A v1alpha1 read followed by any write is rejected before the annotation can restore pipelineTemplate; emit a schema-valid fallback that the restore path recognizes, or do not expose pipeline spawners through v1alpha1.
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 143:
<comment>Pipeline spawners are down-converted with an empty v1alpha1 `taskTemplate`, but v1alpha1 requires `taskTemplate.type` and `taskTemplate.credentials`. A v1alpha1 read followed by any write is rejected before the annotation can restore `pipelineTemplate`; emit a schema-valid fallback that the restore path recognizes, or do not expose pipeline spawners through v1alpha1.</comment>
<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
if err := setPreservedSlackExcludeFilters(dst, src.Spec.When.Slack); err != nil {
return err
}
+ if err := setPreservedPipelineTemplate(dst, src.Spec.PipelineTemplate); err != nil {
+ return err
+ }
</file context>
| 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 does not verify that BuildTaskPipeline sets the controller owner reference: the already-asserted label makes ObjectBelongsToSpawner return true through its legacy fallback. Assert the owner reference’s UID, name, kind, API version, and controller flag directly so ownership regressions 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/taskbuilder/pipeline_test.go, line 74:
<comment>This assertion does not verify that `BuildTaskPipeline` sets the controller owner reference: the already-asserted label makes `ObjectBelongsToSpawner` return true through its legacy fallback. Assert the owner reference’s UID, name, kind, API version, and controller flag directly so ownership regressions are caught.</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>
| - The TaskPipeline is named and deduplicated the way the Task would have been, | ||
| carries the `kelos.dev/taskspawner` label and the same Slack reporting | ||
| annotations, and is owned by the TaskSpawner. Stage Tasks inherit the Slack | ||
| reporting annotations, so every stage that runs posts its status and result |
There was a problem hiding this comment.
P2: This guarantee is too broad for Slack slash-command work items: slash commands have no originating thread, and setSlackReporting intentionally omits the reporting label and thread annotation, so stage Tasks do not report. Limit this statement to message-triggered pipelines or explicitly document that slash-command pipelines do not receive Slack stage reporting.
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 docs/reference.md, line 1523:
<comment>This guarantee is too broad for Slack slash-command work items: slash commands have no originating thread, and `setSlackReporting` intentionally omits the reporting label and thread annotation, so stage Tasks do not report. Limit this statement to message-triggered pipelines or explicitly document that slash-command pipelines do not receive Slack stage reporting.</comment>
<file context>
@@ -1415,10 +1502,62 @@ bundled skills. Reading an AgentConfig through `kelos.dev/v1alpha1` omits
+- The TaskPipeline is named and deduplicated the way the Task would have been,
+ carries the `kelos.dev/taskspawner` label and the same Slack reporting
+ annotations, and is owned by the TaskSpawner. Stage Tasks inherit the Slack
+ reporting annotations, so every stage that runs posts its status and result
+ to the originating thread. Skipped stages post nothing.
+- The work item's string template variables (for Slack: `ID`, `Title`, `Body`,
</file context>
| reporting annotations, so every stage that runs posts its status and result | |
| annotations, so every stage that runs from a Slack message posts its status and result |
| // API server still rejects a TaskSpawner left without a template. | ||
| return | ||
| } | ||
| dst.PipelineTemplate = &pipelineTemplate |
There was a problem hiding this comment.
P2: The restore accepts semantically invalid pipeline templates from the annotation. An edited annotation can therefore produce an invalid v1alpha2 TaskSpawner whose generated TaskPipeline is rejected or repeatedly fails; validate the restored template against its schema constraints or ignore it wholesale.
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 183:
<comment>The restore accepts semantically invalid pipeline templates from the annotation. An edited annotation can therefore produce an invalid v1alpha2 TaskSpawner whose generated TaskPipeline is rejected or repeatedly fails; validate the restored template against its schema constraints or ignore it wholesale.</comment>
<file context>
@@ -133,9 +140,62 @@ func taskSpawnerFromHub(_ context.Context, src *v1alpha2.TaskSpawner, dst *v1alp
+ // API server still rejects a TaskSpawner left without a template.
+ return
+ }
+ dst.PipelineTemplate = &pipelineTemplate
+ // v1alpha1 always serializes taskTemplate, including its non-pointer
+ // credentials, so a pipeline spawner comes back with an empty template.
</file context>
| if dst.Annotations == nil { | ||
| dst.Annotations = map[string]string{} | ||
| } | ||
| dst.Annotations[preservedPipelineTemplateAnnotation] = string(data) |
There was a problem hiding this comment.
P2: This preservation path has no annotation-size guard. A valid v1alpha2 pipeline with large prompts or many stages can exceed Kubernetes’ 256 KiB annotation limit when converted, causing v1alpha1 conversion or write-back to fail; enforce the metadata budget or use a preservation mechanism that is not limited to annotations.
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 preservation path has no annotation-size guard. A valid v1alpha2 pipeline with large prompts or many stages can exceed Kubernetes’ 256 KiB annotation limit when converted, causing v1alpha1 conversion or write-back to fail; enforce the metadata budget or use a preservation mechanism that is not limited to annotations.</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>
What type of PR is this?
/kind feature
What this PR does / why we need it:
Stacked on #1853 and #1854. Only the top commit is new; review that one.
This closes the loop from #1761: results from a Slack-started TaskPipeline go
back to the thread that started it.
kelos.dev/slack-reportinglabel and thekelos.dev/slack-reporting,-channel,-thread-tsand-user-idannotations. Let Slack TaskSpawners create TaskPipelines #1854 sets these onpipelines it creates from Slack messages.
internal/reportingalready reports any Taskthat carries these keys, so each stage that runs posts its acceptance,
progress and result into the thread. Skipped stages create no Task and post
nothing.
as the acknowledgement, and the selected handler posts the real answer.
slack-report-phase), thespawner label (which would double count
activeTasks), and unrelatedmetadata stay on the pipeline.
examples/19-slack-pipeline-routeris a front door thatclassifies each Slack request and runs one of three handlers, or a
NotInfallback.
This is the third of three stacked PRs:
pipelineTemplate.Which issue(s) this PR is related to:
Part of #1760. Depends on #1853 and #1854.
Special notes for your reviewer:
the acknowledgement plus the answer. Longer pipelines will be noisier, and a
per-stage way to opt out of reporting could follow if needed. I kept it out
so the API stays minimal.
four keys instead of importing
internal/reporting, which would pullslack-go into the controller binary for the first time.
TestPipelineSlackReportingKeysMatchReporterpins them to the reporter'sconstants so they can't drift.
(Let Slack TaskSpawners create TaskPipelines #1854), and GitHub comment and check reporting needs its own thought about
one comment or check per stage.
Testing:
make verify,make test, andmake test-integrationare clean(180/180 specs, plus 12/12 install specs).
keys and not reporter state, the spawner label, or unrelated annotations;
reporting label;
internal/reporting.Does this PR introduce a user-facing change?
🤖 Generated with Claude Code
Summary by cubic
TaskPipeline stages started from a Slack message now report each stage's status and result back to the thread that started the pipeline.
Stage Tasks inherit the pipeline's Slack reporting label and annotations (
slack-reporting,slack-channel,slack-thread-ts,slack-user-id), so any stage that runs posts to the thread exactly like a spawned Task. Skipped stages create no Task and post nothing, and the new exampleexamples/19-slack-pipeline-routerroutes each request through a classification stage to one of several handlers.internal/reporting, pinned by a test so they can't drift.In/NotInconditions) andTaskSpawner.spec.pipelineTemplate, which the stacked PRs introduce and this one reports on.Written for commit 4f62567. Summary will update on new commits.