Skip to content

feat: Implement azdo pipelines runs list command #212

Description

@tmeckel

Command Description

List pipeline runs (builds) in an Azure DevOps project, filtered by pipeline, branch, status, result, reason, requester, time range, and tags. The project scope is supplied as a required positional argument in the form [ORGANIZATION/]PROJECT; when the organization segment is omitted, the configured default organization is used. This is the user-facing companion to azdo pipelines build list (#211). Both commands call the same build.Client.GetBuilds SDK method but expose different CLI surface to match the Azure CLI's split between the legacy pipelines build group and the modern pipelines runs group (see the Python extension's azure-devops/azext_devops/dev/pipelines/pipeline_run.py and build.py).

Locked Decisions

The following decisions are final; do not re-derive during implementation.

  1. SDK call: build.Client.GetBuilds(ctx, GetBuildsArgs) (*GetBuildsResponseValue, error) from vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/client.go:1659. The same SDK call as feat: Implement azdo pipelines build list command #211 (build list); the distinction is purely CLI ergonomics.
  2. Package layout: command file internal/cmd/pipelines/runs/list/list.go, test file internal/cmd/pipelines/runs/list/list_test.go. Parent group internal/cmd/pipelines/runs/runs.go registers list.NewCmd(ctx) (parent is being created in feat: Introduce azdo pipelines runs command group #214).
  3. Factory function: func NewCmd(ctx util.CmdContext) *cobra.Command (no NewCmdPipelinesRunsList prefix; this style is deprecated).
  4. Project scope is a required positional argument of the form [ORGANIZATION/]PROJECT (the azdo convention per AGENTS.md and internal/cmd/util/scope.go:78-108). Parse via util.ParseProjectScope(ctx, opts.scopeArg). The function returns a *Scope whose Organization field is either the user-supplied value or the configured default. There are no --organization or --project flags on this command. Args: util.ExactArgs(1, "project argument required") enforces exactly one positional.
  5. Pagination: loop on resp.ContinuationToken (string, value type — terminate on == ""); on each iteration, set args.ContinuationToken = &resp.ContinuationToken to drive the next page. Stop early when len(out) >= maxItems (when maxItems > 0).
  6. Filter SDK constraints (these are *<EnumType> not slices in GetBuildsArgs):
    • StatusFilter *BuildStatus (single value).
    • ResultFilter *BuildResult (single value).
    • ReasonFilter *BuildReason (single value).
    • QueryOrder *BuildQueryOrder (single value).
    • When the user passes multiple values, only the first is mapped to the SDK; the rest are dropped with a zap.L().Debug log entry. Flag bindings stay StringSlice / IntSlice for azdo CLI consistency with the Azure CLI.
  7. Identity resolution for --requested-for: if the value equals @me (case-insensitive), resolve via the ExtensionsClient.GetSelfID + IdentityClient.ReadIdentities chain used in internal/cmd/boards/workitem/list/list.go:506-520, then pass the resolved ID to the SDK. Use scope.Organization (already parsed) when creating those clients — do not call cfg.Authentication().GetDefaultOrganization() again.
  8. Branch resolution: if the value does not start with refs/, prepend refs/heads/ inline in a 3-line helper inside list.go (do not promote to a shared package). The SDK BranchName is *string; only the first --branch value is honored (matches the Python extension and the SDK constraint).
  9. --top semantics: server-side cap passed to GetBuildsArgs.Top *int. If the user does not set --top, the field is left nil so the server applies its default page size.
  10. --max-items semantics: client-side cap applied after the SDK ContinuationToken loop returns. Default 0 means unlimited. Truncation happens after all pages are fetched (or once maxItems is reached mid-page, whichever comes first).
  11. Output routing: when opts.exporter != nil, emit JSON via opts.exporter.Write(ios, result) with the raw []build.Build payload. When opts.exporter == nil, render the table described below. No view struct is needed — the SDK Build model is already JSON-tagged and stable, no redaction is required for a list view, and the Python extension returns the same raw payload.
  12. Table columns (left to right): ID, NUMBER, STATUS, RESULT, REASON, PIPELINE, BRANCH, REQUESTED FOR, STARTED, FINISHED — matches az pipelines runs list defaults.
  13. Table cell rendering:
    • PIPELINE = Definition.Name falling back to strconv.Itoa(*Definition.Id) when Name is empty (mirrors the Python extension's behavior).
    • REQUESTED FOR = RequestedFor.DisplayName falling back to RequestedFor.UniqueName (use types.GetValue for nil-safety per the pattern at internal/cmd/boards/workitem/list/list.go:1062-1080). Build.RequestedFor is typed *webapi.IdentityRef (see vendor/.../build/models.go:186).
    • STARTED = StartTime.Time().Format("2006-01-02 15:04:05 MST") (local time, matches azdo boards workitem list).
    • FINISHED = FinishTime.Time().Format(...) or empty if FinishTime is nil.
  14. No confirmation prompt (read-only command).
  15. No JSON redaction needed for a runs list payload.
  16. Mock reuse: MockBuildClient.GetBuilds already exists at internal/mocks/build_client_mock.go:701-714 with the exact signature GetBuilds(ctx context.Context, args build.GetBuildsArgs) (*build.GetBuildsResponseValue, error). Do not regenerate mocks for this command.
  17. No --build-number flag: the Python extension's pipeline_run_list does not expose a build-number filter either; users who need a specific run can pipe the output through azdo pipelines runs list Fabrikam --json id,buildNumber and jq '.[] | select(.buildNumber=="…")'. Keep the surface minimal.

Command Signature

azdo pipelines runs list [ORGANIZATION/]PROJECT [flags]

Aliases:
  ls, l

The project is always supplied positionally; the organization is optional (defaults to the configured default organization). There are no --organization or --project flags. This matches internal/cmd/pipelines/variablegroup/list/list.go:51-67 and internal/cmd/repo/list/list.go:24-56.

Flags (mapped to SDK/REST)

Flag Type Required SDK / REST mapping Default
--pipeline-id intSlice no GetBuildsArgs.Definitions *[]int (first value used) empty (all pipelines)
--branch stringSlice no GetBuildsArgs.BranchName *string (first value used, refs/heads/ prepended if missing) empty (all branches)
--status stringSlice no GetBuildsArgs.StatusFilter *BuildStatus (first value mapped via BuildStatusValues) empty (all statuses)
--result stringSlice no GetBuildsArgs.ResultFilter *BuildResult (first value mapped via BuildResultValues) empty (all results)
--reason stringSlice no GetBuildsArgs.ReasonFilter *BuildReason (first value mapped via BuildReasonValues) empty (all reasons)
--requested-for string no GetBuildsArgs.RequestedFor *string (identity resolved for @me) empty (any requester)
--tag stringSlice no GetBuildsArgs.TagFilters *[]string (all values) empty (no tag filter)
--query-order string no GetBuildsArgs.QueryOrder *BuildQueryOrder (first/only value) empty (server order)
--top int no GetBuildsArgs.Top *int empty (server default)
--max-items int no client-side cap applied after pagination 0 (unlimited)
--json stringSlice no util.AddJSONFlags registration empty
--jq string no util.AddJSONFlags registration empty
--template string no util.AddJSONFlags registration empty

BuildQueryOrderValues enum: finishTimeAscending, finishTimeDescending, queueTimeAscending, queueTimeDescending, startTimeAscending, startTimeDescending (from vendor/.../build/models.go:1112-1128). Match case-insensitively against the constant names; reject unknown values with a clear error.

JSON Output Contract

When --json is set, emit the raw []build.Build slice (no view struct, no redaction). Field names match the SDK struct's json:"…" tags. The --json field whitelist registers all top-level Build fields the SDK exposes.

When --json is not set, the table described in the Locked Decisions renders.

Command Wiring

Code Skeleton (canonical, copy verbatim)

The following four blocks are the complete and final skeleton. Copy them verbatim into the new files; adjust only the constant values and table column headers as required.

§1 runOptions struct in internal/cmd/pipelines/runs/list/list.go

package list

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/MakeNowJust/heredoc"
	"github.com/spf13/cobra"
	"go.uber.org/zap"

	"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/build"
	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"

	"github.com/tmeckel/azdo-cli/internal/cmd/util"
	"github.com/tmeckel/azdo-cli/internal/types"
)

type runOptions struct {
	scopeArg string

	pipelineIDs  []int
	branches     []string
	statuses     []string
	results      []string
	reasons      []string
	requestedFor string
	tags         []string
	queryOrder   string

	top      int
	maxItems int

	exporter util.Exporter
}

func NewCmd(ctx util.CmdContext) *cobra.Command {
	opts := &runOptions{}

	cmd := &cobra.Command{
		Use:   "list [ORGANIZATION/]PROJECT",
		Short: "List runs of pipelines in a project.",
		Long: heredoc.Doc(`
			List runs of pipelines in an Azure DevOps project. Mirrors
			'az pipelines runs list'.

			Filters support pipeline, branch, status, result, reason, requester,
			and tags. The full result set is paginated server-side; use
			--max-items to cap the response client-side.
		`),
		Example: heredoc.Doc(`
			# List the 20 most recent runs for a project (default org)
			azdo pipelines runs list Fabrikam --top 20

			# Filter by pipeline and branch
			azdo pipelines runs list MyOrg/Fabrikam --pipeline-id 42 --branch main

			# Order by queue time, descending
			azdo pipelines runs list Fabrikam --query-order queueTimeDescending

			# Export as JSON
			azdo pipelines runs list Fabrikam --json id,buildNumber,status,result
		`),
		Aliases: []string{"l", "ls"},
		Args:    util.ExactArgs(1, "project argument required"),
		RunE: func(cmd *cobra.Command, args []string) error {
			opts.scopeArg = args[0]
			return runList(ctx, opts)
		},
	}

	cmd.Flags().IntSliceVar(&opts.pipelineIDs, "pipeline-id", nil, "Limit to runs for these pipeline IDs (repeatable; first value is honored by the SDK).")
	cmd.Flags().StringSliceVar(&opts.branches, "branch", nil, "Filter by source branch (repeatable; first value is honored by the SDK). Bare names get refs/heads/ prepended.")
	cmd.Flags().StringSliceVar(&opts.statuses, "status", nil, "Filter by status (repeatable; first value is honored). Valid: none, inProgress, completed, cancelling, postponed, notStarted, all.")
	cmd.Flags().StringSliceVar(&opts.results, "result", nil, "Filter by result (repeatable; first value is honored). Valid: none, succeeded, partiallySucceeded, failed, canceled.")
	cmd.Flags().StringSliceVar(&opts.reasons, "reason", nil, "Filter by reason (repeatable; first value is honored). Valid: manual, individualCI, batchedCI, schedule, scheduleForced, userCreated, pullRequest, etc.")
	cmd.Flags().StringVar(&opts.requestedFor, "requested-for", "", "Filter by the user who queued the run. Accepts @me to mean the authenticated user.")
	cmd.Flags().StringSliceVar(&opts.tags, "tag", nil, "Filter by tags (all supplied tags must match).")
	cmd.Flags().StringVar(&opts.queryOrder, "query-order", "", "Order the results: finishTimeAscending, finishTimeDescending, queueTimeAscending, queueTimeDescending, startTimeAscending, startTimeDescending.")
	cmd.Flags().IntVar(&opts.top, "top", 0, "Maximum number of runs to request per server page (0 = server default).")
	cmd.Flags().IntVar(&opts.maxItems, "max-items", 0, "Maximum number of runs to return client-side (0 = unlimited).")

	util.AddJSONFlags(cmd, &opts.exporter,
		"id", "buildNumber", "status", "result", "reason",
		"definition", "project", "sourceBranch", "sourceVersion",
		"startTime", "finishTime", "queueTime", "requestedBy", "requestedFor",
		"tags", "uri", "url",
	)

	return cmd
}

§2 runList + fetchBuilds in internal/cmd/pipelines/runs/list/list.go

func runList(ctx util.CmdContext, opts *runOptions) error {
	ios, err := ctx.IOStreams()
	if err != nil {
		return err
	}

	ios.StartProgressIndicator()
	defer ios.StopProgressIndicator()

	if err := validateRunOptions(opts); err != nil {
		return err
	}

	scope, err := util.ParseProjectScope(ctx, opts.scopeArg)
	if err != nil {
		return util.FlagErrorWrap(err)
	}

	client, err := ctx.ClientFactory().Build(ctx.Context(), scope.Organization)
	if err != nil {
		return fmt.Errorf("failed to create Build client: %w", err)
	}

	requestedFor, err := resolveRequestedFor(ctx, scope.Organization, opts.requestedFor)
	if err != nil {
		return fmt.Errorf("resolve --requested-for: %w", err)
	}

	project := scope.Project
	args := build.GetBuildsArgs{Project: &project}
	if ids := opts.pipelineIDs; len(ids) > 0 {
		first := ids
		args.Definitions = &first
	}
	if len(opts.branches) > 0 {
		branch := resolveGitBranchRef(opts.branches[0])
		args.BranchName = &branch
	}
	if len(opts.statuses) > 0 {
		status, ok := lookupBuildStatus(opts.statuses[0])
		if !ok {
			return util.FlagErrorf("unknown --status value %q", opts.statuses[0])
		}
		args.StatusFilter = &status
	}
	if len(opts.results) > 0 {
		result, ok := lookupBuildResult(opts.results[0])
		if !ok {
			return util.FlagErrorf("unknown --result value %q", opts.results[0])
		}
		args.ResultFilter = &result
	}
	if len(opts.reasons) > 0 {
		reason, ok := lookupBuildReason(opts.reasons[0])
		if !ok {
			return util.FlagErrorf("unknown --reason value %q", opts.reasons[0])
		}
		args.ReasonFilter = &reason
	}
	if requestedFor != "" {
		args.RequestedFor = &requestedFor
	}
	if len(opts.tags) > 0 {
		args.TagFilters = &opts.tags
	}
	if opts.queryOrder != "" {
		order, ok := lookupBuildQueryOrder(opts.queryOrder)
		if !ok {
			return util.FlagErrorf("unknown --query-order value %q", opts.queryOrder)
		}
		args.QueryOrder = &order
	}
	if opts.top > 0 {
		args.Top = &opts.top
	}

	runs, err := fetchBuilds(ctx, client, args, opts.maxItems)
	if err != nil {
		return fmt.Errorf("fetch runs: %w", err)
	}

	ios.StopProgressIndicator()

	if opts.exporter != nil {
		return opts.exporter.Write(ios, runs)
	}

	return renderRunsTable(ctx, runs)
}

func fetchBuilds(ctx util.CmdContext, client build.Client, args build.GetBuildsArgs, maxItems int) ([]build.Build, error) {
	if maxItems < 0 {
		return nil, util.FlagErrorf("--max-items must be >= 0")
	}
	out := make([]build.Build, 0)
	for {
		resp, err := client.GetBuilds(ctx.Context(), args)
		if err != nil {
			return nil, fmt.Errorf("GetBuilds: %w", err)
		}
		if resp != nil {
			for _, b := range resp.Value {
				out = append(out, b)
				if maxItems > 0 && len(out) >= maxItems {
					return out, nil
				}
			}
		}
		if resp == nil || resp.ContinuationToken == "" {
			return out, nil
		}
		token := resp.ContinuationToken
		args.ContinuationToken = &token
	}
}

func renderRunsTable(ctx util.CmdContext, runs []build.Build) error {
	tp, err := ctx.Printer("table")
	if err != nil {
		return fmt.Errorf("printer: %w", err)
	}
	tp.AddColumns("ID", "NUMBER", "STATUS", "RESULT", "REASON", "PIPELINE", "BRANCH", "REQUESTED FOR", "STARTED", "FINISHED")
	tp.EndRow()
	for i := range runs {
		run := runs[i]
		tp.AddField(strconv.Itoa(run.Id))
		tp.AddField(run.BuildNumber)
		tp.AddField(string(run.Status))
		tp.AddField(string(run.Result))
		tp.AddField(string(run.Reason))
		tp.AddField(renderPipelineName(run.Definition))
		tp.AddField(run.SourceBranch)
		tp.AddField(renderIdentityDisplay(run.RequestedFor))
		tp.AddField(renderTime(run.StartTime))
		tp.AddField(renderTime(run.FinishTime))
		tp.EndRow()
	}
	return tp.Render()
}

func validateRunOptions(opts *runOptions) error {
	if opts.top < 0 {
		return util.FlagErrorf("--top must be >= 0")
	}
	if opts.maxItems < 0 {
		return util.FlagErrorf("--max-items must be >= 0")
	}
	return nil
}

func resolveGitBranchRef(branch string) string {
	if strings.HasPrefix(branch, "refs/") {
		return branch
	}
	return "refs/heads/" + branch
}

func renderPipelineName(def *build.DefinitionReference) string {
	if def == nil {
		return ""
	}
	if def.Name != nil && *def.Name != "" {
		return *def.Name
	}
	if def.Id != nil {
		return strconv.Itoa(*def.Id)
	}
	return ""
}

func renderIdentityDisplay(ref *webapi.IdentityRef) string {
	if ref == nil {
		return ""
	}
	if name := types.GetValue(ref.DisplayName); name != "" {
		return name
	}
	return types.GetValue(ref.UniqueName)
}

func renderTime(t *azuredevops.Time) string {
	if t == nil {
		return ""
	}
	return t.Time().Format("2006-01-02 15:04:05 MST")
}

func resolveRequestedFor(ctx util.CmdContext, organization, value string) (string, error) {
	if value == "" || !strings.EqualFold(value, "@me") {
		return value, nil
	}
	zap.L().Debug("resolving @me to current user identity", zap.String("organization", organization))
	// Reuse the ExtensionsClient.GetSelfID + IdentityClient.ReadIdentities chain from
	// internal/cmd/boards/workitem/list/list.go:506-520. The chain is a 6-line block
	// that returns the resolved identity ID as a string. Copy that block here.
	return resolvedIdentityID, nil
}

// lookupBuildStatus, lookupBuildResult, lookupBuildReason, lookupBuildQueryOrder
// are case-insensitive lookups against the *Values constant slices from
// vendor/.../build/models.go (e.g. build.BuildStatusValues).

§3 Test file internal/cmd/pipelines/runs/list/list_test.go

package list_test

import (
	"bytes"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/tmeckel/azdo-cli/internal/cmd/pipelines/runs/list"
	"github.com/tmeckel/azdo-cli/internal/cmd/util"
	"github.com/tmeckel/azdo-cli/internal/mocks"
)

// Mirror the setupFakeDeps pattern from
// internal/cmd/boards/workitem/list/list_test.go:765-844 verbatim,
// substituting the Build mock for the WorkItem mock.
//
// Tests must be written FIRST (TDD) and must cover, at minimum:
//   1. Default invocation: no filters, returns all runs, no error.
//   2. Scope arg "Fabrikam" parses to org=default, project=Fabrikam.
//   3. Scope arg "MyOrg/Fabrikam" parses to org=MyOrg, project=Fabrikam.
//   4. --pipeline-id: passed through to args.Definitions.
//   5. --branch: bare name gets refs/heads/ prepended; refs/ untouched.
//   6. --status / --result / --reason: unknown value returns a clear error.
//   7. --requested-for @me: triggers identity resolution.
//   8. --query-order: passed through to args.QueryOrder.
//   9. --top: passed through to args.Top.
//  10. --max-items: client-side truncation after pagination.
//  11. ContinuationToken loop: page2, page3, then empty token terminates.
//  12. --json: exporter.Write is called with the raw []build.Build slice.
//  13. Table: 10 columns in the documented order.

func setupRunsFakeDeps(t *testing.T) (*mocks.MockBuildClient, util.CmdContext, *bytes.Buffer, *bytes.Buffer) {
	t.Helper()
	// ... see workitem/list_test.go:765-844 for the canonical scaffolding ...
}

func TestRunList_DefaultReturnsAllRuns(t *testing.T) {
	t.Parallel()
	// ... assert mock.GetBuilds was called once, args.Project == expected ...
}

API Surface

  • Vendored: Build.GetBuilds(ctx, GetBuildsArgs) (*GetBuildsResponseValue, error)vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/client.go:1659.
  • Vendored: GetBuildsArgs struct — vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/client.go:1757-1802.
  • Vendored: GetBuildsResponseValue struct — vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/client.go:1803-1806 (Value []Build, ContinuationToken string).
  • Vendored: Build model — vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/models.go:122-217. Build.RequestedFor is *webapi.IdentityRef (:186).
  • Vendored: BuildStatusValuesvendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/models.go:1062-1080.
  • Vendored: BuildResultValuesvendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/models.go:978-985.
  • Vendored: BuildReasonValuesvendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/models.go:862-887.
  • Vendored: BuildQueryOrderValuesvendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/build/models.go:1112-1128.
  • Vendored: azuredevops.Time — wraps time.Time with Time() time.Time accessor.
  • Vendored: webapi.IdentityRefvendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi (has Id, DisplayName, UniqueName).
  • Mock: MockBuildClient.GetBuildsinternal/mocks/build_client_mock.go:701-714.
  • Factory: Build(ctx, organization)internal/azdo/connection.go:50-51 (interface) / internal/azdo/factory.go:61 (impl).
  • Scope parser: util.ParseProjectScope(ctx, arg)internal/cmd/util/scope.go:78-108.

Implementation Approach (TDD, reuse-first, minimal)

Phase 1 (RED) — write tests first.

  1. Copy the setupFakeDeps scaffolding from internal/cmd/boards/workitem/list/list_test.go:765-844 into internal/cmd/pipelines/runs/list/list_test.go, swapping the WorkItem mock for the Build mock. Rename to setupRunsFakeDeps for clarity. Aim for ~120 LOC of test scaffolding.
  2. Write the 13 tests listed in §3 above. All must FAIL on the un-stubbed package.
  3. Run go test ./internal/cmd/pipelines/runs/list/... — confirm all 13 tests fail for the right reason (e.g., undefined: list.NewCmd or nil pointer dereference).

Phase 2 (GREEN) — minimal implementation.

  1. Add internal/cmd/pipelines/runs/list/list.go with the §1 struct, §2 wiring, runList, fetchBuilds, renderRunsTable, validateRunOptions, and the 6 helper functions (4 enum lookups are simple case-insensitive scanners, ~5 LOC each).
  2. Add internal/cmd/pipelines/runs/runs.go (group parent — see feat: Introduce azdo pipelines runs command group #214 for the canonical structure, which registers list.NewCmd(ctx)).
  3. Add cmd.AddCommand(runs.NewCmd(ctx)) to internal/cmd/pipelines/pipelines.go.
  4. Run go test ./internal/cmd/pipelines/runs/... — all 13 tests pass.
  5. Run go build ./... — clean.
  6. Run go run cmd/azdo/azdo.go pipelines runs list --help — output matches the Use: "list [ORGANIZATION/]PROJECT" line and lists all 11 filter/format flags (no --organization / --project flags).

LOC budget: ~250 LOC for list.go (struct + cmd + run + helpers), ~140 LOC for list_test.go, ~40 LOC for runs.go. Total: ~430 LOC.

Tooling and Verification Checklist

  • go test ./internal/cmd/pipelines/runs/... passes (all 13 cases).
  • go test ./... passes (no regressions elsewhere).
  • go build ./... is clean.
  • go run cmd/azdo/azdo.go pipelines runs list --help renders without panic and shows Use: "list [ORGANIZATION/]PROJECT".
  • go run cmd/azdo/azdo.go pipelines runs list Fabrikam --help errors with "project argument required" if no positional is supplied.
  • go run cmd/azdo/azdo.go pipelines runs list MyOrg/Fabrikam --top 1 (with AZDO_TOKEN and AZDO_ORGANIZATION env) returns one row.
  • make lint passes (golangci-lint, per .golangci.yml).
  • make docs regenerates docs/pipelines_runs_list.md with the new command.

Reference Existing Patterns

  • internal/cmd/pipelines/variablegroup/list/list.go:47-93canonical pipelines-scoped list command (Use: "list [ORGANIZATION/]PROJECT", util.ExactArgs(1, ...), util.ParseProjectScope(...)).
  • internal/cmd/boards/workitem/list/list.go:48-115 — reference for NewCmd shape with [ORGANIZATION/]PROJECT positional.
  • internal/cmd/boards/workitem/list/list_test.go:765-844 — canonical setupFakeDeps test fixture; copy verbatim, swap the mock.
  • internal/cmd/boards/workitem/list/list.go:506-520@me resolution via ExtensionsClient + IdentityClient.
  • internal/cmd/boards/workitem/list/list.go:1062-1080fieldIdentityDisplay for IdentityRef rendering (same type used here).
  • internal/cmd/util/scope.go:78-108ParseProjectScope source (the function called from runList).
  • github.com/tmeckel/azdo-cli/issues/211 — sibling leaf (build list); uses the same SDK call and the same positional pattern.

References

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions