From 4916e734ed0bd44474b7ae76c4f3026709d34c0f Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Mon, 15 Jun 2026 11:56:43 +0000 Subject: [PATCH 1/3] feat(pipelines): add pipeline show command --- internal/cmd/pipelines/pipelines.go | 2 + internal/cmd/pipelines/show/show.go | 190 +++++++++++++++++++++++++++ internal/cmd/pipelines/show/show.tpl | 40 ++++++ 3 files changed, 232 insertions(+) create mode 100644 internal/cmd/pipelines/show/show.go create mode 100644 internal/cmd/pipelines/show/show.tpl diff --git a/internal/cmd/pipelines/pipelines.go b/internal/cmd/pipelines/pipelines.go index b770e080..7a213923 100644 --- a/internal/cmd/pipelines/pipelines.go +++ b/internal/cmd/pipelines/pipelines.go @@ -6,6 +6,7 @@ import ( "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/list" "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/pool" "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/runs" + "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/show" "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/variablegroup" "github.com/tmeckel/azdo-cli/internal/cmd/util" ) @@ -19,6 +20,7 @@ func NewCmd(ctx util.CmdContext) *cobra.Command { cmd.AddCommand(list.NewCmd(ctx)) cmd.AddCommand(runs.NewCmd(ctx)) + cmd.AddCommand(show.NewCmd(ctx)) cmd.AddCommand(variablegroup.NewCmd(ctx)) cmd.AddCommand(agent.NewCmd(ctx)) cmd.AddCommand(pool.NewCmd(ctx)) diff --git a/internal/cmd/pipelines/show/show.go b/internal/cmd/pipelines/show/show.go new file mode 100644 index 00000000..ef58ed7c --- /dev/null +++ b/internal/cmd/pipelines/show/show.go @@ -0,0 +1,190 @@ +package show + +import ( + _ "embed" + "fmt" + "strconv" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/build" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/tmeckel/azdo-cli/internal/cmd/util" + "github.com/tmeckel/azdo-cli/internal/template" + "github.com/tmeckel/azdo-cli/internal/types" +) + +type showOptions struct { + exporter util.Exporter + scopeArg string +} + +//go:embed show.tpl +var showTmpl string + +func NewCmd(ctx util.CmdContext) *cobra.Command { + opts := &showOptions{} + + cmd := &cobra.Command{ + Use: "show [ORGANIZATION/]PROJECT/PIPELINE", + Short: "Show details of a pipeline definition", + Long: heredoc.Doc(` + Display the details of a single Azure Pipelines definition. + + The pipeline may be specified by ID (integer) or name (string). + When the organization segment is omitted the default organization + from configuration is used. + `), + Example: heredoc.Doc(` + # Show a pipeline by ID using the default organization + azdo pipelines show Fabrikam/42 + + # Show a pipeline by name + azdo pipelines show Fabrikam/My Pipeline + + # Show with explicit organization + azdo pipelines show MyOrg/Fabrikam/42 + + # Export as JSON + azdo pipelines show Fabrikam/42 --json id,name,revision + `), + Aliases: []string{"view", "status"}, + Args: util.ExactArgs(1, "pipeline target is required"), + RunE: func(cmd *cobra.Command, args []string) error { + opts.scopeArg = args[0] + return runShow(ctx, opts) + }, + } + + util.AddJSONFlags(cmd, &opts.exporter, []string{ + "id", "name", "revision", "description", "path", "type", "url", "_links", + "process", "repository", "queue", "authoredBy", "createdDate", "quality", + }) + + return cmd +} + +func runShow(ctx util.CmdContext, opts *showOptions) error { + ios, err := ctx.IOStreams() + if err != nil { + return err + } + + ios.StartProgressIndicator() + defer ios.StopProgressIndicator() + + scope, err := util.ParseProjectTargetWithDefaultOrganization(ctx, opts.scopeArg) + if err != nil { + return util.FlagErrorWrap(err) + } + + clientFact := ctx.ClientFactory() + + buildClient, err := clientFact.Build(ctx.Context(), scope.Organization) + if err != nil { + return fmt.Errorf("failed to create Build client: %w", err) + } + + logger := zap.L().With( + zap.String("organization", scope.Organization), + zap.String("project", scope.Project), + zap.String("pipeline", scope.Targets[0]), + ) + + raw := scope.Targets[0] + pipelineID, err := strconv.Atoi(raw) + if err == nil && pipelineID <= 0 { + return fmt.Errorf("pipeline id must be greater than zero: %q", raw) + } + if err != nil { + defs, err := buildClient.GetDefinitions(ctx.Context(), build.GetDefinitionsArgs{ + Project: types.ToPtr(scope.Project), + Name: types.ToPtr(raw), + }) + if err != nil { + return fmt.Errorf("failed to query pipeline definitions: %w", err) + } + + if defs == nil || len(defs.Value) == 0 { + return fmt.Errorf("pipeline %q not found", raw) + } + + if len(defs.Value) > 1 { + return fmt.Errorf("pipeline %q is ambiguous: %d matches found", raw, len(defs.Value)) + } + + pipelineID = types.GetValue(defs.Value[0].Id, 0) + if pipelineID <= 0 { + return fmt.Errorf("pipeline %q returned empty id", raw) + } + } + + logger.Debug("fetching pipeline definition", zap.Int("pipelineId", pipelineID)) + + definition, err := buildClient.GetDefinition(ctx.Context(), build.GetDefinitionArgs{ + Project: types.ToPtr(scope.Project), + DefinitionId: types.ToPtr(pipelineID), + }) + if err != nil { + return fmt.Errorf("failed to fetch pipeline definition: %w", err) + } + + ios.StopProgressIndicator() + + if opts.exporter != nil { + return opts.exporter.Write(ios, definition) + } + + t := template.New( + ios.Out, + ios.TerminalWidth(), + ios.ColorEnabled(), + ). + WithTheme(ios.TerminalTheme()). + WithFuncs(map[string]any{ + "formatEntity": func(primary, secondary any) string { + first := template.StringOrEmpty(primary) + second := template.StringOrEmpty(secondary) + switch { + case first != "" && second != "": + return fmt.Sprintf("%s (%s)", first, second) + case first != "": + return first + default: + return second + } + }, + "identityDisplay": func(id *webapi.IdentityRef) string { + if id == nil { + return "" + } + display := types.GetValue(id.DisplayName, "") + unique := types.GetValue(id.UniqueName, "") + switch { + case display != "" && unique != "": + return fmt.Sprintf("%s (%s)", display, unique) + case display != "": + return display + default: + return unique + } + }, + "hasItems": template.HasItems, + "hasText": template.HasText, + "s": template.StringOrEmpty, + "int": func(v *int) string { + if v == nil { + return "" + } + return strconv.Itoa(*v) + }, + }) + + if err := t.Parse(showTmpl); err != nil { + return err + } + + return t.ExecuteData(*definition) +} diff --git a/internal/cmd/pipelines/show/show.tpl b/internal/cmd/pipelines/show/show.tpl new file mode 100644 index 00000000..7f841405 --- /dev/null +++ b/internal/cmd/pipelines/show/show.tpl @@ -0,0 +1,40 @@ +{{- if hasText .Url }} +{{bold "url:"}} {{hyperlink (s .Url) (s .Url)}} +{{- end }} +{{- if hasText .Id }} +{{bold "id:"}} {{.Id}} +{{- end }} +{{- if hasText .Name }} +{{bold "name:"}} {{s .Name}} +{{- end }} +{{- if hasText .Revision }} +{{bold "revision:"}} {{.Revision}} +{{- end }} +{{- if hasText .Path }} +{{bold "path:"}} {{s .Path}} +{{- end }} +{{- if hasText .Type }} +{{bold "type:"}} {{s .Type}} +{{- end }} +{{- if .Process }} +{{bold "process:"}} {{s .Process}} +{{- end }} +{{- if and .Repository (hasText (formatEntity .Repository.Name .Repository.Id)) }} +{{bold "repository:"}} {{formatEntity .Repository.Name .Repository.Id}} +{{- end }} +{{- if and .Queue (hasText (formatEntity .Queue.Name .Queue.Id)) }} +{{bold "queue:"}} {{formatEntity .Queue.Name .Queue.Id}} +{{- end }} +{{- if .AuthoredBy }} +{{bold "authored by:"}} {{identityDisplay .AuthoredBy}} +{{- end }} +{{- if .CreatedDate }} +{{bold "created on:"}} {{timeago .CreatedDate.Time}} ({{timefmt "2006-01-02 15:04:05" .CreatedDate.Time}}) +{{- end }} +{{- if hasText .Description }} +{{bold "description:"}} +{{markdown (s .Description)}} +{{- end }} +{{- if hasText .Quality }} +{{bold "quality:"}} {{s .Quality}} +{{- end }} From 664c6e0a9467191eded5c8193f6acfcd7f724e0e Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Mon, 15 Jun 2026 12:06:56 +0000 Subject: [PATCH 2/3] =?UTF-8?q?test(pipelines):=20=F0=9F=A7=AA=20add=20tes?= =?UTF-8?q?ts=20for=20show=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/cmd/pipelines/show/show_test.go | 459 +++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 internal/cmd/pipelines/show/show_test.go diff --git a/internal/cmd/pipelines/show/show_test.go b/internal/cmd/pipelines/show/show_test.go new file mode 100644 index 00000000..ecf60071 --- /dev/null +++ b/internal/cmd/pipelines/show/show_test.go @@ -0,0 +1,459 @@ +package show + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/build" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/tmeckel/azdo-cli/internal/iostreams" + "github.com/tmeckel/azdo-cli/internal/mocks" +) + +type dependencies struct { + cmd *mocks.MockCmdContext + clientFact *mocks.MockClientFactory + buildClient *mocks.MockBuildClient + cfg *mocks.MockConfig + authCfg *mocks.MockAuthConfig + stdout *bytes.Buffer + t *testing.T +} + +func newDependencies(t *testing.T, organization string) *dependencies { + t.Helper() + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + io, _, out, _ := iostreams.Test() + io.SetStdoutTTY(false) + io.SetStderrTTY(false) + + d := &dependencies{ + cmd: mocks.NewMockCmdContext(ctrl), + clientFact: mocks.NewMockClientFactory(ctrl), + buildClient: mocks.NewMockBuildClient(ctrl), + cfg: mocks.NewMockConfig(ctrl), + authCfg: mocks.NewMockAuthConfig(ctrl), + stdout: out, + t: t, + } + + d.cmd.EXPECT().IOStreams().Return(io, nil).AnyTimes() + d.cmd.EXPECT().Context().Return(context.Background()).AnyTimes() + d.cmd.EXPECT().ClientFactory().Return(d.clientFact).AnyTimes() + d.cmd.EXPECT().Config().Return(d.cfg, nil).AnyTimes() + d.cfg.EXPECT().Authentication().Return(d.authCfg).AnyTimes() + d.authCfg.EXPECT().GetDefaultOrganization().Return(organization, nil).AnyTimes() + + return d +} + +func (d *dependencies) setupBuildClient() { + d.clientFact.EXPECT().Build(gomock.Any(), gomock.Any()).Return(d.buildClient, nil).AnyTimes() +} + +func (d *dependencies) setupGetDefinition(def *build.BuildDefinition, err error) { + d.buildClient.EXPECT().GetDefinition(gomock.Any(), gomock.Any()).Return(def, err).Times(1) +} + +func (d *dependencies) expectGetDefinition(project string, id int, def *build.BuildDefinition, err error) { + d.buildClient.EXPECT().GetDefinition(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, args build.GetDefinitionArgs) (*build.BuildDefinition, error) { + require.NotNil(d.t, args.Project) + require.Equal(d.t, project, *args.Project) + require.NotNil(d.t, args.DefinitionId) + require.Equal(d.t, id, *args.DefinitionId) + return def, err + }, + ).Times(1) +} + +func (d *dependencies) expectGetDefinitionsByName(project, name string, resp *build.GetDefinitionsResponseValue, err error) { + d.buildClient.EXPECT().GetDefinitions(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, args build.GetDefinitionsArgs) (*build.GetDefinitionsResponseValue, error) { + require.NotNil(d.t, args.Project) + require.Equal(d.t, project, *args.Project) + require.NotNil(d.t, args.Name) + require.Equal(d.t, name, *args.Name) + return resp, err + }, + ).Times(1) +} + +func defFromJSON(t *testing.T, j string) *build.BuildDefinition { + t.Helper() + var d build.BuildDefinition + require.NoError(t, json.Unmarshal([]byte(j), &d)) + return &d +} + +func sampleDef(t *testing.T) *build.BuildDefinition { + t.Helper() + return defFromJSON(t, `{ + "id": 42, "name": "MyPipeline", "revision": 7, + "description": "

My pipeline description

", + "path": "\\", "type": "build", + "url": "https://dev.azure.com/myorg/fabrikam/_apis/pipelines/definitions/42", + "_links": {}, "process": {"type": 2, "yamlFilename": "azure-pipelines.yml"}, + "repository": {"id": "repo-id", "name": "MyRepo"}, + "queue": {"id": 1, "name": "Azure Pipelines"}, + "authoredBy": {"displayName": "Alice", "uniqueName": "alice@x.com", "id": "alice-id"}, + "createdDate": "2024-01-01T12:00:00Z", "quality": "definition" + }`) +} + +func TestNewCmd_RegistersAsShowLeaf(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + cmd := NewCmd(d.cmd) + assert.Equal(t, "show", cmd.Name()) + assert.Contains(t, cmd.Aliases, "view") + assert.Contains(t, cmd.Aliases, "status") + assert.Contains(t, cmd.Use, "show [ORGANIZATION/]PROJECT/PIPELINE") +} + +func TestNewCmd_RequiresOneArg(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + cmd := NewCmd(d.cmd) + cmd.SetArgs([]string{}) + err := cmd.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "pipeline target is required") +} + +func TestRunShow_ResolveByPositiveInteger(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + d.expectGetDefinition("Fabrikam", 42, sampleDef(t), nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/42"} + err := runShow(d.cmd, opts) + require.NoError(t, err) +} + +func TestRunShow_RejectsNonPositiveNumericPipelineID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + raw string + }{ + {name: "zero", raw: "0"}, + {name: "negative", raw: "-1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/" + tt.raw} + err := runShow(d.cmd, opts) + require.Error(t, err) + assert.Contains(t, err.Error(), "pipeline id must be greater than zero") + }) + } +} + +func TestRunShow_ResolveByName(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + + defID := 42 + defName := "MyPipeline" + d.expectGetDefinitionsByName("Fabrikam", "MyPipeline", &build.GetDefinitionsResponseValue{ + Value: []build.BuildDefinitionReference{ + {Id: &defID, Name: &defName}, + }, + }, nil) + + d.expectGetDefinition("Fabrikam", 42, sampleDef(t), nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/MyPipeline"} + err := runShow(d.cmd, opts) + require.NoError(t, err) + + output := d.stdout.String() + assert.Contains(t, output, "MyPipeline") +} + +func TestRunShow_NameNotFound(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + + d.expectGetDefinitionsByName("Fabrikam", "NonExistent", &build.GetDefinitionsResponseValue{ + Value: []build.BuildDefinitionReference{}, + }, nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/NonExistent"} + err := runShow(d.cmd, opts) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestRunShow_NameAmbiguous(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + + id1, id2 := 1, 2 + n1, n2 := "SameName", "SameName" + d.expectGetDefinitionsByName("Fabrikam", "SameName", &build.GetDefinitionsResponseValue{ + Value: []build.BuildDefinitionReference{ + {Id: &id1, Name: &n1}, + {Id: &id2, Name: &n2}, + }, + }, nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/SameName"} + err := runShow(d.cmd, opts) + require.Error(t, err) + assert.Contains(t, err.Error(), "ambiguous") +} + +func TestRunShow_TemplateOutput_BasicFields(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + d.setupGetDefinition(sampleDef(t), nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/42"} + err := runShow(d.cmd, opts) + require.NoError(t, err) + + output := d.stdout.String() + assert.Contains(t, output, "id:") + assert.Contains(t, output, "name:") + assert.Contains(t, output, "revision:") + assert.Contains(t, output, "path:") + assert.Contains(t, output, "type:") + assert.Contains(t, output, "quality:") + assert.Contains(t, output, "MyPipeline") +} + +func TestRunShow_ResolveByNameQueryError(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + d.expectGetDefinitionsByName("Fabrikam", "MyPipeline", nil, fmt.Errorf("lookup failed")) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/MyPipeline"} + err := runShow(d.cmd, opts) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to query pipeline definitions") +} + +func TestRunShow_ResolveByNameWithEmptyDefinitionID(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + + defName := "MyPipeline" + d.expectGetDefinitionsByName("Fabrikam", "MyPipeline", &build.GetDefinitionsResponseValue{ + Value: []build.BuildDefinitionReference{{Name: &defName}}, + }, nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/MyPipeline"} + err := runShow(d.cmd, opts) + require.Error(t, err) + assert.Contains(t, err.Error(), "returned empty id") +} + +func TestRunShow_TemplateOutput_URL(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + d.setupGetDefinition(sampleDef(t), nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/42"} + err := runShow(d.cmd, opts) + require.NoError(t, err) + + output := d.stdout.String() + assert.Contains(t, output, "url:") + assert.Contains(t, output, "https://dev.azure.com/myorg/fabrikam/_apis/pipelines/definitions/42") +} + +func TestRunShow_TemplateOutput_DescriptionMarkdown(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + + defJSON := `{ + "id": 1, "name": "Test", "description": "

Hello World

", + "path": "\\", "type": "build", "_links": {}, + "url": "https://dev.azure.com/myorg/test/_apis/pipelines/definitions/1", + "quality": "definition" + }` + d.setupGetDefinition(defFromJSON(t, defJSON), nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/1"} + err := runShow(d.cmd, opts) + require.NoError(t, err) + + output := d.stdout.String() + assert.Contains(t, output, "description:") + assert.Contains(t, output, "Hello") + assert.Contains(t, output, "World") +} + +func TestRunShow_TemplateOutput_NoDescription(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + + defJSON := `{ + "id": 1, "name": "Test", "path": "\\", "type": "build", + "_links": {}, + "url": "https://dev.azure.com/myorg/test/_apis/pipelines/definitions/1", + "quality": "definition" + }` + d.setupGetDefinition(defFromJSON(t, defJSON), nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/1"} + err := runShow(d.cmd, opts) + require.NoError(t, err) + + output := d.stdout.String() + assert.NotContains(t, output, "description:") +} + +func TestRunShow_TemplateOutput_ProcessAndRepository_Nested(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + d.setupGetDefinition(sampleDef(t), nil) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/42"} + err := runShow(d.cmd, opts) + require.NoError(t, err) + + output := d.stdout.String() + assert.Contains(t, output, "process:") + assert.Contains(t, output, "repository:") + assert.Contains(t, output, "MyRepo") + assert.Contains(t, output, "Azure Pipelines") +} + +func TestRunShow_JSONOutput(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + d.expectGetDefinition("Fabrikam", 42, sampleDef(t), nil) + + cmd := NewCmd(d.cmd) + cmd.SetArgs([]string{"--json", "MyOrg/Fabrikam/42"}) + err := cmd.Execute() + require.NoError(t, err) + + var payload map[string]any + require.NoError(t, json.Unmarshal(d.stdout.Bytes(), &payload)) + assert.Equal(t, float64(42), payload["id"]) + assert.Equal(t, "MyPipeline", payload["name"]) +} + +func TestRunShow_ProjectScopeParsing(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + scope string + org string + wantErr bool + }{ + {name: "three segments", scope: "MyOrg/Fabrikam/42", org: "MyOrg", wantErr: false}, + {name: "two segments", scope: "Fabrikam/42", org: "Fabrikam", wantErr: false}, + {name: "empty", scope: "", org: "", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + d := newDependencies(t, tt.org) + + if !tt.wantErr { + d.setupBuildClient() + d.setupGetDefinition(sampleDef(t), nil) + } + + opts := &showOptions{scopeArg: tt.scope} + err := runShow(d.cmd, opts) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestRunShow_ClientFactoryError(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + cmd := mocks.NewMockCmdContext(ctrl) + clientFact := mocks.NewMockClientFactory(ctrl) + cfg := mocks.NewMockConfig(ctrl) + authCfg := mocks.NewMockAuthConfig(ctrl) + + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(false) + io.SetStderrTTY(false) + + cmd.EXPECT().IOStreams().Return(io, nil).AnyTimes() + cmd.EXPECT().Context().Return(context.Background()).AnyTimes() + cmd.EXPECT().ClientFactory().Return(clientFact).AnyTimes() + cmd.EXPECT().Config().Return(cfg, nil).AnyTimes() + cfg.EXPECT().Authentication().Return(authCfg).AnyTimes() + authCfg.EXPECT().GetDefaultOrganization().Return("MyOrg", nil).AnyTimes() + + clientFact.EXPECT().Build(gomock.Any(), gomock.Any()).Return(nil, assert.AnError).Times(1) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/42"} + err := runShow(cmd, opts) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create Build client") +} + +func TestRunShow_APIFetchError(t *testing.T) { + t.Parallel() + d := newDependencies(t, "MyOrg") + d.setupBuildClient() + d.setupGetDefinition(nil, fmt.Errorf("API returned status 500")) + + opts := &showOptions{scopeArg: "MyOrg/Fabrikam/42"} + err := runShow(d.cmd, opts) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to fetch pipeline definition") +} + +func TestRunShow_OrganizationFromConfigDefault(t *testing.T) { + t.Parallel() + d := newDependencies(t, "DefaultOrg") + d.setupBuildClient() + d.setupGetDefinition(sampleDef(t), nil) + + opts := &showOptions{scopeArg: "Fabrikam/42"} + err := runShow(d.cmd, opts) + require.NoError(t, err) + + output := d.stdout.String() + assert.Contains(t, output, "MyPipeline") +} From df08689247a23701f0fff5f37e71dafba1ce546b Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Mon, 15 Jun 2026 12:08:18 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(pipelines):=20=F0=9F=93=84=20add=20doc?= =?UTF-8?q?umentation=20for=20pipelines=20show=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/azdo_help_reference.md | 16 +++++++++++ docs/azdo_pipelines.md | 1 + docs/azdo_pipelines_show.md | 57 +++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 docs/azdo_pipelines_show.md diff --git a/docs/azdo_help_reference.md b/docs/azdo_help_reference.md index 08852c1a..f4468280 100644 --- a/docs/azdo_help_reference.md +++ b/docs/azdo_help_reference.md @@ -385,6 +385,22 @@ Aliases view, status ``` +### `azdo pipelines show [ORGANIZATION/]PROJECT/PIPELINE [flags]` + +Show details of a pipeline definition + +``` +-q, --jq expression Filter JSON output using a jq expression + --json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it. +-t, --template string Format JSON output using a Go template; see "azdo help formatting" +``` + +Aliases + +``` +view, status +``` + ### `azdo pipelines variable-group` Manage Azure DevOps variable groups diff --git a/docs/azdo_pipelines.md b/docs/azdo_pipelines.md index 88bd51e8..c37ba313 100644 --- a/docs/azdo_pipelines.md +++ b/docs/azdo_pipelines.md @@ -8,6 +8,7 @@ Manage Azure DevOps pipelines * [azdo pipelines list](./azdo_pipelines_list.md) * [azdo pipelines pool](./azdo_pipelines_pool.md) * [azdo pipelines runs](./azdo_pipelines_runs.md) +* [azdo pipelines show](./azdo_pipelines_show.md) * [azdo pipelines variable-group](./azdo_pipelines_variable-group.md) ### ALIASES diff --git a/docs/azdo_pipelines_show.md b/docs/azdo_pipelines_show.md new file mode 100644 index 00000000..de1b15a6 --- /dev/null +++ b/docs/azdo_pipelines_show.md @@ -0,0 +1,57 @@ +## Command `azdo pipelines show` + +``` +azdo pipelines show [ORGANIZATION/]PROJECT/PIPELINE [flags] +``` + +Display the details of a single Azure Pipelines definition. + +The pipeline may be specified by ID (integer) or name (string). +When the organization segment is omitted the default organization +from configuration is used. + + +### Options + + +* `-q`, `--jq` `expression` + + Filter JSON output using a jq expression + +* `--json` `fields` + + Output JSON with the specified fields. Prefix a field with '-' to exclude it. + +* `-t`, `--template` `string` + + Format JSON output using a Go template; see "azdo help formatting" + + +### ALIASES + +- `view` +- `status` + +### JSON Fields + +`_links`, `authoredBy`, `createdDate`, `description`, `id`, `name`, `path`, `process`, `quality`, `queue`, `repository`, `revision`, `type`, `url` + +### Examples + +```bash +# Show a pipeline by ID using the default organization +azdo pipelines show Fabrikam/42 + +# Show a pipeline by name +azdo pipelines show Fabrikam/My Pipeline + +# Show with explicit organization +azdo pipelines show MyOrg/Fabrikam/42 + +# Export as JSON +azdo pipelines show Fabrikam/42 --json id,name,revision +``` + +### See also + +* [azdo pipelines](./azdo_pipelines.md)