Skip to content

feat: Implement azdo pipelines create command #254

Description

@tmeckel

Sub-issue of #116. Hardened spec — do not re-derive decisions. Mirrors az pipelines create from the Azure CLI and the Python implementation at azure-dev-ops-cli-extension/azure-dev-ops-cli-extension/azure-dev-ops/azure-devops/azext_devops/dev/pipelines/pipeline_create.py#L43-L141.

Note: The Python implementation is heavily interactive (prompts the user for template selection, parameter values, etc.). The azdo Go port scopes this down to the non-interactive subset — i.e., the flags the user must pass to drive the command without prompts. The interactive flow is intentionally out of scope (the azdo CLI is non-interactive by design).

Command Description

Create a new YAML-based Azure Pipeline in a project. The command resolves the repository (by URL, by name, or by auto-detection from the local git checkout), wires up the build definition, and (optionally) triggers the first run. Returns the created BuildDefinition (when --skip-first-run is set) or the first Build run (when the first run is allowed to proceed).

POST https://dev.azure.com/{organization}/{project}/_apis/build/definitions?api-version=7.1
POST https://dev.azure.com/{organization}/{project}/_apis/build/builds?api-version=7.1   (only when not skipping the first run)

Locked Decisions (do not re-derive)

# Decision Rationale
1 Use the vendored SDK build.Client.CreateDefinition and build.Client.QueueBuild (not raw HTTP). Mocks already generated at internal/mocks/build_client_mock.go:167 (CreateDefinition) and :1227 (QueueBuild). Consistent with build list (#211) and build show (#241).
2 The pipeline is identified post-creation by its auto-generated id. Required CLI inputs: --name (string), --repository (string, optional if --repository-type and --branch imply local git detection), --branch (string, optional only if detect is on and the local git repo has a tracked branch), --yml-path (string, optional — defaults to azure-pipelines.yml in the repo root). Mirrors az pipelines create ergonomics.
3 The positional [ORGANIZATION/]PROJECT is parsed via util.ParseProjectScope. Consistent with build list and build show.
4 --repository-type enum: tfsgit (Azure Repos) or github. Required when --repository is given as a short name (e.g. SampleOrg/SampleRepo or SampleRepoName); auto-detected from the URL when --repository is a URL. Mirrors az pipelines create ergonomics.
5 Optional: --description (string), --service-connection (string, GitHub service connection id), --queue-id (int), --skip-first-run (bool, default false — first run is allowed), --folder-path (string). Mirrors Python pipeline_create parameters.
6 Default output is a table that auto-detects the row shape (transform_pipeline_or_run_table_output): renders the created BuildDefinition if --skip-first-run is set, otherwise the first Build run. JSON output via --json passes the raw SDK type to opts.exporter.Write. Mirrors transform_pipeline_or_run_table_output from _format.py.
7 No confirmation prompt. Create is non-destructive. Mirrors az pipelines create.
8 No new SDK client, no new helper, no new package beyond internal/cmd/pipelines/create. Mandate: minimal code.
9 Mocks for CreateDefinition and QueueBuild are already generated. Do not regenerate. Verified.
10 The Python implementation's interactive template-selection flow (prompts the user to pick a YAML template and renders it via the CIX client) is out of scope. The Go port expects the YAML to already exist in the repo and only requires --yml-path to point at it. If --yml-path is omitted, the SDK defaults to azure-pipelines.yml. The azdo CLI is non-interactive by design; mirroring interactive UX is a no-go.

Command Signature

azdo pipelines create [ORGANIZATION/]PROJECT
  --name NAME                           (string, required)
  --repository REPOSITORY               (string: URL, name, or Owner/Repo)
  --branch BRANCH                       (string: refs/heads/main, main, refs/pull/1/merge, refs/tags/X)
  --repository-type tfsgit|github       (enum, default: tfsgit)
  --yml-path YAML_PATH                  (string, default: azure-pipelines.yml)
  [--description DESCRIPTION]
  [--service-connection SERVICE_CONNECTION_ID]   (GitHub only)
  [--queue-id QUEUE_ID]                 (int)
  [--folder-path FOLDER_PATH]           (string, e.g. "user1/production")
  [--skip-first-run]                    (bool)
  [--json ...]
  • cobra.ExactArgs(1)args[0] → project scope (via util.ParseProjectScope).
  • --name is required; --repository and --branch are required unless the local git context can satisfy them.

Flags

Flag Maps to Notes
--name (str, required) BuildDefinition.Name Unique within project+folder
--repository (str) BuildDefinition.Repository.{Id,Name,Url} URL or name; auto-resolved
--branch (str) BuildDefinition.Repository.DefaultBranch and Build.SourceBranch Normalized by SDK
--repository-type (enum) BuildDefinition.Repository.Type tfsgit or github
--yml-path (str) BuildDefinition.Process.YamlFilename Default azure-pipelines.yml
--description (str) BuildDefinition.Description Optional
--service-connection (str) BuildDefinition.Repository.Properties.ConnectedServiceId GitHub only
--queue-id (int) BuildDefinition.Queue.Id Override default queue
--folder-path (str) BuildDefinition.Path Folder path within the project
--skip-first-run (bool) post-create behavior If true, returns the BuildDefinition; if false, queues and returns the first Build run
--json / --jq / --template util.AddJSONFlags JSON export

JSON Output Contract

Pass the raw SDK type to opts.exporter.Write. The shape is *build.BuildDefinition when --skip-first-run is set, otherwise *build.Build. No view struct is required (per the show-sibling convention from #203 Decision 7 / #205 Decision 11).

Table Output Contract

Mirrors transform_pipeline_or_run_table_output from _format.py. The output auto-detects the row shape by sniffing for a buildNumber field:

  • If buildNumber is present (a Build), renders Run ID, Number, Status, Result, Pipeline ID, Pipeline Name, Source Branch, Queued Time, Reason.
  • Otherwise (a BuildDefinition), renders ID, Path, Name, [Draft?], Status, Default Queue.

Command Wiring

  • Package path: internal/cmd/pipelines/create
  • Files:
    • create.goNewCmd(ctx util.CmdContext) *cobra.Command + createOptions + runCreate
    • create_test.go — table-driven gomock tests
  • Update internal/cmd/pipelines/pipelines.go to add create.NewCmd(ctx) as a top-level leaf (cmd.AddCommand(...)). Update the Example block.
  • Higher-level parents must already remain wired: pipelinescreate (top-level).

API Surface

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

  • build.Client.CreateDefinitionDefinitions - Create (REST 7.1)
  • build.Client.QueueBuildBuilds - Queue (REST 7.1)
  • build.CreateDefinitionArgs struct: {Definition *BuildDefinition, Project *string}.
  • build.QueueBuildArgs struct: {Build *Build, Project *string}.
  • build.BuildDefinition model and build.BuildRepository model — see vendor/.../build/models.go.

Mocks for CreateDefinition (:167) 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