Skip to content

feat: Implement azdo pipelines folder create command #263

Description

@tmeckel

Sub-issue of #262. Hardened spec — do not re-derive decisions.

Command Description

Create a new build definition folder at PATH under PROJECT. Mirrors az pipelines folder create.

Locked Decisions

# Decision Rationale
1 Project-scoped, with embedded path target. Folders are owned by projects.
2 Use: "create [ORGANIZATION/]PROJECT/PATH". Mirrors variable-group delete's Use: "delete [ORGANIZATION/]PROJECT/GROUP" (internal/cmd/pipelines/variablegroup/delete/delete.go:34).
3 Aliases: []string{"c", "cr"}. Per AGENTS.md "create" command convention.
4 cobra.ExactArgs(1). One positional target.
5 Parse the single positional with util.ParseProjectPathTargetWithDefaultOrganization(ctx, args[0], ...). This helper is added as part of this issue's implementation in internal/cmd/util/scope.go (mirrors the pattern from #247: a thin new wrapper that splits [ORGANIZATION/]PROJECT/PATH into 3 strings; PATH may itself contain /). No existing helper supports the 3-segment form.
6 --description is optional. Mirrors az pipelines folder create --description.
7 Non-interactive: no confirmation prompt. Create is not destructive.
8 Plain output: print Created folder <PROJECT>/<PATH> to stdout on success. Mirrors internal/cmd/repo/create/create.go.
9 JSON output: pass the raw *build.Folder from CreateFolder directly to opts.exporter.Write. No view struct. The SDK type is the API surface; expose every field.
10 Table output: N/A. Create commands print a single confirmation line, not a table.
11 Uses vendored build.Client.CreateFolder. Vendor verified at vendor/.../v7/build/client.go:584.
12 No new mock needed. internal/mocks/build_client_mock.go:182-193 already mocks CreateFolder.
13 Predecessors: none. First leaf to implement.
14 No go mod tidy / go mod vendor / scripts/generate_mocks.sh work.

Command Signature

var createCmd = &cobra.Command{
    Use:     "create [ORGANIZATION/]PROJECT/PATH",
    Aliases: []string{"c", "cr"},
    Short:   "Create a folder.",
    Long: `Create a build definition folder at PATH under PROJECT.

Mirrors 'az pipelines folder create'. PATH is the full path
(e.g. "External/CI"). Azure DevOps stores folder paths with '/'.`,
    Args: cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        return runCreate(cmd.Context(), opts, args[0])
    },
}

Flags

Flag Type Default Required Description
--description string "" no Description of the folder.
--organization string $AZDO_ORGANIZATION or default config no Azure DevOps organization. Omit to use the default.
--project string "" no Project name or ID. Omit to use the project from the positional target.

Also add util.AddJSONFlags(cmd, &opts.exporter, ...) registering every field of *build.Folder: createdBy, createdOn, description, lastChangedBy, lastChangedDate, path, project.

JSON Output Contract

Pass the raw *build.Folder returned by CreateFolder to opts.exporter.Write. No view struct.

JSON fields exposed (matching SDK struct tags): createdBy, createdOn, description, lastChangedBy, lastChangedDate, path, project.

Table Output Contract

N/A. On success, print one line to stdout:

Created folder <PROJECT>/<PATH>

where <PATH> is the path returned by the SDK (the SDK normalizes the separator to /).

Command Wiring

  1. Create internal/cmd/pipelines/folder/create/create.go with package create, factory func NewCmd(ctx util.CmdContext) *cobra.Command.
  2. Add import "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/folder/create" to internal/cmd/pipelines/folder/folder.go.
  3. Register in folder.go with cmd.AddCommand(create.NewCmd(ctx)).

API Surface

Vendored SDK call (from vendor/.../v7/build/client.go:584):

folder := &build.Folder{Description: &opts.description}
args := build.CreateFolderArgs{
    Folder:  folder,
    Project: &opts.project,
    Path:    &opts.path,
}
created, err := client.CreateFolder(ctx, args)

The vendored SDK enforces args.Folder != nil, args.Project != "", args.Path != nil — will return ArgumentNilError / ArgumentNilOrEmptyError if any are missing. No extra validation needed in our wrapper.

Reference Existing Patterns

  • internal/cmd/pipelines/variablegroup/delete/delete.go:34 — closest sibling for the [ORGANIZATION/]PROJECT/TARGET Use syntax and util.ParseProjectTargetWithDefaultOrganization (with a project-only target; this issue adds the path variant).
  • internal/cmd/repo/create/create.go — closest sibling for the non-destructive create pattern (no confirmation, single-line success message, pass-through JSON).
  • internal/cmd/util/scope.go — the new ParseProjectPathTargetWithDefaultOrganization wrapper is added as part of this issue's implementation, parallel to the wrapper added in feat: Implement azdo pipelines agent show command #247 (ParsePoolAgentTargetWithDefaultOrganization).

TDD Test Plan

Test name Scenario Expected
TestNewCmd_create Inspect the command struct. Use == "create [ORGANIZATION/]PROJECT/PATH", Aliases == ["c", "cr"], Args == cobra.ExactArgs(1).
Test_runCreate_success_noDescription Mock CreateFolder returns *build.Folder{Path: ptr("MyProject/Foo")}. Run via RunE. Returns nil error; prints Created folder MyProject/Foo to stdout.
Test_runCreate_success_withDescription Mock returns folder with Description: ptr("hello"). Returns nil error; output includes MyProject/Foo.
Test_runCreate_APIError Mock returns errors.New("boom"). Returns wrapped error.
Test_runCreate_JSON --json flag set, exporter captures output. Exporter receives a *build.Folder with Path == ptr("MyProject/Foo").
Test_runCreate_missingPath User runs azdo pipelines folder create with no args. Cobra args validation returns the standard "accepts 1 arg(s)" error.
Test_runCreate_defaultOrganization No --organization flag, default config returns "myOrg". The build client is obtained with "myOrg" via the factory.

References

  • Vendored SDK: vendor/.../v7/build/client.go:584 (CreateFolder), :615 (CreateFolderArgs).
  • Vendored model: vendor/.../v7/build/models.go:1401 (Folder).
  • Mock: internal/mocks/build_client_mock.go:182-193 (CreateFolder).
  • Internal client accessor: internal/azdo/factory.go:61ClientFactory().Build(ctx, organization).
  • Azure CLI reference: https://learn.microsoft.com/en-us/cli/azure/pipelines/folder?view=azure-cli-latest#az-pipelines-folder-create
  • Azure DevOps REST API: Folders — Create (PUT, route id a906531b-d2da-4f55-bda7-f3e676cc50d9, API version 7.1-preview.2).
  • Umbrella: Introduce azdo pipelines folder command group #262.
  • Pattern sibling: internal/cmd/pipelines/variablegroup/delete/delete.go:34 (project-scope + path-target Use syntax).
  • Pattern sibling: internal/cmd/repo/create/create.go (non-destructive create, no confirmation, pass-through JSON).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions