❣ We love pull requests from everyone !
Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
- Fork the repo and create your branch from
master. - If you've added code that should be tested, add tests.
- If you've added code that need documentation, update the documentation.
- Write a good commit message.
- Issue that pull request!
If you've never written Go in your life, then join the club! Go is widely considered an easy-to-learn language, so if you're looking for an open source project to gain dev experience, you've come to the right place.
If you want to start contributing to azdo with the click of a button, you can
open the azdo codebase in a Codespace. First fork the repo, then click to
create a codespace:
This allows you to contribute to azdo without needing to install anything on
your local machine. The Codespace has all the necessary tools and extensions
pre-installed.
Please note by participating in this project, you agree to abide by the code of conduct.
In short, when you submit code changes, your submissions are understood to be under the same MIT License that covers the project.
Report bugs using Github's issues
We use GitHub issues to track public bugs. Report a bug by opening a new issue; it's that easy!
This project is written in Go. Go is an opinionated language with strict idioms, but some of those idioms are a little extreme. Some things we do differently:
- There is no shame in using
selfas a receiver name in a struct method. In fact we encourage it - There is no shame in prefixing an interface with 'I' instead of suffixing with 'er' when there are several methods on the interface.
- If a struct implements an interface, we make it explicit with something like:
var _ MyInterface = &MyStruct{}This makes the intent clearer and means that if we fail to satisfy the interface we'll get an error in the file that needs fixing.
To check code formatting gofumpt (which is a bit stricter than gofmt) is used.
VSCode will format the code correctly if you tell the Go extension to use gofumpt via your settings.json
by setting formatting.gofumpt to true:
To run gofumpt from your terminal go:
go install mvdan.cc/gofumpt@latest && gofumpt -l -w .If you are new to the codebase, start here before making structural changes.
azdo is organized as a layered CLI:
cmd/azdo/azdo.goboots the binary.internal/cmd/root/root.gobuilds the root Cobra command and wires the top-level command tree.internal/cmd/...contains command groups and leaf commands.internal/cmd/util/cmd_context.goprovides the shared runtime context used by commands.internal/azdo/...creates Azure DevOps connections and typed SDK clients.internal/git/...,internal/iostreams/..., andinternal/printer/...handle repository discovery, terminal UX, and output formatting.
When in doubt, trace a command from the root command down into its run... helper and then into the client factory.
internal/cmd/root/root.go- best entry point for understanding the CLI surface area.internal/cmd/util/cmd_context.go- central dependency-injection seam for config, I/O, prompting, printers, repo helpers, and API clients.internal/azdo/connection.go- interfaces for Azure DevOps connections and typed clients.internal/azdo/factory.go- concrete implementation for creating org-scoped SDK clients.internal/iostreams/iostreams.go- pager, TTY, spinner, and stream lifecycle behavior.TESTING.md- required testing conventions, mock usage, and acceptance-test boundaries.
Most commands follow the same pattern:
- each package exports
NewCmd(ctx util.CmdContext) *cobra.Command - a private options struct stores flags and args
RunEdoes only minimal parsing and validation- actual behavior lives in a dedicated
run...helper
Representative examples:
internal/cmd/repo/create/create.gointernal/cmd/project/show/show.gointernal/cmd/pr/list/list.go
Prefer this structure when adding or refactoring commands. It keeps CLI plumbing small and makes testing easier.
util.CmdContext is the main abstraction shared across commands. Use it to retrieve:
IOStreams()for stdout, stderr, TTY, and progress behaviorConfig()for organization and auth-related settingsPrompter()for interactive flowsPrinter(...)for table, list, or JSON outputConnectionFactory()andClientFactory()for Azure DevOps accessRepoContext()for local-repository-aware operations
Avoid constructing these dependencies ad hoc inside commands unless there is a strong reason. The codebase is designed around the context abstraction.
The usual service path is:
- Parse organization/project/repo scope in a command.
- Call
ctx.ClientFactory().<Service>(ctx.Context(), organization). - Build the vendored Azure DevOps SDK
Argsstruct. - Call the SDK client method.
- Map the result into printer or JSON output.
The vendored SDK under vendor/github.com/microsoft/azure-devops-go-api/azuredevops/v7/ is the source of truth for API types. Check the vendored struct definitions before adding new fields or assumptions to command code or tests.
- Prefer
util.AddJSONFlags(...)for commands that expose structured JSON output. - Keep JSON and table/plain output as separate code paths.
- When returning JSON, define a stable view struct instead of dumping raw SDK types unless the existing command already intentionally does that.
- For non-JSON output, prefer
ctx.Printer("list")for tables.
See internal/cmd/util/json_flags.go for the common implementation and internal/cmd/repo/create/create.go for a representative command-level use of exporter vs table output.
Progress handling is centralized in internal/iostreams/iostreams.go, but each command is responsible for sequencing it correctly.
Use this pattern:
- Get
IOStreams(). - Start the progress indicator immediately.
defer StopProgressIndicator().- Stop it explicitly before writing user-visible output.
If you print while the spinner is still active, CLI output can become messy or hard to read.
Tests are expected to be hermetic.
- unit tests live next to the code as
*_test.go - mocks are generated under
internal/mocks/ - command tests typically use
gomock,testify, andiostreams.Test() - acceptance tests are opt-in and documented in
TESTING.md
Before adding a new mock, update scripts/generate_mocks.sh and regenerate mocks as described in TESTING.md.
make build- build the CLImake lint- rungolangci-lintgo test ./...- run testsmake docs- regenerate CLI docs from the Cobra treemake generate-mocks- regenerate mocks after interface changes
If you change command flags, examples, or hierarchy, regenerate docs/ before opening a pull request.
internal/cmd/root/root.go- top-level wiring is easy to break when adding commands or aliases.internal/cmd/util/cmd_context.go- changes here ripple widely through the command tree.internal/azdo/connection.goandinternal/azdo/factory.go- client interface changes require matching updates in implementations and mocks.internal/iostreams/iostreams.go- output and spinner lifecycle bugs can surface as UX regressions.- pagination helpers such as
internal/azdo/loader.go- small mistakes here can affect many list commands.
Treat these files as high-leverage areas: small changes can have wide effects.
- keep the patch focused
- add or update tests for behavior changes
- regenerate docs when CLI output or flags change
- regenerate mocks when interfaces change
- run
make lintandgo test ./... - write a Conventional Commit style message if you are creating commits in the repo workflow
If you can think of any way to improve these docs let us know.