From 2ce097167c900768575fb62ece09448d3df2f3c4 Mon Sep 17 00:00:00 2001 From: Corey Christous Date: Sat, 28 Feb 2026 08:18:04 -0500 Subject: [PATCH 1/4] feat: surface after-pipeline task info in pipeline describe The REST API already returns with_after_task and after_task_id fields in pipeline describe responses, but the CLI model was missing these fields so they were silently dropped during JSON unmarshaling. This makes after-pipeline jobs visible in `sem get pipeline` output instead of being completely hidden. --- api/models/pipeline_v1_alpha.go | 14 ++++--- .../2026-02-28-after-pipeline-jobs-design.md | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 docs/plans/2026-02-28-after-pipeline-jobs-design.md diff --git a/api/models/pipeline_v1_alpha.go b/api/models/pipeline_v1_alpha.go index b5ad561..5f7cc1e 100644 --- a/api/models/pipeline_v1_alpha.go +++ b/api/models/pipeline_v1_alpha.go @@ -8,12 +8,14 @@ import ( type PipelineV1Alpha struct { Pipeline struct { - ID string `json:"ppl_id"` - Name string `json:"name,omitempty"` - State string `json:"state,omitempty"` - Result string `json:"result,omitempty" yaml:"result,omitempty"` - Reason string `json:"result_reason,omitempty" yaml:"result_reason,omitempty"` - Error string `json:"error_description,omitempty" yaml:"error_description,omitempty"` + ID string `json:"ppl_id"` + Name string `json:"name,omitempty"` + State string `json:"state,omitempty"` + Result string `json:"result,omitempty" yaml:"result,omitempty"` + Reason string `json:"result_reason,omitempty" yaml:"result_reason,omitempty"` + Error string `json:"error_description,omitempty" yaml:"error_description,omitempty"` + WithAfterTask bool `json:"with_after_task,omitempty" yaml:"with_after_task,omitempty"` + AfterTaskID string `json:"after_task_id,omitempty" yaml:"after_task_id,omitempty"` } `json:"pipeline,omitempty"` Blocks []PipelineV1AlphaBlock `json:"blocks,omitempty"` } diff --git a/docs/plans/2026-02-28-after-pipeline-jobs-design.md b/docs/plans/2026-02-28-after-pipeline-jobs-design.md new file mode 100644 index 0000000..8e352d5 --- /dev/null +++ b/docs/plans/2026-02-28-after-pipeline-jobs-design.md @@ -0,0 +1,42 @@ +# Design: Surface after-pipeline jobs in pipeline describe + +## Problem + +When a pipeline has after-pipeline jobs (cleanup/publish steps that run after the main pipeline), `sem get pipeline` doesn't show them. The REST API already returns `with_after_task` and `after_task_id` fields, but the CLI's `PipelineV1Alpha` model doesn't declare them, so `json.Unmarshal` silently drops them. + +This matches the issue fixed for the MCP server in [semaphoreio/semaphore#866](https://github.com/semaphoreio/semaphore/pull/866). + +## Approach (Phase 1: flag only) + +Add `WithAfterTask` and `AfterTaskID` fields to the `PipelineV1Alpha.Pipeline` struct with `omitempty` tags. No changes to the API client or describe command — the existing `DescribePpl` call already returns these fields. + +### Changes + +- `api/models/pipeline_v1_alpha.go`: Add two fields to the `Pipeline` inner struct. + +### Output + +Pipelines with after-tasks will show: + +```yaml +pipeline: + ppl_id: abc-123 + name: Deploy + state: done + result: passed + with_after_task: true + after_task_id: zebra-task-456 +blocks: + - name: Build + ... +``` + +Pipelines without after-tasks are unchanged (omitempty). + +## Future work (Phase 2: resolve job IDs) + +The v1alpha `describe_topology` REST endpoint currently drops the `after_pipeline` field from its response. Once the server-side response formatter is updated to include it, the CLI can: + +1. Add a `DescribeTopology` method to the pipelines API client +2. Call it when `with_after_task=true` to resolve actual job IDs +3. Display after-pipeline job IDs so users can run `sem logs ` From 0b8472fd07f24f13e2b44bf4c6a8b6a5ec60d1de Mon Sep 17 00:00:00 2001 From: Corey Christous Date: Sat, 28 Feb 2026 08:43:18 -0500 Subject: [PATCH 2/4] Delete docs/plans/2026-02-28-after-pipeline-jobs-design.md --- .../2026-02-28-after-pipeline-jobs-design.md | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 docs/plans/2026-02-28-after-pipeline-jobs-design.md diff --git a/docs/plans/2026-02-28-after-pipeline-jobs-design.md b/docs/plans/2026-02-28-after-pipeline-jobs-design.md deleted file mode 100644 index 8e352d5..0000000 --- a/docs/plans/2026-02-28-after-pipeline-jobs-design.md +++ /dev/null @@ -1,42 +0,0 @@ -# Design: Surface after-pipeline jobs in pipeline describe - -## Problem - -When a pipeline has after-pipeline jobs (cleanup/publish steps that run after the main pipeline), `sem get pipeline` doesn't show them. The REST API already returns `with_after_task` and `after_task_id` fields, but the CLI's `PipelineV1Alpha` model doesn't declare them, so `json.Unmarshal` silently drops them. - -This matches the issue fixed for the MCP server in [semaphoreio/semaphore#866](https://github.com/semaphoreio/semaphore/pull/866). - -## Approach (Phase 1: flag only) - -Add `WithAfterTask` and `AfterTaskID` fields to the `PipelineV1Alpha.Pipeline` struct with `omitempty` tags. No changes to the API client or describe command — the existing `DescribePpl` call already returns these fields. - -### Changes - -- `api/models/pipeline_v1_alpha.go`: Add two fields to the `Pipeline` inner struct. - -### Output - -Pipelines with after-tasks will show: - -```yaml -pipeline: - ppl_id: abc-123 - name: Deploy - state: done - result: passed - with_after_task: true - after_task_id: zebra-task-456 -blocks: - - name: Build - ... -``` - -Pipelines without after-tasks are unchanged (omitempty). - -## Future work (Phase 2: resolve job IDs) - -The v1alpha `describe_topology` REST endpoint currently drops the `after_pipeline` field from its response. Once the server-side response formatter is updated to include it, the CLI can: - -1. Add a `DescribeTopology` method to the pipelines API client -2. Call it when `with_after_task=true` to resolve actual job IDs -3. Display after-pipeline job IDs so users can run `sem logs ` From 6fea2f9ef5553da8666405ec273e5ff733d1e19b Mon Sep 17 00:00:00 2001 From: Corey Christous Date: Sat, 28 Feb 2026 08:56:33 -0500 Subject: [PATCH 3/4] test: add pipeline model tests for after-task fields Covers JSON deserialization and YAML output for pipelines with and without after-pipeline tasks, verifying omitempty behavior. --- api/models/pipeline_v1_alpha_test.go | 85 ++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 api/models/pipeline_v1_alpha_test.go diff --git a/api/models/pipeline_v1_alpha_test.go b/api/models/pipeline_v1_alpha_test.go new file mode 100644 index 0000000..9f48e53 --- /dev/null +++ b/api/models/pipeline_v1_alpha_test.go @@ -0,0 +1,85 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPipelineFromJsonWithAfterTask(t *testing.T) { + content := `{ + "pipeline": { + "ppl_id": "abc-123", + "name": "Deploy", + "state": "done", + "result": "passed", + "with_after_task": true, + "after_task_id": "zebra-task-456" + }, + "blocks": [ + { + "name": "Build", + "state": "done", + "result": "passed", + "jobs": [{"name": "compile", "job_id": "job-1"}] + } + ] + }` + + ppl, err := NewPipelineV1AlphaFromJson([]byte(content)) + assert.Nil(t, err) + assert.Equal(t, "abc-123", ppl.Pipeline.ID) + assert.Equal(t, "Deploy", ppl.Pipeline.Name) + assert.True(t, ppl.Pipeline.WithAfterTask) + assert.Equal(t, "zebra-task-456", ppl.Pipeline.AfterTaskID) + assert.Len(t, ppl.Blocks, 1) +} + +func TestPipelineFromJsonWithoutAfterTask(t *testing.T) { + content := `{ + "pipeline": { + "ppl_id": "abc-123", + "name": "CI", + "state": "done", + "result": "passed" + }, + "blocks": [] + }` + + ppl, err := NewPipelineV1AlphaFromJson([]byte(content)) + assert.Nil(t, err) + assert.False(t, ppl.Pipeline.WithAfterTask) + assert.Empty(t, ppl.Pipeline.AfterTaskID) +} + +func TestPipelineToYamlWithAfterTask(t *testing.T) { + ppl := PipelineV1Alpha{} + ppl.Pipeline.ID = "abc-123" + ppl.Pipeline.Name = "Deploy" + ppl.Pipeline.State = "done" + ppl.Pipeline.Result = "passed" + ppl.Pipeline.WithAfterTask = true + ppl.Pipeline.AfterTaskID = "zebra-task-456" + + yamlBytes, err := ppl.ToYaml() + assert.Nil(t, err) + + yaml := string(yamlBytes) + assert.Contains(t, yaml, "with_after_task: true") + assert.Contains(t, yaml, "after_task_id: zebra-task-456") +} + +func TestPipelineToYamlWithoutAfterTask(t *testing.T) { + ppl := PipelineV1Alpha{} + ppl.Pipeline.ID = "abc-123" + ppl.Pipeline.Name = "CI" + ppl.Pipeline.State = "done" + ppl.Pipeline.Result = "passed" + + yamlBytes, err := ppl.ToYaml() + assert.Nil(t, err) + + yaml := string(yamlBytes) + assert.NotContains(t, yaml, "with_after_task") + assert.NotContains(t, yaml, "after_task_id") +} From ba058e366615a35ecb59fe56b407f8b8ab5f397b Mon Sep 17 00:00:00 2001 From: Corey Christous Date: Sat, 28 Feb 2026 09:24:52 -0500 Subject: [PATCH 4/4] refactor: drop WithAfterTask field, keep only AfterTaskID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WithAfterTask is redundant — a non-empty AfterTaskID already indicates the pipeline has an after-task. Removing it gives cleaner output. --- api/models/pipeline_v1_alpha.go | 15 +++++++-------- api/models/pipeline_v1_alpha_test.go | 6 ------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/api/models/pipeline_v1_alpha.go b/api/models/pipeline_v1_alpha.go index 5f7cc1e..b75a21a 100644 --- a/api/models/pipeline_v1_alpha.go +++ b/api/models/pipeline_v1_alpha.go @@ -8,14 +8,13 @@ import ( type PipelineV1Alpha struct { Pipeline struct { - ID string `json:"ppl_id"` - Name string `json:"name,omitempty"` - State string `json:"state,omitempty"` - Result string `json:"result,omitempty" yaml:"result,omitempty"` - Reason string `json:"result_reason,omitempty" yaml:"result_reason,omitempty"` - Error string `json:"error_description,omitempty" yaml:"error_description,omitempty"` - WithAfterTask bool `json:"with_after_task,omitempty" yaml:"with_after_task,omitempty"` - AfterTaskID string `json:"after_task_id,omitempty" yaml:"after_task_id,omitempty"` + ID string `json:"ppl_id"` + Name string `json:"name,omitempty"` + State string `json:"state,omitempty"` + Result string `json:"result,omitempty" yaml:"result,omitempty"` + Reason string `json:"result_reason,omitempty" yaml:"result_reason,omitempty"` + Error string `json:"error_description,omitempty" yaml:"error_description,omitempty"` + AfterTaskID string `json:"after_task_id,omitempty" yaml:"after_task_id,omitempty"` } `json:"pipeline,omitempty"` Blocks []PipelineV1AlphaBlock `json:"blocks,omitempty"` } diff --git a/api/models/pipeline_v1_alpha_test.go b/api/models/pipeline_v1_alpha_test.go index 9f48e53..1b10ec8 100644 --- a/api/models/pipeline_v1_alpha_test.go +++ b/api/models/pipeline_v1_alpha_test.go @@ -13,7 +13,6 @@ func TestPipelineFromJsonWithAfterTask(t *testing.T) { "name": "Deploy", "state": "done", "result": "passed", - "with_after_task": true, "after_task_id": "zebra-task-456" }, "blocks": [ @@ -30,7 +29,6 @@ func TestPipelineFromJsonWithAfterTask(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "abc-123", ppl.Pipeline.ID) assert.Equal(t, "Deploy", ppl.Pipeline.Name) - assert.True(t, ppl.Pipeline.WithAfterTask) assert.Equal(t, "zebra-task-456", ppl.Pipeline.AfterTaskID) assert.Len(t, ppl.Blocks, 1) } @@ -48,7 +46,6 @@ func TestPipelineFromJsonWithoutAfterTask(t *testing.T) { ppl, err := NewPipelineV1AlphaFromJson([]byte(content)) assert.Nil(t, err) - assert.False(t, ppl.Pipeline.WithAfterTask) assert.Empty(t, ppl.Pipeline.AfterTaskID) } @@ -58,14 +55,12 @@ func TestPipelineToYamlWithAfterTask(t *testing.T) { ppl.Pipeline.Name = "Deploy" ppl.Pipeline.State = "done" ppl.Pipeline.Result = "passed" - ppl.Pipeline.WithAfterTask = true ppl.Pipeline.AfterTaskID = "zebra-task-456" yamlBytes, err := ppl.ToYaml() assert.Nil(t, err) yaml := string(yamlBytes) - assert.Contains(t, yaml, "with_after_task: true") assert.Contains(t, yaml, "after_task_id: zebra-task-456") } @@ -80,6 +75,5 @@ func TestPipelineToYamlWithoutAfterTask(t *testing.T) { assert.Nil(t, err) yaml := string(yamlBytes) - assert.NotContains(t, yaml, "with_after_task") assert.NotContains(t, yaml, "after_task_id") }