Developer: # Repository Guidelines
- Source Code: Located in
internal/(core packages:cmd/*,azdo,config,git,iostreams,docs, etc.). - CLI Entrypoints: Main binary at
cmd/azdo/azdo.go, documentation generator atcmd/gen-docs/. - Documentation: Generated Markdown in
docs/(output from Cobra command hierarchy). - Tests: Placed alongside production code as
*_test.go. - Vendoring: Dependencies locked in
vendor/.
- Build CLI:
make build(producesazdobinary). - Lint:
make lint(runs golangci-lint based on.golangci.yml). - Testing:
go test ./...(useTIMEOUT=...for custom timeout). - Documentation:
make docs(rebuildsdocs/viacmd/gen-docs). - Housekeeping:
make tidy(go mod tidy),make clean(removes binaries/distribution files). - Run Locally:
go run cmd/azdo/azdo.go --version(no installation required).
- Language: Go 1.22; use
gofmtandgoimportsfor code formatting; keep diffs minimal. - Linting: Follow
golangci-lintguidance; wrap errors using%wfor error chains. - Naming: Packages use lowercase; exported identifiers in CamelCase; files use lower_snake_case.
- CLI Flags: Use kebab-case (e.g.,
--organization-url). - Multi-value flag conventions: When supporting “remove all” semantics on list flags, reserve
*as the exclusive sentinel. Commands must reject combinations like--remove-label foo,*and treat a lone*as “remove every existing entry.” - Editing Tools: Modify files using git-aware patches (e.g.,
apply_patch). Do not rely on ad-hoc scripts (Python, sed, etc.) to edit tracked files so diffs stay reviewable. - Logging: Use
zap.L()with structured messages; prefer%wfor 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
gofumptafter coding is complete, so focus on correctness first.
- Frameworks: Use standard
testingpackage andtestifytools. - Conventions: Place tests in
*_test.go; followTestXxxfunction format; prefer table-driven tests. - Execution: tests must be hermetic and use mocks (
internal/mocks). Create new mocks as needed. All tests must use a simulated API via mocks, no calling the Azure DevOps REST API directly. - Coverage: Add tests for new features and edge cases (e.g., authentication, URL parsing, remote operations).
- Imports: Review and understand structs and functions from the
vendordirectory to maintain correct imports. - REST API Commands: For commands that interact with Azure DevOps REST API endpoints (
internal/cmd), always add black box tests informed by Azure DevOps REST API 7.1 documentation.
For a complete guidance on how to implement tests refer to TESTING.md
- Commits: Adhere to Conventional Commits specification (e.g.,
feat: ...,fix: ...,chore: ...,refactor: ...). - Pull Requests: Clearly describe changes, rationale, and how to reproduce/verify; reference linked issues.
- CI Checks: Run
make lintandgo test ./...before submission; updatedocs/if CLI changes.
- Secrets Management: Never commit secrets. Use
AZDO_TOKENfor headless runs;azdo auth loginuses OS keyring for token storage by default. - Default Organization: Set via
AZDO_ORGANIZATIONor config files under${AZDO_CONFIG_DIR:-~/.config/azdo}.
- If you propose a plan and the user approves it, you must follow it; any material deviation (approach, dependencies, SDK-vs-REST, output shape) requires you to stop and ask for approval before implementing.
- If a planned step is blocked by sandbox/network/tooling constraints, do not “work around” by changing the approach; pause and ask for approval (or for the user to perform required external steps) instead.
- If you need to create directories or delete/move files, do not do it yourself; stop and ask the user to perform the action, then continue once confirmed.
- Do not introduce placeholder/stub implementations (e.g., TODOs, empty helpers, or temporary
return nil) to “get unstuck”; if information is missing, stop and ask the user for guidance/approval. - Prefer MCP tools (GitHub MCP / msdocs / context7) over direct network fetches (
curl, etc.); if MCP cannot provide what’s needed, try network fetches or internet search. If this does not work either, ask the user to fetch/provide the information instead.
- Location & Structure: Place new command code in an appropriate subdirectory under
internal/cmd/<category>/<command>/, matching the CLI hierarchy. For example,azdo repo create→internal/cmd/repo/create/create.go. - Factory Function: Define a single factory function named
NewCmd(ctx util.CmdContext) *cobra.Command— do not prefix with category names (oldNewCmdRepoXstyle is deprecated). - Options Handling: Use an unexported
optsstruct to bind CLI flags. Keep parsing and validation inRunEminimal, delegating logic to a separaterunXfunction. - CmdContext Usage: Always use the injected
util.CmdContextto retrieveIOStreams, configuration (ctx.Config()), connection (ctx.ConnectionFactory()), and typed API clients viactx.ClientFactory(). - Vendored API: Access Azure DevOps endpoints via the vendored
azuredevops/v7client packages instead of raw HTTP calls. Build the appropriateArgsstructs and call the client method (e.g.,git.Client.CreateRepository).- CRITICAL: Verify Data Types: Before using API response data, always inspect the struct definitions in the
vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/directory. Mismatched types (e.g.,int64vsuint64) between the API struct and your command's structs will cause compilation errors.
- CRITICAL: Verify Data Types: Before using API response data, always inspect the struct definitions in the
- Output: Use
ctx.Printer(format)and the standard printer helpers to format table or JSON output. When not emitting JSON, preferctx.Printer("list")for clear tables and pick the most relevant columns for the scenario.
- Start the progress indicator immediately after successfully obtaining
IOStreamsso users see activity while you parse inputs or build clients. - Immediately
defer ios.StopProgressIndicator()to guarantee cleanup on every return path. - If you need to print to stdout/stderr before the deferred stop executes, call
ios.StopProgressIndicator()first to keep progress text from interleaving with user-visible output. - Testing: Create mocks for the relevant client interface methods under
internal/mocksand write hermetic, table-driven tests alongside the command.
When a required Azure DevOps client is not available in the vendored Go SDK:
- 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). - Extend
type ClientFactory interfaceininternal/azdo/connection.gowith the new client method signature. - Ask the user to run
go mod tidyfollowed bygo mod vendorafter the interface additions (the sandbox cannot do this automatically). - Add a matching mock generation entry to
scripts/generate_mocks.sh. - Let the user run
bash ./scripts/generate_mocks.sh - Implement the factory method in
internal/azdo/factory.goso the new client can be constructed via existing connection plumbing.
Do not hand-roll HTTP calls or add new internal/azdo/extensions methods as a shortcut when an SDK client can be introduced through this process; if the user-approved plan is to add an SDK client, follow the steps above or stop and ask for approval to change approach.
-
JSON and Table/Plain output are handled via separate code paths.
-
Use
util.AddJSONFlags(cmd, &opts.exporter, ...)inNewCmdto register JSON-related flags (--json,--jq,--template). This populates theopts.exporterfield when a user specifies one of those flags. The string slice you pass must list every JSON field you expose (matching the struct tag names) so users can filter output predictably. -
JSON Output Logic:
- In the command's
run...function, check ifopts.exporter != nil. - If true, this indicates the user wants JSON output.
- Define a dedicated view struct (or slice of structs) that represents the JSON surface you intend to support. Field names must match the strings you register with
util.AddJSONFlagsand every optional field should use a pointer type withjson:"...,omitempty"so unset values disappear from the payload. - Populate that view struct from the SDK model (write small helper functions when mapping requires normalization—e.g., formatting
azuredevops.Time, collapsing identities, adding derived counts). Avoid returning the raw SDK types directly; surface only the columns you are committed to supporting. - Call
opts.exporter.Write(ios, result)to serialize the struct and print it.
- In the command's
-
Table/Plain Output Logic:
- This is the default path, executed when
opts.exporter == nil. - For creating horizontal tables:
- Get a printer:
tp, err := ctx.Printer("list"). - Define all column headers:
tp.AddColumns("Header1", "Header2", ...). - Finalize the header row:
tp.EndRow(). - For each data row, add cell values in order:
tp.AddField(value1),tp.AddField(value2), etc. - Finalize the data row:
tp.EndRow(). - Render the table:
tp.Render().
- CRITICAL:
AddFieldpopulates a cell in a horizontal row corresponding to a column defined byAddColumns. It is not for creating vertical key-value lists (e.g.,Label: Value). Do not pass a label toAddField.
- Get a printer:
- For simple text output (e.g., a success message),
fmt.Fprintf(ios.Out, "...")is acceptable.
- This is the default path, executed when
-
Documentation:
- Add CLI help examples for using
--jsonand--format table. - Run
make docsto regenerate markdown so output options appear in generated documentation.
- Add CLI help examples for using
-
Parameter Derivation: Before defining CLI flags/args for a new command, analyze the corresponding method in the vendored Azure DevOps Go API under
vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/<service>/client.go(or related file). Review its function signature and the associatedArgsstruct:- Map required struct fields or parameters to required CLI flags or positional arguments.
- Map optional fields to optional CLI flags, providing sensible defaults from environment variables (
AZDO_*) or configuration where applicable. - Use repository conventions for flag naming (kebab-case) and argument positioning.
- Ensure every CLI parameter has clear help text and appears in regenerated documentation (
make docs).
-
Organization/Project Positional Argument Parsing:
- For commands that operate within a project, accept the target project and optional organization as the first positional argument in the form
[ORGANIZATION/]PROJECT. - Parse by splitting on
/:- One segment → project only; retrieve organization from default config.
- Two segments → first is organization, second is project.
- Any other segment count → return a flag/argument error.
- If organization is omitted and no default is configured, return an error stating that no organization was specified or configured.
- Follow patterns established in existing commands (e.g.,
internal/cmd/repo/list/list.go) for parsing and validation.
- For commands that operate within a project, accept the target project and optional organization as the first positional argument in the form
-
Pagination for List Operations:
- When using any Azure DevOps SDK
List*method that supports continuation tokens, loop until theContinuationTokenin the response is nil or empty. - Append results across all pages before applying filters or selections.
- Break the loop only after exhausting all pages, or if the command supports
--max-items [int>0]and the user specified a value stop after the max value
- When using any Azure DevOps SDK
-
Confirmation for Destructive Operations:
- Destructive commands must prompt the user for confirmation unless a
--yesflag is provided. - On cancellation, return
util.ErrCancel.
- Destructive commands must prompt the user for confirmation unless a
-
Nil Handling for API Fields:
- Explicitly check for nil pointer fields returned from API calls before use.
- Use
types.GetValuefor safe dereferencing or return an error if the value is required and missing.
-
Debug Logging:
- Use
zap.L().Debugto log critical decision points (e.g., parsing format detected, scope descriptor resolutions, API call stages).
- Use
-
Aliases:
- For common destructive commands, provide short command aliases such as
d,del,rmto improve ergonomics. - For list commands, provide aliases such as
l,ls - For commands that create objects provide aliases like
c,cr
- For common destructive commands, provide short command aliases such as
-
Working with Scope Descriptors:
- For project-scoped operations, you must first fetch the project object using the
core.Client. - Then, you must use the
graph.Client'sGetDescriptormethod with the project'sStorageKey(ID) to get the correct scope descriptor. - The
graph.Client'sGetDescriptorcan be used to get the storage descriptor for various objects like projects, users, groups etc.
- For project-scoped operations, you must first fetch the project object using the
-
Single-Level Addition (Leaf to Existing Group):
- Implement the new leaf command in the correct parent group’s subdirectory, using
NewCmd(ctx). - Import and register it in the parent’s
NewCmdviaparentCmd.AddCommand(child.NewCmd(ctx)).
- Implement the new leaf command in the correct parent group’s subdirectory, using
-
New Subgroup Under Existing Group:
- Create a new directory for the subgroup under the group’s folder.
- Implement
NewCmd(ctx)for the subgroup (acts as a parent for its own children). - Register the subgroup in the immediate parent’s
NewCmdwithAddCommand. - Add leaf commands under the subgroup and register them similarly.
-
New Top-Level Group:
- Implement the group’s
NewCmd(ctx)ininternal/cmd/<group>/<group>.go. - Add it to the root command in
internal/cmd/root/root.goviarootCmd.AddCommand(group.NewCmd(ctx)).
- Implement the group’s
-
Multi-Level Nesting:
- For deeper hierarchies (e.g.,
root → graph → viz → generate), ensure each parent in the chain callsAddCommand()for its direct children. - Create folders reflecting the hierarchy (
internal/cmd/graph/viz/generate/). - Provide a
NewCmdfunction for every level that initializes its Cobra command and wires its children.
- For deeper hierarchies (e.g.,
-
Documentation Update: After adding any new command at any level, run
make docsto regenerate CLI documentation so all new commands appear indocs/. -
Checklist: Before making changes, begin with a concise checklist (3-7 conceptual bullets) outlining intended actions. Skip if the change is trivial
-
Change Scope: Keep changes focused and consistent with existing structure and naming.
-
Patch Size: Favor small, isolated patches and update/add nearby tests where relevant.
-
Patch Size: Favor small, isolated patches and update/add nearby tests where relevant. If a planned step becomes blocked, stop and ask instead of substituting a different implementation strategy.
-
Validation: After code edits, validate results in 1-2 lines and proceed or self-correct if validation fails.
-
Code Edits: Explicitly state assumptions before edits, create or run minimal tests when possible, and produce ready-to-review diffs following the repository style.
-
Code Scope: When you create, change or fix tests you only work on tests. You don't change any other code. When required prompt the user to deviate from that instruction.
-
Context7: Always use context7 when I need code generation, setup or configuration steps, or library/API documentation. This means you should automatically use the Context7 MCP tools to resolve library id and get library docs without me having to explicitly ask.
-
JSON and Table/Plain output are handled via separate code paths.
-
Use
util.AddJSONFlags(cmd, &opts.exporter, ...)inNewCmdto register JSON-related flags (--json,--jq,--template). This populates theopts.exporterfield when a user specifies one of those flags, and the provided slice must enumerate every JSON field (matching the struct tags) that the command supports so consumers can filter reliably. -
JSON Output Logic:
- In the command's
run...function, check ifopts.exporter != nil. - If true, this indicates the user wants JSON output.
- Define a dedicated view struct (or slice of structs) with explicit
json:"..."tags and register the matching field names withutil.AddJSONFlags. Use pointer types plusomitemptyfor optional fields so unset data is omitted, and map values from the SDK into this view (formatting times, flattening identities, computing derived helpers). Do not expose raw SDK structs in the JSON response. - Call
opts.exporter.Write(ios, result)to serialize the struct and print it.
- In the command's
-
Table/Plain Output Logic:
- This is the default path, executed when
opts.exporter == nil. - For tabular data, get a printer with
tp, err := ctx.Printer("list"). Usetp.AddColumns(),tp.AddField(), andtp.EndRow()to build the table, then calltp.Render().
- This is the default path, executed when
To ensure high-quality, production-ready code and prevent common errors, adhere to the following guidelines when generating or modifying Go code:
-
Mandate: When generating or modifying Go code, always explicitly list and verify all required import statements. Before writing the file, perform a dry run or a mental check to ensure all types, functions, and packages used in the new/modified code are correctly imported.
-
Detail: Ensure imports for standard library packages (e.g.,
fmt,strings,context), third-party libraries (e.g.,github.com/spf13/cobra,github.com/MakeNowJust/heredoc), and internal project modules (e.g.,github.com/tmeckel/azdo-cli/internal/cmd/util,github.com/tmeckel/azdo-cli/internal/azdo) are present. If unsure, err on the side of including common imports for the context.
- Prefer the generic helpers in
internal/types(e.g.,MapSlice,MapSlicePtr) when transforming SDK slices instead of rewriting mapping loops. These helpers already handle nil pointers and keep slice code consistent across commands.
-
Context & IOStreams: When interacting with
util.CmdContext, always retrieveIOStreamsandPrompterinto local variables to handle potential errors immediately.// GOOD: // iostreams, err := ctx.IOStreams() // if err != nil { return err } // p, err := ctx.Prompter() // if err != nil { return err } // BAD: // if !ctx.IOStreams().CanPrompt() { ... }
-
Safely Dereference Pointers: The Azure DevOps API often returns pointers for fields that can be null. To prevent
nil pointer dereferencepanics, use the generic helpertypes.GetValue[T](ptr *T, defaultVal T) T. This is the preferred way to safely access the value of a pointer.// BAD: This will panic if project.Description is nil // description := *project.Description // GOOD: This safely returns the description or an empty string description := types.GetValue(project.Description, "")
-
User Cancellation: For operations that can be cancelled by the user (e.g., confirmation prompts), prefer returning
util.ErrCanceloverutil.SilentExitto clearly distinguish user-initiated cancellations from other silent exits.