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 ([ORG:]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:391. 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 [ORG:]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 [ORG:]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:391util.ParseProjectTargetWithDefaultOrganization (project-scoped parser).

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions