Skip to content

feat: Implement azdo pipelines run command #258

@tmeckel

Description

@tmeckel

Sub-issue of #116. Hardened spec — do not re-derive decisions. Mirrors az pipelines run from the Azure CLI and the Python implementation at azure-dev-ops-cli-extension/azure-dev-ops/azuredevops/azext_devops/dev/pipelines/pipeline.py#L100-L142.

Note: The Python implementation has two paths — a legacy build.Client.QueueBuild path (when --parameters is not supplied) and a modern v6.0 pipelines.Client.RunPipeline path (when --parameters is supplied). The modern v6.0 Pipelines client is not vendored in the Go SDK, so the --parameters path is out of scope for this issue. The Go port only implements the legacy QueueBuild path, mirroring the same scope decision as #254 (pipelines create is non-interactive only).

Command Description

Queue (run) an existing 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), builds a Build payload with the supplied branch, commit, and variables, and queues it via build.Client.QueueBuild. Returns the queued Build (i.e. the new run).

POST https://dev.azure.com/{organization}/{project}/_apis/build/builds?api-version=7.1

Locked Decisions (do not re-derive)

# Decision Rationale
1 Use the vendored SDK build.Client.QueueBuild (not raw HTTP). Mock already generated at internal/mocks/build_client_mock.go:1227 (QueueBuild). The build.Client.GetDefinitions mock at :837 is reused for name resolution. Consistent with build queue (#253) and pipelines list (#256).
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 and --name flags into a single positional. Mirrors sibling leaves (#243, #255, #257). 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 --branch (str) sets Build.SourceBranch. The SDK normalizes bare branch names (e.g. main) to refs/heads/main; the Python helper resolve_git_ref_heads does the same. We replicate the normalization in Go to avoid surfacing an error when the user passes a bare branch name. Mirrors Python resolve_git_ref_heads (line 25 of pipeline.py).
5 --commit-id (str) sets Build.SourceVersion. Mirrors Python pipeline_run.
6 --variable name=value is repeatable. Each pair is parsed on the first =. Empty value is allowed. Pairs are accumulated into Build.Parameters (*map[string]string). Mirrors Python set_param_variable (line 73 of pipeline.py).
7 --folder-path (str) is used as a filter when resolving the name to an ID; ignored if the segment is a positive integer. Mirrors Python get_definition_id_from_name(..., path=folder_path).
8 No confirmation prompt. Run is non-destructive (creates a new build). Mirrors az pipelines run.
9 No new SDK client, no new helper beyond shared.ResolvePipelineDefinition, no new package beyond internal/cmd/pipelines/run. Mandate: minimal code.
10 Mocks for QueueBuild and GetDefinitions are already generated. Do not regenerate. Verified at internal/mocks/build_client_mock.go:1227 and :837.
11 The modern v6.0 pipelines.Client.RunPipeline path (used by Python when --parameters is supplied) is out of scope. The vendored SDK does not include a pipelines package. If/when the v6.0 Pipelines client is added to the vendored SDK, a follow-up issue will re-enable --parameters. Same scope decision as #254 (non-interactive only).
12 The --open flag from Python (open the run page in the web browser) is out of scope for the Go port. The azdo CLI has no equivalent of Python's webbrowser.open_new integration; if a user wants to open the run in a browser they can copy the URL from the JSON output. Mirror existing azdo conventions; no browser opener.
13 Default output is a single-row table via transform_pipeline_run_table_output (Run ID, Number, Status, Result, Pipeline ID, Pipeline Name, Source Branch, Queued Time, Reason). JSON output via --json passes the raw SDK type to opts.exporter.Write. Mirrors _transform_pipeline_run_row from _format.py; mirrors the show-sibling convention from #203 Decision 7 / #205 Decision 11.

Command Signature

azdo pipelines run [ORGANIZATION/]PROJECT/PIPELINE
  [--branch BRANCH]                    (string: refs/heads/main, main, refs/pull/1/merge, refs/tags/X)
  [--commit-id COMMIT_ID]              (string)
  [--variable NAME=VALUE]              (repeatable: space-separated name=value pairs)
  [--folder-path FOLDER_PATH]          (string: filter for name resolution)
  [--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]).
  • --variable may be specified multiple times; values are accumulated into Build.Parameters.
  • --branch is normalized to refs/heads/{branch} if it does not already start with refs/heads/, refs/pull/, or refs/tags/.

Flags

Flag Maps to Notes
--branch (str) Build.SourceBranch Normalized to refs/heads/...
--commit-id (str) Build.SourceVersion Optional
--variable (str, repeatable) Build.Parameters (*map[string]string) name=value format
--folder-path (str) GetDefinitions.Path filter Used only during name → ID resolution
--json / --jq / --template util.AddJSONFlags JSON export

util.AddJSONFlags must list every JSON field exposed: id, buildNumber, status, result, sourceBranch, sourceVersion, queueTime, reason (mirroring the #252 and #253 build-sibling JSON surfaces, but keyed by the run-side field names).

JSON Output Contract

Pass the raw SDK type *build.Build 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_run_table_output from _format.py. Single row with columns:

  • Run ID*int
  • Number*string (build number)
  • Status*string
  • Result*string (or blank if empty)
  • Pipeline ID*Build.Definition.Id
  • Pipeline Name*Build.Definition.Name
  • Source Branch*string, with refs/heads/ prefix stripped (e.g. main not refs/heads/main)
  • Queued Time*azuredevops.Time formatted as YYYY-MM-DD HH:MM:SS in local timezone (mirrors Python's dateutil.parser.parse(...).astimezone(tzlocal()))
  • Reason*string

Command Wiring

  • Package path: internal/cmd/pipelines/run
  • Files:
    • run.goNewCmd(ctx util.CmdContext) *cobra.Command + runOptions + runRun
    • shared/resolve.go (under internal/cmd/pipelines/shared/) — ResolvePipelineDefinition(ctx, clientFact, raw) (int, error) (positive-int fast path + GetDefinitions first-match lookup)
    • run_test.go — table-driven gomock tests
  • Update internal/cmd/pipelines/pipelines.go to add run.NewCmd(ctx) as a top-level leaf (cmd.AddCommand(...)). Update the Example block.
  • Higher-level parents must already remain wired: pipelinesrun (top-level).

API Surface

Reuse the already-vendored client. No new SDK clients or mocks required.

  • build.Client.GetDefinitionsDefinitions - List (REST 7.1) — for name → ID resolution
  • build.Client.QueueBuildBuilds - Queue (REST 7.1)
  • build.GetDefinitionsArgs struct: {Project *string, Name *string, Path *string}.
  • build.GetDefinitionsResponseValue struct: {Value *[]BuildDefinitionReference, Count *int}.
  • build.BuildDefinitionReference struct: minimal reference (id, name, etc.).
  • build.QueueBuildArgs struct: {Build *Build, Project *string}.
  • build.Build model — has Definition *DefinitionReference, SourceBranch *string, SourceVersion *string, Parameters *map[string]string, plus all the standard status/result fields.
  • build.DefinitionReference struct: {Id *int, Name *string}.

Mocks for GetDefinitions (:837) and QueueBuild (:1227) are already generated. No mock regeneration needed.

Reference Existing Patterns

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions