Instructions for AI coding agents working with the Azure Developer CLI.
Azure Developer CLI (azd) is a Go-based CLI for Azure application development and deployment. It handles infrastructure provisioning with Bicep/Terraform, app deployment, environment management, project and service lifecycle hooks, and features a gRPC-based extension framework.
cli/azd/
├── main.go # Entry point
├── cmd/ # Commands (ActionDescriptor pattern)
│ ├── root.go # Command tree registration
│ ├── container.go # IoC service registration
│ ├── actions/ # Action framework
│ └── middleware/ # Cross-cutting concerns (telemetry, hooks, extensions)
├── pkg/ # Reusable public packages
│ ├── ioc/ # Dependency injection container
│ ├── project/ # Project configuration (azure.yaml), service targets, framework services
│ └── infra/ # Infrastructure providers (Bicep, Terraform)
│ ├── azapi/ # Azure APIs
│ └── tools/ # External tools
├── internal/ # Internal packages (telemetry, tracing)
├── test/ # Test utilities
├── extensions/ # First-party extensions
└── docs/ # Documentation
Entry points: main.go → cmd/root.go (command tree) → cmd/container.go (IoC registration)
Tip: Service registration in cmd/container.go shows all major components. To find where a feature is implemented, start with the command in cmd/, follow to the action, then trace service dependencies.
For Microsoft contributors, this repository's agency.toml preconfigures Agency with Azure DevOps MCP access for the azure-sdk organization and Kusto MCP access to the DevCli database. Prefer these tools for ADO operations and authorized telemetry investigations. Do not expose customer-identifying data in issues, pull requests, logs, or generated artifacts.
All repository-owned azd pipelines are in the Azure DevOps internal project:
- Core and repository pipelines are under
\azure-dev. - Extension pipelines are under
\azure-dev\extensions.
When investigating a pipeline:
- Start from the GitHub pull request check link when available. It identifies the exact Azure DevOps build and often the specific job.
- Otherwise, locate the relevant pipeline in the folders above.
- Correlate runs using the pull request number, commit SHA, and source branch.
- Inspect the failed or running job logs first. Distinguish the root failure from secondary warnings, infrastructure noise, or downstream failures.
- For releases, distinguish publishing from validation using the source branch and template parameters. For the core CLI pipeline,
DoPublish=trueidentifies a publishing release run.
For an end-to-end core release roadmap, version correlation, and known recovery procedures, read Tracking and recovering an azd core release. Changelog preparation alone does not establish release completion.
Commands assume you are in cli/azd.
Mage is included in go.mod. Use go tool mage <target> instead of installing or invoking it separately.
go buildNote: In CI environments like inside a GitHub coding agent session, run go build first as the automatic build is skipped and the azd binary must exist for tests that spawn the CLI process. This applies to snapshot tests and functional tests in test/functional/.
# Specific test
go test ./pkg/project/... -run TestProjectConfig
# Update command snapshots (whenever command help text changes or new commands are added)
UPDATE_SNAPSHOTS=true go test ./cmd -run 'TestFigSpec|TestUsage'
# Unit tests only (can take up to 10 min)
go test ./... -short
# Full suite including E2E (can take 10+ min)
go test ./...
Test file patterns:
- Unit tests:
*_test.goalongside source files - Functional tests:
test/functional/ - Shared mocks:
test/mocks/
When writing tests, prefer table-driven tests. Use testify/mock for mocking.
Tip: The
/azd-preflightCopilot skill runs all these checks and auto-fixes issues. See.github/skills/azd-preflight/.
Additional mage targets:
go tool mage schema:format— format every JSON file below the repository'sschemas/directory with four-space indentation.go tool mage schema:check— verify that every JSON file below the repository'sschemas/directory is formatted.go tool mage record— re-record functional test cassettes against a live Azure subscription. Accepts an optional-filter=TestNameflag to re-record specific tests. Typically only core maintainers need to run this; external contributors can rely on playback mode (the default) which requires no Azure access. Requiresazd auth loginand a configured test subscription (seedocs/recording-functional-tests-guide.md).go tool mage generateProtos— regenerate the checked-in Go, Python, and JavaScript protobuf bindings using the pinned containerized toolchain.go tool mage coverage:pr— preview the CI PR coverage gate locally before pushing. Resolves PR-touched.gofiles viagit merge-base origin/main HEADfor the per-package summary, runs the diff against the latestmainbaseline, and fails (exit 2) on either breach type: any PR-touched package drops more than 0.5 pp, or overall coverage falls below 69% (defaults match CI; override viaCOVERAGE_MAX_PACKAGE_DECREASE,COVERAGE_MIN_OVERALL). Seedocs/code-coverage-guide.mdfor details.go tool mage updateGoVersion <version>— bump the pinned Go toolchain version everywhere it is referenced (everycli/azdgo.mod, the ADOsetup-gotemplate, Dockerfiles, and the devcontainer Go feature).cli/azd/go.modis the source of truth enforced by thevalidate-go-versionworkflow. This is the single source of truth for the sync logic. Example:go tool mage updateGoVersion 1.26.4.
gofmt -s -w .
go fix ./...
golangci-lint run ./...
cspell lint "**/*.go" --relative --config ./.vscode/cspell.yaml --no-progress
../../eng/scripts/copyright-check.sh . --fix
# From repo root — spell check docs/misc files (mirrors CI cspell-misc.yml)
cd ../..
cspell lint "**/*" --relative --config ./.vscode/cspell.misc.yaml --no-progress
cd cli/azd- Line length: 125 chars max for Go (enforced by
llllinter); no limit for Markdown - Spelling (Go): Add technical terms to
cli/azd/.vscode/cspell.yamloverrides - Spelling (docs/misc): Add terms to
.vscode/cspell.misc.yamloverrides or.vscode/cspell.global.yaml- Use file-scoped
overridesentries (not the globalwordslist) for terms specific to one file
- Use file-scoped
- Copyright: All Go files need the Microsoft header (handled by copyright-check.sh)
- Code modernization:
go fix ./...applies automatic modernizations (e.g.interface{}→any, loop simplifications, use ofslices/mapspackages). CI enforces this check.
The project uses golangci-lint with these key linters enabled (see .golangci.yaml):
lll— max line length 125 characters (tab width 4). Break long lines with continuation.gofmt— standard Go formattingforbidigo— forbidsfmt.Print*andos.Stdoutin test files to prevent phantom CI failures (see #8385). Scoped to*_test.goonly; allowlisted files that intentionally test stdout are excluded.gosec— security checkserrorlint— correcterrors.Is/errors.As/errors.AsTypeusageunused— detect unused codestaticcheck— comprehensive static analysis
Common line-length issues and fixes:
// BAD: 135 chars — too long for lll
if respErr, ok := errors.AsType[*azcore.ResponseError](err); ok && respErr.StatusCode == 404 { // already deleted
// GOOD: break the condition across lines
// Resource group is already deleted
if respErr, ok := errors.AsType[*azcore.ResponseError](err); ok &&
respErr.StatusCode == 404 {When running tools like CodeQL or go mod tidy, go.mod and go.sum files may be modified across multiple Go modules. Only commit go.mod/go.sum changes that are relevant to the task.
- azd core changes (
cli/azd/excludingextensions/): Only commitcli/azd/go.modandcli/azd/go.sum. Do NOT commit anygo.mod/go.sumfiles incli/azd/extensions/. - Extension changes (
cli/azd/extensions/<extension-name>/): Only commitgo.mod/go.sumfor the specific extension being modified.
If unrelated go.mod/go.sum files are staged, unstage them before committing.
Always use IoC for service registration—never instantiate services directly (see cli/azd/cmd/container.go):
ioc.RegisterSingleton(container, func() *MyService {
return &MyService{dep: ioc.Get[*Dependency](container)}
})Commands implement the actions.Action interface, not traditional Cobra handlers:
type myAction struct {
svc *SomeService
}
func newMyAction(svc *SomeService) actions.Action {
return &myAction{svc: svc}
}
func (a *myAction) Run(ctx context.Context) (*actions.ActionResult, error) {
return &actions.ActionResult{
Message: &actions.ResultMessage{Header: "Success"},
}, nil
}- Commands can support multiple output formats via
--outputflag likejsonandtable - Use structured output for machine consumption
- Shell-safe output: When emitting shell commands in user-facing messages (e.g.,
cd <path>), quote paths that may contain spaces. Usefmt.Sprintf("cd %q", path)or conditionally wrap in quotes - Consistent JSON types: When adding fields to JSON output (
--output json), match the types used by similar fields across commands. Don't mix*time.Timeand custom timestamp wrappers (e.g.,*RFC3339Time) in the same API surface
When working on CLI output, terminal UX, spinners, progress states, colors, or prompts, you MUST read the relevant style guide before making any changes or recommendations. Reference the right file for the flow you're working on:
📄 cli/azd/docs/style-guidelines/azd-style-guide.md — core azd design patterns (full path from repo root)
This file is the authoritative reference for core azd terminal UX patterns including:
- Progress report states (
(✓) Done,(x) Failed,(!) Warning,(-) Skipped) - Spinner type (bar-fill
|=======|) - Color conventions (
WithSuccessFormat,WithErrorFormat,WithHighLightFormat, etc.) - User input patterns (text input, list select, yes/no confirm)
- Prompt styling (
?marker in bold blue,[Type ? for hint], post-submit states)
📄 cli/azd/docs/style-guidelines/responsive-layout-style-guide.md — responsive list and table layouts
Read this file when adding or changing list-command output that uses output.PrettyTableFormatter. It documents full, compact, and stacked-row layouts, column priorities, grouping, status styling, and empty states.
📄 cli/azd/docs/style-guidelines/agentic-ux-style-guide.md — agentic (AI / GitHub Copilot) UX patterns
Read this file instead when working on the AI-driven / agentic experience (the "Set up with GitHub Copilot (Preview)" flow, the Copilot session runtime, and the AgentDisplay renderer). It documents the distinct magenta + glyph visual language for agent identity, tool activity, subagents, and thinking states.
Note: These are two distinct visual systems. The core guide's patterns are not enforced on extensions, but extension developers are encouraged to follow them where applicable for consistency. Do not apply core azd status patterns to agentic flows (or vice versa) — pick the guide that matches the flow. Extension-specific UX is documented in
cli/azd/docs/extensions/extensions-style-guide.md.
- Import order: stdlib → external → azure/azd internal → local
- Complex packages: Consider using
types.gofor shared type definitions (3+ types) - Context propagation: Pass
ctx context.Contextas the first parameter to functions that do I/O or may need cancellation - Don't duplicate logic across scopes: When similar logic exists for multiple deployment scopes (e.g., resource group + subscription), extract shared helpers (e.g.,
filterActiveDeployments()) instead of copying code between scope implementations
- Wrap errors with
fmt.Errorf("context: %w", err)to preserve the error chain - Consider using
internal.ErrorWithSuggestionfor straightforward, deterministic user-fixable issues - Handle context cancellations appropriately
ErrorWithSuggestioncompleteness: When returningErrorWithSuggestion, populate all relevant fields (Err,Suggestion,Message,Links).ErrorMiddlewareskips the YAML error-suggestion pipeline for errors already typed asErrorWithSuggestion, so leaving fields empty means the user misses guidance that the YAML rule would have provided- Telemetry service attribution: Only set
error.service.name(e.g.,"aad") when an external service actually returned the error. For locally-generated errors (e.g., "not logged in" state checks), don't attribute to an external service — use a local classification instead - Scope-agnostic error messages: Error messages and suggestions in
error_suggestions.yamlshould work across all deployment scopes (subscription, resource group, etc.). Use "target scope" or "deployment scope" instead of hardcoding "resource group" - Match links to suggestion text: If a suggestion mentions multiple tools (e.g., "Docker or Podman"), the
links:list must include URLs for all of them. Don't mention options you don't link to - Stale data in polling loops: When polling for state changes (e.g., waiting for active deployments), refresh display data (names, counts) from each poll result rather than capturing it once at the start
ProjectManageris target-agnostic:project_manager.goshould not contain service-target-specific logic (e.g., Container Apps details, Docker checks). Target-specific behavior belongs in the target implementations (e.g.,service_target_containerapp.go) or in theerror_suggestions.yamlpipeline. The project manager is an interface for service management and should not make assumptions about which target is running- Extension-specific documentation: Keep extension-specific environment variables and configuration documented in the extension's own docs, not in core azd reference docs, unless they are consumed by the core CLI itself
- Verify env vars against source: When documenting environment variables, verify the actual parsing method in code —
os.LookupEnv(presence-only) vsstrconv.ParseBool(true/false) vstime.ParseDurationvs integer seconds. Document the expected format and default value accurately
The graph-driven up/provision/deploy engine runs multiple service steps and (when infra.layers[] is configured) multiple layer provision steps in parallel. Several long-lived types now have explicit locking contracts that you MUST honor when adding new methods or write paths.
See docs/concurrency-model.md for the full list — environment.Environment, environment.Manager, kubectl.Cli, containerAppTarget/aksTarget, and serviceManager — and the rules for adding new concurrent state.
When adding a method that mutates one of these types: take the documented lock, hold it across the full read-modify-write, and run go test -race to catch missed locks (single-goroutine tests will not).
- Validate derived paths: When deriving directory names from user input or template paths, always validate the result is not
.,.., empty, or contains path separators. These can cause path traversal outside the working directory - Quote paths in user-facing output: Shell commands in suggestions, follow-up messages, and error hints should quote file paths that may contain spaces
- Public functions and types must have Go doc comments
- Comments should start with the function/type name
- Document non-obvious dependencies or assumptions
- Changelog entries: Generally defer
CHANGELOG.mdupdates for core and extensions to release/version-bump PRs. Exceptions are reasonable when a PR is intentionally also preparing a release, such as for a hotfix - Help text consistency: When changing command behavior, update all related help text —
Short,Long, custom description functions used by help generators, and usage snapshot files. Stale help text that contradicts the actual behavior is a common review finding - No dead references: Don't reference files, scripts, directories, or workflows that don't exist in the PR. If a README lists
scripts/generate-report.ts, it must exist. If a CI table listseval-human.yml, it must be included - PR description accuracy: Keep the PR description in sync with the actual implementation. If the description says "server-side filtering" but the code does client-side filtering, update the description
The file cli/azd/docs/environment-variables.md is the single source of truth for every environment
variable azd reads. When adding or modifying an os.Getenv / os.LookupEnv call:
- Add the variable to the appropriate section in
environment-variables.md. - Include a one-line description that explains what it controls and its default if non-obvious.
- Place debug/internal variables under Debug Variables with the unsupported warning.
azd telemetry coverage is tracked by an ongoing metrics audit whose authoritative spec lives in
docs/specs/metrics-audit/. When you add or change a telemetry event or field you MUST update the
code, all telemetry docs, and the coverage test in the same change — otherwise the audit, the
public reference, and downstream Kusto/LENS consumers drift out of sync. Verify every claim against
actual code behavior, not comments.
When adding or reviewing extension telemetry, read the attribute naming rules before choosing or approving field names.
1. Code
- Field — define an exported, package-level
AttributeKeyvar incli/azd/internal/tracing/fields/fields.go(this file holds the field/key definitions; within the same packagefeatures.goholds feature-name attribute values anddomains.gothe Azure host-domain table). Every field MUST set aClassification(e.g.SystemMetadata,OrganizationalIdentifiableInformation,EndUserPseudonymizedInformation) and aPurpose(FeatureInsight/BusinessInsight/PerformanceAndHealth); telemetry metadata also includes the optionalEndpointandIsMeasurementmembers. Do not add newCustomerContentcollection. - First-party extension field — keep the runtime
ReportUsagemap key as a string literal or same-package compile-time constant and declare its finalext.*name as an exportedAttributeKeyincli/azd/extensions/telemetry/fields.go. Declarations are shared by final key across first-party extensions; reuse an existing key only when its meaning, allowed values, classification, and purpose are identical. Rungo test ./extensions/telemetry; repository validation rejects undeclared or dynamically keyed fields, missing classification/purpose/endpoint metadata, measurements, andCustomerContent. If a value would requireCustomerContent, do not report it. - Event — define a constant in
cli/azd/internal/tracing/events/events.gofollowing theprefix.noun.verbvalue convention. It must be an exported stringconstwhose Go identifier containsEvent(end it withPrefixfor a prefix-match group) so repository metadata tooling discovers it. - Emit at the call site via
tracing.Start(spans/events) plustracing.SetUsageAttributesorspan.SetAttributes(attributes). Always pass afields.AttributeKeymethod (e.g.fields.MyKey.String(v)/.Bool(v)/.Int(v)) — never a rawattribute.String("my.key", v). Telemetry metadata is derived from exportedAttributeKeydeclarations; raw literal keys bypass that contract and are rejected byTestNoRawTelemetryAttributes(cli/azd/cmd/telemetry_test.go). Dynamicext.*keys are the only sanctioned exception. - Follow the field's hashing and omission rules. Use
fields.StringHashed/fields.StringSliceHashed(cli/azd/internal/tracing/fields/key.go) where hashing is required. Preserve documented raw-value exceptions and omission rules in the schema's hashing reference. Prefer counts or fixed categories to customer-derived names; ask a maintainer when the supported inputs or classification are unclear.
2. Documentation — keep all of these in sync
docs/reference/telemetry-data.md— public, user-facing reference for every event/field. Add to Events Reference / Fields Reference with a one-line description, value type (string/bool/number), and applicable events/commands; note conditional or feature-gated behavior under Data Nuances & Gotchas and/or Feature → Telemetry Mapping.docs/specs/metrics-audit/telemetry-schema.md— authoritative schema: add a row with the OTel key, classification, purpose, whether it is hashed, whether it is a measurement, and the allowed enum values.docs/specs/metrics-audit/feature-telemetry-matrix.md— command→telemetry inventory: update the command's row (and/or the Cross-Cutting Subsystems table) and the ✅/⚠️ /❌ coverage flags.docs/specs/metrics-audit/privacy-review-checklist.md. Follow the contributor checklist and copy its PR checklist template into your PR description. Update the checklist when the process changes; keep per-field hashing rules intelemetry-schema.md.
3. Tests
cli/azd/cmd/telemetry_test.go— classify the command in exactly one ofcommandsWithSpecificTelemetry/commandsWithOnlyGlobalTelemetry(both lists are kept sorted), and add field-constant assertions where applicable.
4. Privacy & downstream
- Assess every telemetry change against the approved collection and handling scope using the privacy review checklist. A new field or event does not automatically require a separate formal review. Have a maintainer confirm existing coverage or identify the required additional review before uncovered collection begins.
- For code review, follow the checklist's reviewer guidance and ask a maintainer to assess coverage when it is unclear.
- Record implementation, metadata deployment, and downstream verification separately, with owners for any follow-up.
- If the field is queried downstream, coordinate Kusto-function / cooked-table / dashboard updates
per the recurring process in
docs/specs/metrics-audit/audit-process.md.
This project uses Go 1.26. Use modern standard library features:
slices,maps,cmppackages: Use for searching, sorting, cloning, and iterating—avoid manual loops- Iterators: Use
rangeover functions/iterators (e.g.,maps.Keys(),slices.All()) - Built-ins: Use
min(),max(),clear()directly - Range over integers:
for i := range 10 { }
-
Use
new(val)notx := val; &x- returns pointer to any value.
Go 1.26 extendsnew()to accept expressions, not just types.
Type is inferred:new(0) → *int,new("s") → *string,new(T{}) → *T.
DO NOT usex := val; &xpattern — always usenew(val)directly.
DO NOT use redundant casts likenew(int(0))— just writenew(0).
Before:timeout := 30 debug := true cfg := Config{ Timeout: &timeout, Debug: &debug, }
After:
cfg := Config{ Timeout: new(30), // *int Debug: new(true), // *bool }
-
Use
errors.AsType[T](err)noterrors.As(err, &target). ALWAYS use errors.AsType when checking if error matches a specific type.Before:
var pathErr *os.PathError if errors.As(err, &pathErr) { handle(pathErr) }
After:
if pathErr, ok := errors.AsType[*os.PathError](err); ok { handle(pathErr) }
-
Use
slices.SortFunc(items, func(a, b T) int { return cmp.Compare(a.Name, b.Name) })instead ofsort.Slice -
Use
slices.Clone(s)instead ofappend([]T{}, s...) -
Use
slices.Sorted(maps.Keys(m))instead of collecting keys and sorting them separately -
Use
http.NewRequestWithContext(ctx, method, url, body)instead ofhttp.NewRequest(...) -
Use
wg.Go(func() { ... })instead ofwg.Add(1); go func() { defer wg.Done(); ... }() -
Use
for i := range ninstead offor i := 0; i < n; i++for simple counted loops -
Use
t.Context()instead ofcontext.Background()in tests -
Use
t.Chdir(dir)instead ofos.Chdirplus a deferred restore in tests -
Run
go fix ./...before committing; CI enforces these modernizations
- Test the actual rules, not just the framework: When adding YAML-based error suggestion rules, write tests that exercise the rules end-to-end through the pipeline, not just tests that validate the framework's generic matching behavior
- Search for prior art before adding helpers: Search
test/,pkg/osutil, and the package under test before adding cleanup, retry, process, or platform-specific helpers. Reuse or promote existing behavior and extract shared test utilities instead of creating parallel implementations - Isolate persistent user state: Functional tests that install extensions or mutate config, auth, cache, or other user-level state must set
AZD_CONFIG_DIRto a dedicatedtempDirWithDiagnostics(t)directory on the CLI env, preserving both the parent environment and existing CLI env (append(os.Environ(), cli.Env...)) and settingAZURE_DEV_COLLECT_TELEMETRY=noso the telemetry background writer doesn't hold files open in the temp dir. Tests that only use the existing login should leaveAZD_CONFIG_DIRunset - Make cleanup process-safe: Wait for spawned commands to finish, register cleanup when state is created, and report failures. Use
osutil.RemoveAllfor directories containing executed binaries because Windows may retain transient locks; keep retries at the filesystem boundary instead of adding test sleeps. Sincet.Context()is canceled before cleanup runs, use a separate bounded context when cleanup needs one - Isolate Docker client state: Functional tests that run local Docker builds or logins should set
DOCKER_CONFIGto a directory undertempDirWithDiagnostics(t). This prevents parallel tests and Docker Desktop from sharing mutable context metadata or credentials - Guard fixture type assertions: Check presence and type with
requirebefore casting values loaded from maps, JSON, environment files, or recordings. A raw type assertion panic can abort the package and hide the initiating failure - Use correct env vars for testing:
- Non-interactive mode:
AZD_FORCE_TTY=false(notAZD_DEBUG_FORCE_NO_TTY, which doesn't exist) - No-prompt mode: use the
--no-promptflag for core azd commands;AZD_NO_PROMPT=trueis only used for propagating no-prompt into extension subprocesses - Suppress color:
NO_COLOR=1— always set in test environments to prevent ANSI escape codes from breaking assertions - Recording setup: use
AZD_TEST_AZURE_SUBSCRIPTION_ID,AZD_TEST_TENANT_ID, andAZD_TEST_AZURE_LOCATION. For multi-tenant user logins, also setAZURE_TENANT_IDto prevent azd's tenant picker
- Non-interactive mode:
- Keep provision validation consistent between record and playback:
validation.provisionis a user config key, not an environment variable. Do not disable it only while recording a cassette; either keep it enabled and handle possible warning prompts deterministically or ensure playback uses the same config - TypeScript test patterns: Use
catch (e: unknown)with type assertions, notcatch (e: any)which bypasses strict mode - Reasonable timeouts: Set test timeouts proportional to expected execution time. Don't use 5-minute timeouts for tests that shell out to
azd --help(which completes in seconds) - Efficient directory checks: To check if a directory is empty, use
os.Open+f.Readdirnames(1)instead ofos.ReadDirwhich reads the entire listing into memory - Cross-platform paths: When resolving binary paths in tests, handle
.exesuffix on Windows (e.g.,azdvsazd.exeviaprocess.platform === "win32") - Test new JSON fields: When adding fields to JSON command output (e.g.,
expiresOninazd auth status --output json), add a test asserting the field's presence and format - No unused dependencies: Don't add npm/pip packages that aren't imported anywhere. Remove dead
devDependenciesbefore submitting - Keep terminal output on the injected writer: Commands and UX components must pass the caller's
io.Writerto every nested spinner, prompt, cursor, and formatter. In tests, uset.Log/t.Logffor diagnostics andio.Discardor&bytes.Buffer{}for UX writers. Raw stdout corruptsgo test -jsonand creates phantom CI failures.forbidigoblocks direct writes in tests, but production writer propagation still needs buffer-based coverage. See #8385
Tools follow server.ServerTool interface from github.com/mark3labs/mcp-go/server:
- Constructor:
NewXXXTool() server.ServerTool - Handler:
handleXXX(ctx, request) (*mcp.CallToolResult, error) - Snake_case names (e.g.,
azd_plan_init)
Go module version tags (cli/azd/vX.Y.Z) are created alongside each CLI release tag (azure-dev-cli_X.Y.Z). Extension developers should use semver references in go.mod instead of pseudo-versions. See docs/sdk-versioning.md for details.
When cutting a release, eng/scripts/Update-CliVersion.ps1 automatically updates both cli/version.txt and pkg/azdext/version.go to keep versions in sync.
cli/azd/go.mod is the source of truth for dependencies directly shared by core and first-party Go extensions.
Run go tool mage checkDependencyVersions to validate alignment or go tool mage syncDependencyVersions to update unapproved
mismatches. Temporary exceptions must be exact, issue-linked entries in dependency-versions.json. See
docs/dependency-version-sync.md for the policy and workflow.
First-party azd extensions live in cli/azd/extensions/.
To build:
cd cli/azd/extensions/<extension-name>
# Build using developer extension (for local development)
azd x build
# Or build using Go directly
go buildFeature-specific docs are in docs/ — refer to them as needed. Some key docs include:
docs/style-guidelines/azd-style-guide.md- Core azd CLI style guide (colors, spinners, progress states, terminal UX)docs/style-guidelines/responsive-layout-style-guide.md- Responsive list and table layoutsdocs/style-guidelines/agentic-ux-style-guide.md- Agentic UX patterns (magenta styling and status symbols)docs/style-guidelines/new-azd-command.md- Adding new commandsdocs/extensions/extension-framework.md- Extension development using gRPC extension frameworkdocs/style-guidelines/guiding-principles.md- Design principlesdocs/tracing-in-azd.md- Tracing/telemetry guidelinesdocs/sdk-versioning.md- Go module versioning for extension developers
When creating or modifying GitHub Actions workflows:
- Always declare
permissions:explicitly with least-privilege (e.g.,contents: read). All workflows in the repo should have this block for consistency - Keep workflows readable: Prefer separate scripts for substantial logic so it can be edited and tested directly. For
pull_request_target, execute only trusted repository code, never code from the PR. - Don't overwrite
PATHusing${{ env.PATH }}— it's not defined in GitHub Actions expressions and will wipe the real PATH. Useecho "$DIR" >> $GITHUB_PATHinstead - Cross-workflow artifacts:
actions/download-artifact@v4withoutrun-idonly downloads artifacts from the current workflow run. Cross-workflow artifact sharing requiresrun-idandrepositoryparameters - Prefer Azure DevOps pipelines for jobs that need secrets or Azure credentials — the team uses internal ADO pipelines for authenticated workloads in this public repo
- No placeholder steps: Don't add workflow steps that echo "TODO" or list directories without producing output. If downstream steps depend on generated files, implement the generation or remove the dependency
When adding or changing GitHub API access in a pipeline:
- Always use the shared GitHub App login template instead of personal access tokens. Generate the token in the job that needs it and map
GH_TOKENexplicitly to the consuming task. See publish-cli-winget.yml for an example. - Check the documentation or source for the exact tool version in use. Some tools recommend passing tokens through an environment variable or credential store instead of a command-line argument because arguments may be recorded in logs or diagnostic files.
When reviewing code in Copilot Code Review, also use the azd-code-reviewer skill to look for azd-specific criteria.
For every PR review, including fork PRs, inspect the complete changed-file list
first. If any file under cli/azd/extensions/ changed, follow the mandatory
contract review pass in the shared extension guide
before general review lenses, even when azd-code-reviewer is not loaded.
- Explicit CLI arguments and flags take precedence over defaults. Do not prompt
toward a different default when the user supplied a valid value; preserve
deterministic
--no-promptbehavior for CI and scripts. - Reject an explicitly set flag when the code path would silently ignore it.
Use
cmd.Flags().Changed("<name>")to detect explicit flags and return a clear error for unsupported combinations. - When filtering AI models or quota data by location, associate location-specific usage data only with models available in that location. Empty or unknown usage data from another location must not make a model eligible elsewhere; add regression coverage for cross-location quota cases.