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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Developer: # Repository Guidelines
- **CLI Flags:** Use kebab-case (e.g., `--organization-url`).
- **Logging:** Use `zap.L()` with structured messages; prefer `%w` for wrapping errors.
- **Variables:** Variable names must never collide with any imports or name of GO packages
- **Indentation During Drafts:** Cosmetic indentation mismatches are acceptable while implementing changes. Final formatting is applied with `gofumpt` after coding is complete, so focus on correctness first.

## Testing Guidelines

Expand Down Expand Up @@ -61,6 +62,19 @@ For a complete guidance on how to implement tests refer to [TESTING.md](./TESTIN
- **Output:** Use `ctx.Printer(format)` and repository’s standard `printer` helpers to format output in table or JSON, following patterns from existing commands. For commands that make API calls, always wrap the logic with `ios.StartProgressIndicator()` and `defer ios.StopProgressIndicator()` to provide feedback to the user. When not outputting JSON, provide a clean, human-readable table using the `ctx.Printer("list")` helper for list-like output. Choose the most relevant columns to display. Always call `ios.StopProgressIndicator()` **before** creating any output on the command line.
- **Testing:** Create mocks for the relevant client interface methods under `internal/mocks` and write hermetic, table-driven tests alongside the command.

### Handling Missing Azure DevOps SDK Clients

When a required Azure DevOps client is not available in the vendored Go SDK:

1. Confirm the absence by searching the upstream SDK using the GitHub MCP server (`https://github.com/microsoft/azure-devops-go-api/blob/dev/azuredevops/v7`).
2. Extend `type ClientFactory interface` in `internal/azdo/connection.go` with the new client method signature.
3. Ask the user to run `go mod tidy` followed by `go mod vendor` after the interface additions (the sandbox cannot do this automatically).
4. Add a matching mock generation entry to `scripts/generate_mocks.sh`.
5. Let the user run `bash ./scripts/generate_mocks.sh`
6. Implement the factory method in `internal/azdo/factory.go` so the new client can be constructed via existing connection plumbing.

Do not hand-roll HTTP calls if an SDK client can be introduced through this process.

### Implementing Commands with JSON and Table/Plain Output

- **JSON and Table/Plain output are handled via separate code paths.**
Expand Down
1 change: 1 addition & 0 deletions docs/azdo_boards.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Work with Azure Boards resources.
### Available commands

* [azdo boards area](./azdo_boards_area.md)
* [azdo boards iteration](./azdo_boards_iteration.md)

### ALIASES

Expand Down
23 changes: 23 additions & 0 deletions docs/azdo_boards_iteration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Command `azdo boards iteration`

Work with iteration/classification nodes.

### Available commands

* [azdo boards iteration project](./azdo_boards_iteration_project.md)

### ALIASES

- `it`
- `i`

### Examples

```bash
# List iterations for a project
azdo boards iteration project list Fabrikam
```

### See also

* [azdo boards](./azdo_boards.md)
23 changes: 23 additions & 0 deletions docs/azdo_boards_iteration_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Command `azdo boards iteration project`

Project-scoped iteration commands.

### Available commands

* [azdo boards iteration project list](./azdo_boards_iteration_project_list.md)

### ALIASES

- `prj`
- `p`

### Examples

```bash
# List iterations for a project
azdo boards iteration project list Fabrikam
```

### See also

* [azdo boards iteration](./azdo_boards_iteration.md)
79 changes: 79 additions & 0 deletions docs/azdo_boards_iteration_project_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## Command `azdo boards iteration project list`

```
azdo boards iteration project list [ORGANIZATION/]PROJECT [flags]
```

List the iteration (sprint) hierarchy for a project within an Azure DevOps organization.


### Options


* `-d`, `--depth` `int` (default `3`)

Depth to fetch (1-10)

* `--finish-date` `string`

Apply a comparison filter to iteration finish dates; supports operators like <= and special value "today" (e.g., "<=today")

* `--include-dates`

Include iteration start and finish dates

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

* `-p`, `--path` `string`

Iteration path relative to project root

* `--start-date` `string`

Apply a comparison filter to iteration start dates; supports operators like >= and special value "today" (e.g., ">=today")

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

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


### ALIASES

- `ls`
- `l`

### JSON Fields

`finishDate`, `hasChildren`, `level`, `name`, `path`, `startDate`

### Examples

```bash
# List the top-level iterations (depth 3)
azdo boards iteration project list myorg/myproject

# List from a specific path
azdo boards iteration project list myproject --path "Release 2025/Sprint 1"

# Include start and finish dates
azdo boards iteration project list myproject --include-dates

# List iterations starting today or later
azdo boards iteration project list myproject --start-date ">=today"

# Filter to iterations finishing before a specific date
azdo boards iteration project list myproject --finish-date "<=2024-12-31"

# Export JSON
azdo boards iteration project list myproject --json name,path,startDate
```

### See also

* [azdo boards iteration project](./azdo_boards_iteration_project.md)
41 changes: 41 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,47 @@ Aliases
ls, l
```

### `azdo boards iteration <command>`

Work with iteration/classification nodes.

Aliases

```
it, i
```

#### `azdo boards iteration project <command>`

Project-scoped iteration commands.

Aliases

```
prj, p
```

##### `azdo boards iteration project list [ORGANIZATION/]PROJECT [flags]`

List iteration hierarchy for a project.

```
-d, --depth int Depth to fetch (1-10) (default 3)
--finish-date string Apply a comparison filter to iteration finish dates; supports operators like <= and special value "today" (e.g., "<=today")
--include-dates Include iteration start and finish dates
-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.
-p, --path string Iteration path relative to project root
--start-date string Apply a comparison filter to iteration start dates; supports operators like >= and special value "today" (e.g., ">=today")
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
```

Aliases

```
ls, l
```

## `azdo co`

Alias for "pr checkout"
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
2 changes: 2 additions & 0 deletions internal/azdo/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/operations"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/security"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
"github.com/tmeckel/azdo-cli/internal/azdo/extensions"
)

Expand Down Expand Up @@ -52,4 +53,5 @@ type ClientFactory interface {
ServiceEndpoint(ctx context.Context, organization string) (serviceendpoint.Client, error)
Security(ctx context.Context, organization string) (security.Client, error)
Extensions(ctx context.Context, organization string) (extensions.Client, error)
WorkItemTracking(ctx context.Context, organization string) (workitemtracking.Client, error)
}
9 changes: 9 additions & 0 deletions internal/azdo/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/operations"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/security"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
"github.com/tmeckel/azdo-cli/internal/azdo/extensions"
"github.com/tmeckel/azdo-cli/internal/config"
)
Expand Down Expand Up @@ -115,3 +116,11 @@ func (c *clientFactory) Extensions(ctx context.Context, org string) (extensions.
}
return extensions.NewClient(ctx, conn.(*connectionAdapter).conn), nil
}

func (c *clientFactory) WorkItemTracking(ctx context.Context, org string) (workitemtracking.Client, error) {
conn, err := c.factory.Connection(org)
if err != nil {
return nil, err
}
return workitemtracking.NewClient(ctx, conn.(*connectionAdapter).conn)
}
5 changes: 4 additions & 1 deletion internal/azdo/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ func (r RemoteSet) FindByName(names ...string) (*Remote, error) {

func (rs RemoteSet) MarshalLogArray(enc zapcore.ArrayEncoder) error {
for _, remote := range rs {
enc.AppendObject(remote)
err := enc.AppendObject(remote)
if err != nil {
return err
}
}
return nil
}
Expand Down
58 changes: 11 additions & 47 deletions internal/cmd/boards/area/project/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,34 +94,29 @@ func runList(ctx util.CmdContext, opts *listOptions) error {
}

ios.StartProgressIndicator()
progressStopped := false
defer func() {
if !progressStopped {
ios.StopProgressIndicator()
}
}()
defer ios.StopProgressIndicator()

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

cfg, err := ctx.Config()
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}

orgURL, err := cfg.Authentication().GetURL(org)
orgURL, err := cfg.Authentication().GetURL(scope.Organization)
if err != nil {
return fmt.Errorf("failed to resolve organization URL: %w", err)
}

conn, err := ctx.ConnectionFactory().Connection(org)
conn, err := ctx.ConnectionFactory().Connection(scope.Organization)
if err != nil {
return fmt.Errorf("failed to create connection: %w", err)
}

endpoint, err := buildAreaEndpoint(strings.TrimRight(orgURL, "/"), project, opts.path)
endpoint, err := buildAreaEndpoint(strings.TrimRight(orgURL, "/"), scope.Project, opts.path)
if err != nil {
return err
}
Expand All @@ -143,8 +138,8 @@ func runList(ctx util.CmdContext, opts *listOptions) error {
}

zap.L().Debug("listing project area paths",
zap.String("organization", org),
zap.String("project", project),
zap.String("organization", scope.Organization),
zap.String("project", scope.Project),
zap.String("path", strings.TrimSpace(opts.path)),
zap.Int("depth", opts.depth),
zap.String("endpoint", reqURL.String()),
Expand Down Expand Up @@ -174,18 +169,14 @@ func runList(ctx util.CmdContext, opts *listOptions) error {

if opts.exporter != nil {
ios.StopProgressIndicator()
progressStopped = true
return opts.exporter.Write(ios, nodes)
}

tp, err := ctx.Printer("list")
tp, err := ctx.Printer("table")
if err != nil {
return err
}

ios.StopProgressIndicator()
progressStopped = true

tp.AddColumns("Name", "Path", "HasChildren")
for _, n := range nodes {
tp.AddField(n.Name)
Expand All @@ -198,36 +189,9 @@ func runList(ctx util.CmdContext, opts *listOptions) error {
tp.EndRow()
}

return tp.Render()
}
ios.StopProgressIndicator()

func resolveProjectScope(ctx util.CmdContext, arg string) (string, string, error) {
parts := strings.Split(strings.TrimSpace(arg), "/")
switch len(parts) {
case 1:
project := strings.TrimSpace(parts[0])
if project == "" {
return "", "", util.FlagErrorf("project argument cannot be empty")
}
cfg, err := ctx.Config()
if err != nil {
return "", "", fmt.Errorf("failed to read configuration: %w", err)
}
org, err := cfg.Authentication().GetDefaultOrganization()
if err != nil || strings.TrimSpace(org) == "" {
return "", "", util.FlagErrorf("no organization specified and no default organization configured")
}
return strings.TrimSpace(org), project, nil
case 2:
org := strings.TrimSpace(parts[0])
project := strings.TrimSpace(parts[1])
if org == "" || project == "" {
return "", "", util.FlagErrorf("invalid project argument %q; expected format ORGANIZATION/PROJECT", arg)
}
return org, project, nil
default:
return "", "", util.FlagErrorf("invalid project argument %q; expected format ORGANIZATION/PROJECT", arg)
}
return tp.Render()
}

func buildAreaEndpoint(baseURL, project, path string) (string, error) {
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/boards/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/boards/area"
"github.com/tmeckel/azdo-cli/internal/cmd/boards/iteration"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
)

Expand All @@ -23,6 +24,7 @@ func NewCmd(ctx util.CmdContext) *cobra.Command {
}

cmd.AddCommand(area.NewCmd(ctx))
cmd.AddCommand(iteration.NewCmd(ctx))

return cmd
}
Loading
Loading