Sub-issue of #116. Hardened spec — do not re-derive decisions. Mirrors az pipelines update from the Azure CLI and the Python implementation at azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/pipeline_create.py#L154-L172.
Command Description
Update an existing YAML-based Azure Pipeline (build definition) in a project. The command resolves the target pipeline (a positive integer is used directly; a string is resolved via GetDefinitions), fetches the current BuildDefinition, mutates only the fields the user explicitly passed, and writes the modified definition back. Returns the updated BuildDefinition.
GET https://dev.azure.com/{organization}/{project}/_apis/build/definitions/{id}?api-version=7.1
PUT https://dev.azure.com/{organization}/{project}/_apis/build/definitions/{id}?api-version=7.1
Locked Decisions (do not re-derive)
| # |
Decision |
Rationale |
| 1 |
Use the vendored SDK build.Client.GetDefinition and build.Client.UpdateDefinition (not raw HTTP). Mocks already generated at internal/mocks/build_client_mock.go:731 (GetDefinition) and :1375 (UpdateDefinition). The GetDefinitions mock at :837 is reused for name resolution. |
Consistent with pipelines create (#254) and pipelines delete (#255). |
| 2 |
The pipeline is identified by a positional PIPELINE argument ([ORGANIZATION/]PROJECT/PIPELINE). The target segment is resolved via a new shared.ResolvePipelineDefinition helper at internal/cmd/pipelines/shared/resolve.go that mirrors Python's get_definition_id_from_name: if it parses as a positive integer, use it directly; otherwise call build.Client.GetDefinitions and pick the first match. Folds the previous --id flag into the positional. |
Mirrors sibling leaves (#243, #255, #258). One positional for either ID or name. |
| 2.5 |
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/variablegroup/{create,delete,show,update}/ precedent. |
| 3 |
util.ExactArgs(1, "pipeline target is required"). |
Standard cobra pattern; the 1st positional is the full target. |
| 4 |
The fetch-then-mutate-then-write flow preserves all fields the user did not touch (triggers, badges, retention rules, variables, etc.). All flags except the resolved target are optional. |
Mirrors Python pipeline_update semantics; safe partial-update pattern. |
| 5 |
--name (str) sets BuildDefinition.Name. Python uses new_name to avoid the id=name keyword collision in the DefinitionReference; Go has no such conflict, so --name is the natural choice and matches the Azure CLI convention. |
Aligns with az pipelines update UX. |
| 6 |
--description (str) sets BuildDefinition.Description. |
Mirrors Python pipeline_update. |
| 7 |
--branch (str) sets BuildDefinition.Repository.DefaultBranch. If Repository is nil, allocate a new BuildRepository first. |
Mirrors Python pipeline_update. |
| 8 |
--yml-path (str) sets BuildDefinition.Process to map[string]interface{}{"yamlFilename": yml_path, "type": 2}. The type: 2 constant corresponds to YAML process type in the Azure DevOps REST API. |
Mirrors Python _create_process_object. |
| 9 |
--queue-id (int) sets BuildDefinition.Queue.Id after allocating a new AgentPoolQueue if Queue is nil. |
Mirrors Python pipeline_update. |
| 10 |
--folder-path (str) sets BuildDefinition.Path. |
Mirrors Python new_folder_path. |
| 11 |
No confirmation prompt. Update is non-destructive (modifies metadata, does not destroy history or runs). |
Mirrors az pipelines update. |
| 12 |
No new SDK client, no new helper beyond shared.ResolvePipelineDefinition, no new package beyond internal/cmd/pipelines/update. |
Mandate: minimal code. |
| 13 |
Mocks for GetDefinition, UpdateDefinition, and GetDefinitions are already generated. Do not regenerate. |
Verified at internal/mocks/build_client_mock.go:731, :1375, and :837. |
| 14 |
Default output is a single-row table via transform_pipeline_table_output (ID, Path, Name, [Draft?], Status, Default Queue). JSON output via --json passes the raw SDK type to opts.exporter.Write. |
Mirrors _transform_pipeline_row from _format.py; mirrors the show-sibling convention from #203 Decision 7 / #205 Decision 11. |
| 15 |
The RepoType for the GitHub case-sensitivity hack from pipeline_create.py is not needed here (no GitHub type is constructed). |
Out of scope by data-flow shape. |
Command Signature
azdo pipelines update [ORGANIZATION/]PROJECT/PIPELINE
[--name NAME] (string: new pipeline name)
[--description DESCRIPTION] (string)
[--branch BRANCH] (string: refs/heads/main, main, etc.)
[--yml-path YAML_PATH] (string: path to the YAML file in the repo)
[--queue-id QUEUE_ID] (int: id of the agent pool queue)
[--folder-path FOLDER_PATH] (string: e.g. "user1/production")
[--json ...]
cobra.ExactArgs(1) — args[0] → target (via util.ParseProjectTargetWithDefaultOrganization).
- The
Target field of the parsed *Target is resolved via shared.ResolvePipelineDefinition(ctx, clientFact, args[0]).
- All other flags are optional; only the supplied flags mutate the definition.
Flags
| Flag |
Maps to |
Notes |
--name (str) |
BuildDefinition.Name |
Replaces existing name |
--description (str) |
BuildDefinition.Description |
Optional |
--branch (str) |
BuildDefinition.Repository.DefaultBranch |
Allocated Repository if nil |
--yml-path (str) |
BuildDefinition.Process → {"yamlFilename": ..., "type": 2} |
Replaces existing Process |
--queue-id (int) |
BuildDefinition.Queue.Id |
Allocated Queue if nil |
--folder-path (str) |
BuildDefinition.Path |
Full path of the folder |
--json / --jq / --template |
util.AddJSONFlags |
JSON export |
util.AddJSONFlags must list every JSON field exposed: id, name, path, description, revision, quality, type, queueStatus, url (mirroring the #254 and #256 show-sibling JSON surfaces).
JSON Output Contract
Pass the raw SDK type *build.BuildDefinition to opts.exporter.Write. No view struct is required (per the show-sibling convention from #203 Decision 7 / #205 Decision 11).
Table Output Contract
Mirrors transform_pipeline_table_output from _format.py. Single row with columns:
ID — *int
Path — *string, truncated to 50+.. if longer
Name — *string, truncated to 50+.. if longer
Draft (optional column) — True if *DefinitionQuality == "draft", else blank. Included only if the returned definition has quality == "draft".
Status — *DefinitionQueueStatus, blank if empty
Default Queue — *AgentPoolQueue.Name, blank if Queue is nil
The Draft column is auto-detected: include it if *BuildDefinition.Quality == "draft", otherwise omit. This mirrors the Python list pattern (include_draft_column in transform_pipelines_table_output).
Command Wiring
- Package path:
internal/cmd/pipelines/update
- Files:
update.go — NewCmd(ctx util.CmdContext) *cobra.Command + updateOptions + runUpdate
shared/resolve.go (under internal/cmd/pipelines/shared/) — ResolvePipelineDefinition(ctx, clientFact, raw) (int, error) (positive-int fast path + GetDefinitions first-match lookup)
update_test.go — table-driven gomock tests
- Update
internal/cmd/pipelines/pipelines.go to add update.NewCmd(ctx) as a top-level leaf (cmd.AddCommand(...)). Update the Example block.
- Higher-level parents must already remain wired:
pipelines → update (top-level).
API Surface
Reuse the already-vendored client. No new SDK clients or mocks required.
build.Client.GetDefinitions → for name → ID resolution.
build.Client.GetDefinition → Definitions - Get (REST 7.1)
build.Client.UpdateDefinition → Definitions - Update (REST 7.1)
build.GetDefinitionArgs struct: {DefinitionId *int, Project *string}.
build.UpdateDefinitionArgs struct: {DefinitionId *int, Definition *BuildDefinition, Project *string}.
build.BuildDefinition model — see vendor/.../build/models.go:323. Note Process interface{}, Repository *BuildRepository (line 927, has DefaultBranch *string at line 931), Queue *AgentPoolQueue (line 22).
Mocks for GetDefinitions (:837), GetDefinition (:731), and UpdateDefinition (:1375) are already generated. No mock regeneration needed.
Reference Existing Patterns
azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/pipeline_create.py#L154-L172 — pipeline_update Python implementation (the fetch-mutate-write pattern).
azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/commands.py#L99 — g.command('update', 'pipeline_update', table_transformer=transform_pipeline_table_output).
azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/_format.py — transform_pipeline_table_output and _transform_pipeline_row.
internal/cmd/pipelines/variablegroup/delete/delete.go — primary target-resolution precedent (Decision 2 / 2.5): uses Use: "delete [ORGANIZATION/]PROJECT/GROUP", util.ExactArgs(1, "..."), util.ParseProjectTargetWithDefaultOrganization, and a shared.ResolveVariableGroup helper.
internal/cmd/pipelines/variablegroup/update/update.go — closest update sibling (project-scoped, modifies a variable group by ID). Provides the partial-update pattern.
internal/cmd/pipelines/variablegroup/list/list.go — primary list reference for the modern list pattern, JSON view struct, table printer. update does not use --max-items; the table emits exactly one row.
internal/cmd/boards/workitem/list/list_test.go:765-844 — setupFakeDeps / stub* fixture.
internal/mocks/build_client_mock.go:731 — mock for GetDefinition (already generated).
internal/mocks/build_client_mock.go:1375 — mock for UpdateDefinition (already generated).
internal/mocks/build_client_mock.go:837 — mock for GetDefinitions (already generated, used for name → ID resolution).
internal/azdo/factory.go:61 — ClientFactory().Build(...) accessor (reuse).
internal/cmd/util/scope.go:183 — util.ParseProjectTargetWithDefaultOrganization (project-scoped parser).
References
Sub-issue of #116. Hardened spec — do not re-derive decisions. Mirrors
az pipelines updatefrom the Azure CLI and the Python implementation atazure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/pipeline_create.py#L154-L172.Command Description
Update an existing YAML-based Azure Pipeline (build definition) in a project. The command resolves the target pipeline (a positive integer is used directly; a string is resolved via
GetDefinitions), fetches the currentBuildDefinition, mutates only the fields the user explicitly passed, and writes the modified definition back. Returns the updatedBuildDefinition.Locked Decisions (do not re-derive)
build.Client.GetDefinitionandbuild.Client.UpdateDefinition(not raw HTTP). Mocks already generated atinternal/mocks/build_client_mock.go:731(GetDefinition) and:1375(UpdateDefinition). TheGetDefinitionsmock at:837is reused for name resolution.pipelines create(#254) andpipelines delete(#255).PIPELINEargument ([ORGANIZATION/]PROJECT/PIPELINE). The target segment is resolved via a newshared.ResolvePipelineDefinitionhelper atinternal/cmd/pipelines/shared/resolve.gothat mirrors Python'sget_definition_id_from_name: if it parses as a positive integer, use it directly; otherwise callbuild.Client.GetDefinitionsand pick the first match. Folds the previous--idflag into the positional.util.ParseProjectTargetWithDefaultOrganizationfrominternal/cmd/util/scope.go:183. The function returns a*TargetwithOrganization,Project,Targetfields, accepting 2- or 3-segment inputs.internal/cmd/pipelines/variablegroup/{create,delete,show,update}/precedent.util.ExactArgs(1, "pipeline target is required").pipeline_updatesemantics; safe partial-update pattern.--name(str) setsBuildDefinition.Name. Python usesnew_nameto avoid theid=namekeyword collision in theDefinitionReference; Go has no such conflict, so--nameis the natural choice and matches the Azure CLI convention.az pipelines updateUX.--description(str) setsBuildDefinition.Description.pipeline_update.--branch(str) setsBuildDefinition.Repository.DefaultBranch. IfRepositoryis nil, allocate a newBuildRepositoryfirst.pipeline_update.--yml-path(str) setsBuildDefinition.Processtomap[string]interface{}{"yamlFilename": yml_path, "type": 2}. Thetype: 2constant corresponds toYAMLprocess type in the Azure DevOps REST API._create_process_object.--queue-id(int) setsBuildDefinition.Queue.Idafter allocating a newAgentPoolQueueifQueueis nil.pipeline_update.--folder-path(str) setsBuildDefinition.Path.new_folder_path.az pipelines update.shared.ResolvePipelineDefinition, no new package beyondinternal/cmd/pipelines/update.GetDefinition,UpdateDefinition, andGetDefinitionsare already generated. Do not regenerate.internal/mocks/build_client_mock.go:731,:1375, and:837.transform_pipeline_table_output(ID, Path, Name, [Draft?], Status, Default Queue). JSON output via--jsonpasses the raw SDK type toopts.exporter.Write._transform_pipeline_rowfrom_format.py; mirrors the show-sibling convention from #203 Decision 7 / #205 Decision 11.RepoTypefor the GitHub case-sensitivity hack frompipeline_create.pyis not needed here (no GitHub type is constructed).Command Signature
cobra.ExactArgs(1)—args[0]→ target (viautil.ParseProjectTargetWithDefaultOrganization).Targetfield of the parsed*Targetis resolved viashared.ResolvePipelineDefinition(ctx, clientFact, args[0]).Flags
--name(str)BuildDefinition.Name--description(str)BuildDefinition.Description--branch(str)BuildDefinition.Repository.DefaultBranchRepositoryif nil--yml-path(str)BuildDefinition.Process→{"yamlFilename": ..., "type": 2}--queue-id(int)BuildDefinition.Queue.IdQueueif nil--folder-path(str)BuildDefinition.Path--json/--jq/--templateutil.AddJSONFlagsutil.AddJSONFlagsmust list every JSON field exposed:id,name,path,description,revision,quality,type,queueStatus,url(mirroring the#254and#256show-sibling JSON surfaces).JSON Output Contract
Pass the raw SDK type
*build.BuildDefinitiontoopts.exporter.Write. No view struct is required (per the show-sibling convention from #203 Decision 7 / #205 Decision 11).Table Output Contract
Mirrors
transform_pipeline_table_outputfrom_format.py. Single row with columns:ID—*intPath—*string, truncated to 50+..if longerName—*string, truncated to 50+..if longerDraft(optional column) —Trueif*DefinitionQuality == "draft", else blank. Included only if the returned definition hasquality == "draft".Status—*DefinitionQueueStatus, blank if emptyDefault Queue—*AgentPoolQueue.Name, blank ifQueueis nilThe
Draftcolumn is auto-detected: include it if*BuildDefinition.Quality == "draft", otherwise omit. This mirrors the Python list pattern (include_draft_columnintransform_pipelines_table_output).Command Wiring
internal/cmd/pipelines/updateupdate.go—NewCmd(ctx util.CmdContext) *cobra.Command+updateOptions+runUpdateshared/resolve.go(underinternal/cmd/pipelines/shared/) —ResolvePipelineDefinition(ctx, clientFact, raw) (int, error)(positive-int fast path +GetDefinitionsfirst-match lookup)update_test.go— table-driven gomock testsinternal/cmd/pipelines/pipelines.goto addupdate.NewCmd(ctx)as a top-level leaf (cmd.AddCommand(...)). Update theExampleblock.pipelines→update(top-level).API Surface
Reuse the already-vendored client. No new SDK clients or mocks required.
build.Client.GetDefinitions→ for name → ID resolution.build.Client.GetDefinition→ Definitions - Get (REST 7.1)build.Client.UpdateDefinition→ Definitions - Update (REST 7.1)build.GetDefinitionArgsstruct:{DefinitionId *int, Project *string}.build.UpdateDefinitionArgsstruct:{DefinitionId *int, Definition *BuildDefinition, Project *string}.build.BuildDefinitionmodel — seevendor/.../build/models.go:323. NoteProcess interface{},Repository *BuildRepository(line 927, hasDefaultBranch *stringat line 931),Queue *AgentPoolQueue(line 22).Mocks for
GetDefinitions(:837),GetDefinition(:731), andUpdateDefinition(:1375) are already generated. No mock regeneration needed.Reference Existing Patterns
azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/pipeline_create.py#L154-L172—pipeline_updatePython implementation (the fetch-mutate-write pattern).azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/commands.py#L99—g.command('update', 'pipeline_update', table_transformer=transform_pipeline_table_output).azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/_format.py—transform_pipeline_table_outputand_transform_pipeline_row.internal/cmd/pipelines/variablegroup/delete/delete.go— primary target-resolution precedent (Decision 2 / 2.5): usesUse: "delete [ORGANIZATION/]PROJECT/GROUP",util.ExactArgs(1, "..."),util.ParseProjectTargetWithDefaultOrganization, and ashared.ResolveVariableGrouphelper.internal/cmd/pipelines/variablegroup/update/update.go— closest update sibling (project-scoped, modifies a variable group by ID). Provides the partial-update pattern.internal/cmd/pipelines/variablegroup/list/list.go— primary list reference for the modern list pattern, JSON view struct, table printer.updatedoes not use--max-items; the table emits exactly one row.internal/cmd/boards/workitem/list/list_test.go:765-844—setupFakeDeps/stub*fixture.internal/mocks/build_client_mock.go:731— mock forGetDefinition(already generated).internal/mocks/build_client_mock.go:1375— mock forUpdateDefinition(already generated).internal/mocks/build_client_mock.go:837— mock forGetDefinitions(already generated, used for name → ID resolution).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/pipeline_create.pyazext_devops/dev/pipelines/commands.pyazext_devops/dev/pipelines/_format.py