Sub-issue of #276. Hardened spec — do not re-derive decisions. Mirrors az pipelines build tag add from the Azure CLI and the Python implementation at azure-devops/azext_devops/dev/pipelines/build.py#L184-L196.
Command Description
Add one or more tags to a single Azure Pipelines build identified by its numeric build ID. The command accepts either a single tag value (calls the singular SDK method) or multiple tag values (calls the bulk SDK method), and returns the resulting set of tags on the build.
PUT https://dev.azure.com/{organization}/{project}/_apis/build/builds/{buildId}/tags/{tag}?api-version=7.1-preview.3 (single tag)
POST https://dev.azure.com/{organization}/{project}/_apis/build/builds/{buildId}/tags?api-version=7.1-preview.3 (multiple tags)
Locked Decisions (do not re-derive)
| # |
Decision |
Rationale |
| 1 |
Use the vendored SDK methods build.Client.AddBuildTag (singular) and build.Client.AddBuildTags (bulk) — not raw HTTP. Mocks already generated at internal/mocks/build_client_mock.go:46 and :61. |
Mandate: vendored API. |
| 2 |
The build is identified by a positional BUILD argument ([ORGANIZATION/]PROJECT/BUILD). The BUILD segment must parse as a positive integer (no name resolution). |
Mirrors cancel (#252) and queue (#253). |
| 3 |
Parse the positional using util.ParseProjectTargetWithDefaultOrganization from internal/cmd/util/scope.go:183. The function returns a *Target with Organization, Project, Target fields, accepting 2- or 3-segment inputs. |
Mirrors internal/cmd/pipelines/build/{cancel,queue}/ precedent. |
| 4 |
The --tag flag is repeatable (cobra StringSliceVarP). Multiple occurrences accumulate into a []string. Internally: if len(tags) == 1, call AddBuildTag; if len(tags) >= 2, call AddBuildTags (bulk). |
Mirrors Python's split-on-comma behavior; preserves both singular and bulk SDK paths. |
| 5 |
--tag is required (minimum one occurrence). Cobra validation errors with util.FlagErrorf if zero --tag values are provided. |
Mirrors az pipelines build tag add which requires --tags. |
| 6 |
The user is not required to pass a project; the project segment in the positional is optional. When omitted, the project is resolved from the default config. |
Mirrors all other pipelines build leaves. |
| 7 |
Default output is a table. JSON output via --json passes the raw SDK result *[]string to opts.exporter.Write (a *string-element slice; the Go SDK unmarshals it as a []string of tag names). |
No view struct needed for a list of strings; mirrors the show-sibling convention. |
| 8 |
No confirmation prompt. Adding a tag is reversible (delete via #278) and idempotent at the server. |
Mirrors az pipelines build tag add. |
| 9 |
Tag names are not validated client-side. The Azure DevOps REST API accepts any non-empty string. Empty --tag "" would be rejected by cobra's StringSliceVarP parser (it trims empties only if the implementation explicitly does so). |
Keep the surface minimal; trust the server. |
| 10 |
No new SDK client, no new helper, no new package beyond internal/cmd/pipelines/build/tag/add. |
Mandate: minimal code. |
| 11 |
Mocks for AddBuildTag and AddBuildTags are already generated. Do not regenerate. |
Verified at internal/mocks/build_client_mock.go:46 and :61. |
Command Signature
azdo pipelines build tag add [ORGANIZATION/]PROJECT/BUILD
--tag TAG (repeatable; minimum one required)
[--json ...]
cobra.ExactArgs(1) — args[0] → target (via util.ParseProjectTargetWithDefaultOrganization).
- The
Target field is parsed as int; non-numeric / zero / negative values rejected with util.FlagErrorf.
Flags
| Flag |
Maps to |
Notes |
--tag (str, repeatable) |
AddBuildTagArgs.Tag (single) or AddBuildTagsArgs.Tags (bulk) |
cobra.StringSliceVarP; first occurrence also bound to -t |
--json / --jq / --template |
util.AddJSONFlags |
JSON export |
JSON Output Contract
Pass the raw SDK result *[]string to opts.exporter.Write. The result is a list of tag name strings. No view struct is required. The slice elements are strings (tag names), not pointers — the SDK uses a []string for the response.
Caveat: the SDK returns *[]string (a pointer to a slice of strings). When the slice is non-nil, the elements are dereferenced automatically. The exporter should handle both nil and empty-slice cases by emitting null and [] respectively.
Table Output Contract
Mirrors transform_build_tags_output from _format.py (single column Tags). Each row's only field is the tag name string.
| Tags |
| release |
| nightly |
| v1.2.3 |
The header is Tags (uppercase, single column). One row per tag returned by the SDK.
Command Wiring
- Package path:
internal/cmd/pipelines/build/tag/add
- Files:
add.go — NewCmd(ctx util.CmdContext) *cobra.Command + addOptions + runAdd
add_test.go — table-driven gomock tests
- Update
internal/cmd/pipelines/build/tag/tag.go to add add.NewCmd(ctx) to cmd.AddCommand(...). Update the Example block.
- Higher-level parents must already remain wired:
pipelines → build → tag → add.
API Surface
Reuse the already-vendored client. No new SDK clients or mocks required.
Mocks for AddBuildTag (:46) and AddBuildTags (:61) are already generated. No mock regeneration needed.
Reference Existing Patterns
References
Sub-issue of #276. Hardened spec — do not re-derive decisions. Mirrors
az pipelines build tag addfrom the Azure CLI and the Python implementation atazure-devops/azext_devops/dev/pipelines/build.py#L184-L196.Command Description
Add one or more tags to a single Azure Pipelines build identified by its numeric build ID. The command accepts either a single tag value (calls the singular SDK method) or multiple tag values (calls the bulk SDK method), and returns the resulting set of tags on the build.
Locked Decisions (do not re-derive)
build.Client.AddBuildTag(singular) andbuild.Client.AddBuildTags(bulk) — not raw HTTP. Mocks already generated atinternal/mocks/build_client_mock.go:46and:61.BUILDargument ([ORGANIZATION/]PROJECT/BUILD). TheBUILDsegment must parse as a positive integer (no name resolution).cancel(#252) andqueue(#253).util.ParseProjectTargetWithDefaultOrganizationfrominternal/cmd/util/scope.go:183. The function returns a*TargetwithOrganization,Project,Targetfields, accepting 2- or 3-segment inputs.internal/cmd/pipelines/build/{cancel,queue}/precedent.--tagflag is repeatable (cobraStringSliceVarP). Multiple occurrences accumulate into a[]string. Internally: iflen(tags) == 1, callAddBuildTag; iflen(tags) >= 2, callAddBuildTags(bulk).--tagis required (minimum one occurrence). Cobra validation errors withutil.FlagErrorfif zero--tagvalues are provided.az pipelines build tag addwhich requires--tags.pipelines buildleaves.--jsonpasses the raw SDK result*[]stringtoopts.exporter.Write(a*string-element slice; the Go SDK unmarshals it as a[]stringof tag names).az pipelines build tag add.--tag ""would be rejected by cobra'sStringSliceVarPparser (it trims empties only if the implementation explicitly does so).internal/cmd/pipelines/build/tag/add.AddBuildTagandAddBuildTagsare already generated. Do not regenerate.internal/mocks/build_client_mock.go:46and:61.Command Signature
cobra.ExactArgs(1)—args[0]→ target (viautil.ParseProjectTargetWithDefaultOrganization).Targetfield is parsed asint; non-numeric / zero / negative values rejected withutil.FlagErrorf.Flags
--tag(str, repeatable)AddBuildTagArgs.Tag(single) orAddBuildTagsArgs.Tags(bulk)cobra.StringSliceVarP; first occurrence also bound to-t--json/--jq/--templateutil.AddJSONFlagsJSON Output Contract
Pass the raw SDK result
*[]stringtoopts.exporter.Write. The result is a list of tag name strings. No view struct is required. The slice elements are strings (tag names), not pointers — the SDK uses a[]stringfor the response.Table Output Contract
Mirrors
transform_build_tags_outputfrom_format.py(single columnTags). Each row's only field is the tag name string.The header is
Tags(uppercase, single column). One row per tag returned by the SDK.Command Wiring
internal/cmd/pipelines/build/tag/addadd.go—NewCmd(ctx util.CmdContext) *cobra.Command+addOptions+runAddadd_test.go— table-driven gomock testsinternal/cmd/pipelines/build/tag/tag.goto addadd.NewCmd(ctx)tocmd.AddCommand(...). Update theExampleblock.pipelines→build→tag→add.API Surface
Reuse the already-vendored client. No new SDK clients or mocks required.
build.Client.AddBuildTag→ Tags - Add (REST 7.1-preview.3, PUT single tag)build.Client.AddBuildTags→ Tags - Add Bulk (REST 7.1-preview.3, POST multiple tags)build.AddBuildTagArgsstruct:{Project *string, BuildId *int, Tag *string}.build.AddBuildTagsArgsstruct:{Project *string, BuildId *int, Tags *[]string}.*[]string(the updated set of tags on the build).Mocks for
AddBuildTag(:46) andAddBuildTags(:61) are already generated. No mock regeneration needed.Reference Existing Patterns
azure-devops/azext_devops/dev/pipelines/build.py#L184-L196—add_build_tagsPython implementation. Splits thetagsargument on,; calls singular or bulk SDK based on count.azure-devops/azext_devops/dev/pipelines/commands.py—g.command('add', 'add_build_tags', table_transformer=transform_build_tags_output).azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/_format.py—transform_build_tags_output(single-columnTagstable).azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/arguments.py— flag definitions forpipelines build(no special args for tag subcommand; default--build-idmapping applies).internal/cmd/pipelines/build/cancel/cancel.go(feat: Implementazdo pipelines build cancelcommand #252) — primary target-resolution precedent: usesUse: "cancel [ORGANIZATION/]PROJECT/BUILD",cobra.ExactArgs(1),util.ParseProjectTargetWithDefaultOrganization.addis non-destructive, so the destructive confirmation flow is dropped.internal/cmd/pipelines/build/queue/queue.go(feat: Implementazdo pipelines build queuecommand #253) — secondary target-resolution precedent: uses the same parser and the samecobra.ExactArgs(1)validation.internal/cmd/boards/workitem/list/list_test.go:765-844—setupFakeDeps/stub*fixture.internal/mocks/build_client_mock.go:46— mock forAddBuildTag(already generated).internal/mocks/build_client_mock.go:61— mock forAddBuildTags(already generated).internal/azdo/factory.go:61—ClientFactory().Build(...)accessor (reuse).internal/cmd/util/scope.go:183—util.ParseProjectTargetWithDefaultOrganization(project-scoped parser).References
azext_devops/dev/pipelines/build.pyazext_devops/dev/pipelines/commands.pyazext_devops/dev/pipelines/_format.py