Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,36 @@ Aliases
view, status
```

### `azdo pipelines build`

Manage classic builds

#### `azdo pipelines build list [ORGANIZATION/]PROJECT [flags]`

List classic build results in a project.

```
--branch string Limit to builds for this branch. Bare names get refs/heads/ prepended.
--build-number string Limit to builds that match this build number. Append * for prefix search.
--definition-id ints Limit to builds for these definition IDs (repeatable)
-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.
--max-items int Maximum number of builds to return across all pages (client-side; 0 = unlimited)
--reason string Limit to builds with this reason: {all|batchedci|buildcompletion|checkinshelveset|individualci|manual|none|pullrequest|resourcetrigger|schedule|scheduleforced|triggered|usercreated|validateshelveset}
--requested-for string Limit to builds requested for this user or group; supports @me
--result string Limit to builds with this result: {canceled|failed|none|partiallysucceeded|succeeded}
--status string Limit to builds with this status: {all|cancelling|completed|inprogress|none|notstarted|postponed}
--tag strings Limit to builds that have all of the specified tags (repeatable)
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
--top int Maximum number of builds to return per page (server-side; 0 = server default)
```

Aliases

```
ls, l
```

### `azdo pipelines delete [ORGANIZATION/]PROJECT/PIPELINE [flags]`

Delete a pipeline definition
Expand Down
1 change: 1 addition & 0 deletions docs/azdo_pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Manage Azure DevOps pipelines
### Available commands

* [azdo pipelines agent](./azdo_pipelines_agent.md)
* [azdo pipelines build](./azdo_pipelines_build.md)
* [azdo pipelines delete](./azdo_pipelines_delete.md)
* [azdo pipelines list](./azdo_pipelines_list.md)
* [azdo pipelines pool](./azdo_pipelines_pool.md)
Expand Down
20 changes: 20 additions & 0 deletions docs/azdo_pipelines_build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Command `azdo pipelines build`

Manage classic Azure Pipelines builds (Build v1).
For modern Pipelines runs, see 'azdo pipelines runs'.


### Available commands

* [azdo pipelines build list](./azdo_pipelines_build_list.md)

### Examples

```bash
# List builds in a project
azdo pipelines build list Fabrikam
```

### See also

* [azdo pipelines](./azdo_pipelines.md)
92 changes: 92 additions & 0 deletions docs/azdo_pipelines_build_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
## Command `azdo pipelines build list`

```
azdo pipelines build list [ORGANIZATION/]PROJECT [flags]
```

List classic build (Build v1) records in a project. Supports filter,
pagination, and JSON export. For the modern Pipelines runs surface,
see 'azdo pipelines runs list'.


### Options


* `--branch` `string`

Limit to builds for this branch. Bare names get refs/heads/ prepended.

* `--build-number` `string`

Limit to builds that match this build number. Append * for prefix search.

* `--definition-id` `ints`

Limit to builds for these definition IDs (repeatable)

* `-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.

* `--max-items` `int` (default `0`)

Maximum number of builds to return across all pages (client-side; 0 = unlimited)

* `--reason` `string`

Limit to builds with this reason: {all|batchedci|buildcompletion|checkinshelveset|individualci|manual|none|pullrequest|resourcetrigger|schedule|scheduleforced|triggered|usercreated|validateshelveset}

* `--requested-for` `string`

Limit to builds requested for this user or group; supports @me

* `--result` `string`

Limit to builds with this result: {canceled|failed|none|partiallysucceeded|succeeded}

* `--status` `string`

Limit to builds with this status: {all|cancelling|completed|inprogress|none|notstarted|postponed}

* `--tag` `strings`

Limit to builds that have all of the specified tags (repeatable)

* `-t`, `--template` `string`

Format JSON output using a Go template; see "azdo help formatting"

* `--top` `int` (default `0`)

Maximum number of builds to return per page (server-side; 0 = server default)


### ALIASES

- `ls`
- `l`

### JSON Fields

`buildNumber`, `definition`, `finishTime`, `id`, `project`, `queueTime`, `reason`, `requestedBy`, `requestedFor`, `result`, `sourceBranch`, `sourceVersion`, `startTime`, `status`, `tags`, `uri`, `url`

### Examples

```bash
# List the 20 most recent builds for a project
azdo pipelines build list Fabrikam --top 20

# Filter by branch, status, and tag
azdo pipelines build list Fabrikam --branch main --status completed --tag release

# Export as JSON
azdo pipelines build list Fabrikam --json id,buildNumber,status,result
```

### See also

* [azdo pipelines build](./azdo_pipelines_build.md)
3 changes: 3 additions & 0 deletions internal/azdo/extensions/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ type Client interface {
// Inputs that cannot be resolved are absent from the result map (keyed by the trimmed input string).
// The map naturally deduplicates repeated inputs; only catastrophic failures (e.g. client creation) return an error.
ResolveSubjects(ctx context.Context, members []string) (map[string]*graph.GraphSubject, error)
// ResolveIdentity resolves a member identifier (SID, descriptor, email, display name, or account name).
ResolveIdentity(ctx context.Context, member string) (*identity.Identity, error)
// ResolveCurrentIdentity retrieves the identity record for the authenticated user.
ResolveCurrentIdentity(ctx context.Context) (*identity.Identity, error)
}

type extensionClient struct {
Expand Down
22 changes: 22 additions & 0 deletions internal/azdo/extensions/member_lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,28 @@ func resolveSearchBatch(ctx context.Context, identityClient identity.Client, gra
return nil
}

func (c *extensionClient) ResolveCurrentIdentity(ctx context.Context) (*identity.Identity, error) {
selfID, err := c.GetSelfID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to resolve @me identity: %w", err)
}

identityClient, err := identity.NewClient(ctx, c.conn)
if err != nil {
return nil, fmt.Errorf("failed to create Identity client: %w", err)
}

idStr := selfID.String()
identities, err := identityClient.ReadIdentities(ctx, identity.ReadIdentitiesArgs{IdentityIds: &idStr})
if err != nil {
return nil, fmt.Errorf("failed to resolve @me identity details: %w", err)
}
if identities == nil || len(*identities) != 1 {
return nil, fmt.Errorf("failed to resolve @me identity details")
}
return &(*identities)[0], nil
}

func (c *extensionClient) ResolveIdentity(ctx context.Context, member string) (*identity.Identity, error) {
member = strings.TrimSpace(member)
if member == "" {
Expand Down
43 changes: 6 additions & 37 deletions internal/cmd/boards/workitem/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,19 +480,12 @@ func resolveAssignedToFilter(ctx util.CmdContext, organization string, assignedT
}

var extensionsClient extensions.Client
var identityClient identity.Client
if needsLookup {
ext, err := ctx.ClientFactory().Extensions(ctx.Context(), organization)
if err != nil {
return nil, fmt.Errorf("failed to create Extensions client: %w", err)
}
extensionsClient = ext

idc, err := ctx.ClientFactory().Identity(ctx.Context(), organization)
if err != nil {
return nil, fmt.Errorf("failed to create Identity client: %w", err)
}
identityClient = idc
}

//nolint:dupl // intentional duplicate of resolveCreatedByFilter loop
Expand All @@ -504,21 +497,11 @@ func resolveAssignedToFilter(ctx util.CmdContext, organization string, assignedT
}

if strings.EqualFold(raw, "@me") {
selfID, err := extensionsClient.GetSelfID(ctx.Context())
if err != nil {
return nil, fmt.Errorf("failed to resolve @me identity: %w", err)
}
identityIds := selfID.String()
identities, err := identityClient.ReadIdentities(ctx.Context(), identity.ReadIdentitiesArgs{
IdentityIds: &identityIds,
})
ident, err := extensionsClient.ResolveCurrentIdentity(ctx.Context())
if err != nil {
return nil, fmt.Errorf("failed to resolve @me identity details: %w", err)
return nil, err
}
if identities == nil || len(*identities) != 1 {
return nil, fmt.Errorf("failed to resolve @me identity details")
}
value := identityAccountOrDisplay((*identities)[0])
value := identityAccountOrDisplay(*ident)
if value == "" {
return nil, fmt.Errorf("authenticated identity is missing account or display name")
}
Expand Down Expand Up @@ -568,18 +551,12 @@ func resolveCreatedByFilter(ctx util.CmdContext, organization string, createdBy
}
}
var extensionsClient extensions.Client
var identityClient identity.Client
if needsLookup {
ext, err := ctx.ClientFactory().Extensions(ctx.Context(), organization)
if err != nil {
return nil, fmt.Errorf("failed to create Extensions client: %w", err)
}
extensionsClient = ext
idc, err := ctx.ClientFactory().Identity(ctx.Context(), organization)
if err != nil {
return nil, fmt.Errorf("failed to create Identity client: %w", err)
}
identityClient = idc
}
resolved := make([]string, 0, len(createdBy))
for _, raw := range createdBy {
Expand All @@ -588,19 +565,11 @@ func resolveCreatedByFilter(ctx util.CmdContext, organization string, createdBy
continue
}
if strings.EqualFold(raw, "@me") {
selfID, err := extensionsClient.GetSelfID(ctx.Context())
ident, err := extensionsClient.ResolveCurrentIdentity(ctx.Context())
if err != nil {
return nil, fmt.Errorf("failed to resolve @me identity: %w", err)
}
identityIds := selfID.String()
identities, err := identityClient.ReadIdentities(ctx.Context(), identity.ReadIdentitiesArgs{IdentityIds: &identityIds})
if err != nil {
return nil, fmt.Errorf("failed to resolve @me identity details: %w", err)
}
if identities == nil || len(*identities) != 1 {
return nil, fmt.Errorf("failed to resolve @me identity details")
return nil, err
}
value := identityAccountOrDisplay((*identities)[0])
value := identityAccountOrDisplay(*ident)
if value == "" {
return nil, fmt.Errorf("authenticated identity is missing account or display name")
}
Expand Down
41 changes: 11 additions & 30 deletions internal/cmd/boards/workitem/list/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"testing"
"time"

"github.com/google/uuid"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/identity"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -114,8 +113,7 @@ func TestRunList_SortInvalidField(t *testing.T) {
t.Parallel()
ios, _, _, _ := iostreams.Test()
deps := &fakeListDeps{
cmd: mocks.NewMockCmdContext(ctrlFromT(t)),
stdout: &bytes.Buffer{},
cmd: mocks.NewMockCmdContext(ctrlFromT(t)),
}
deps.cmd.EXPECT().IOStreams().Return(ios, nil).AnyTimes()

Expand Down Expand Up @@ -188,8 +186,7 @@ func TestRunList_InvalidDateFlag(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
deps := &fakeListDeps{
cmd: mocks.NewMockCmdContext(ctrl),
stdout: &bytes.Buffer{},
cmd: mocks.NewMockCmdContext(ctrl),
}
deps.cmd.EXPECT().IOStreams().Return(ios, nil).AnyTimes()

Expand Down Expand Up @@ -259,21 +256,14 @@ func TestRunList_CreatedByMe(t *testing.T) {
deps := setupFakeDeps(t, "org")
stubDefaultOpenTypes(deps)
stubBatch(t, deps, false)

selfID := uuid.New()
deps.clientFact.EXPECT().Extensions(gomock.Any(), "org").Return(deps.ext, nil)
deps.ext.EXPECT().GetSelfID(gomock.Any()).Return(selfID, nil)
deps.clientFact.EXPECT().Identity(gomock.Any(), "org").Return(deps.ident, nil)
deps.ident.EXPECT().ReadIdentities(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, args identity.ReadIdentitiesArgs) (*[]identity.Identity, error) {
require.NotNil(t, args.IdentityIds)
assert.Equal(t, selfID.String(), *args.IdentityIds)
id := identity.Identity{
deps.ext.EXPECT().ResolveCurrentIdentity(gomock.Any()).DoAndReturn(
func(_ context.Context) (*identity.Identity, error) {
return &identity.Identity{
Properties: map[string]any{
"Account": map[string]any{"$value": "Alice <alice@x.com>"},
},
}
return &[]identity.Identity{id}, nil
}, nil
},
)

Expand Down Expand Up @@ -403,7 +393,7 @@ func TestRunList_StatusAndStateIntersect(t *testing.T) {
})
require.NoError(t, err)
// We expect the category predicate (e.g. from "New","Active","Proposed","InProgress")
// ANDed with the state predicate, both inside the state segment.
// ANDead with the state predicate, both inside the state segment.
// The exact form: ( [System.State] IN ('New','Active','Proposed','InProgress') ) AND ( [System.State] IN ('Active') )
assert.Contains(t, captured, ") AND ([System.State] IN ('Active')")
}
Expand Down Expand Up @@ -977,24 +967,15 @@ func TestRunList_AssignedToMeResolvesIdentity(t *testing.T) {
t.Parallel()

deps := setupFakeDeps(t, "org")

selfID := uuid.MustParse("11111111-1111-1111-1111-111111111111")
deps.clientFact.EXPECT().Extensions(gomock.Any(), "org").Return(deps.ext, nil)
deps.clientFact.EXPECT().Identity(gomock.Any(), "org").Return(deps.ident, nil)
deps.ext.EXPECT().GetSelfID(gomock.Any()).Return(selfID, nil)

idsArg := selfID.String()
deps.ident.EXPECT().ReadIdentities(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, args identity.ReadIdentitiesArgs) (*[]identity.Identity, error) {
require.NotNil(t, args.IdentityIds)
assert.Equal(t, idsArg, *args.IdentityIds)
deps.ext.EXPECT().ResolveCurrentIdentity(gomock.Any()).DoAndReturn(
func(_ context.Context) (*identity.Identity, error) {
account := "Account.From.Properties"
display := "Self User"
out := []identity.Identity{{
return &identity.Identity{
Properties: map[string]any{"Account": map[string]any{"$value": account}},
ProviderDisplayName: &display,
}}
return &out, nil
}, nil
},
)

Expand Down
Loading
Loading