Skip to content

feat: Implement azdo pipelines update command #257

Description

@tmeckel

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.goNewCmd(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: pipelinesupdate (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.GetDefinitionDefinitions - Get (REST 7.1)
  • build.Client.UpdateDefinitionDefinitions - 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-L172pipeline_update Python implementation (the fetch-mutate-write pattern).
  • azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/commands.py#L99g.command('update', 'pipeline_update', table_transformer=transform_pipeline_table_output).
  • azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/_format.pytransform_pipeline_table_output and _transform_pipeline_row.
  • internal/cmd/pipelines/variablegroup/delete/delete.goprimary 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.goclosest update sibling (project-scoped, modifies a variable group by ID). Provides the partial-update pattern.
  • internal/cmd/pipelines/variablegroup/list/list.goprimary 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-844setupFakeDeps / 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:61ClientFactory().Build(...) accessor (reuse).
  • internal/cmd/util/scope.go:183util.ParseProjectTargetWithDefaultOrganization (project-scoped parser).

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions