You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parse the single positional with util.ParseProjectPathTargetWithDefaultOrganization(ctx, args[0], ...). The wrapper is added as part of the create sub-issue's implementation.
No existing helper supports the 3-segment form.
6
--new-path and --new-description are optional flags. At least one of the two must be set; if neither is, the command returns a flag error via util.FlagErrorf ("specify at least one of --new-path or --new-description").
Mirrors az pipelines folder update (requires at least one).
7
Behavior: fetch the folder at PATH via GetFolders(Project, Path), pick the unique match (the SDK returns a single folder when the path is exact), mutate the desired field(s), then call UpdateFolder(Project, Path, Folder) (the SDK's POST is full-replace, not PATCH — so the body must include the complete updated Folder).
Plain output: print Updated folder <PROJECT>/<new-path> to stdout on success, where <new-path> is the new path (or the original if only the description changed).
Mirrors variable-group update.
9
JSON output: pass the raw *build.Folder returned by UpdateFolder directly to opts.exporter.Write. No view struct.
Vendor verified at vendor/.../v7/build/client.go:2490 and :3919.
11
No new mock needed.
internal/mocks/build_client_mock.go:881-893 (GetFolders) and :1420-1432 (UpdateFolder) are already generated.
12
Predecessors: the create sub-issue (so the ParseProjectPathTargetWithDefaultOrganization wrapper exists) and the list sub-issue (so the GetFolders call to resolve the existing folder is well-understood).
13
No go mod tidy / go mod vendor / scripts/generate_mocks.sh work.
Command Signature
varupdateCmd=&cobra.Command{
Use: "update [ORGANIZATION/]PROJECT/PATH",
Aliases: []string{"u"},
Short: "Update a folder.",
Long: `Update the path or description of a build definition folder.Mirrors 'az pipelines folder update'. At least one of --new-path or--new-description must be specified. The full updated folder is sentto the server (full replace, not a partial patch).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd*cobra.Command, args []string) error {
returnrunUpdate(cmd.Context(), opts, args[0])
},
}
Flags
Flag
Type
Default
Required
Description
--new-path
string
""
no
New full path for the folder.
--new-description
string
""
no
New description for 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.
Mutex Check
After flag parsing (in runUpdate):
ifopts.newPath==""&&opts.newDescription=="" {
returnutil.FlagErrorf("specify at least one of --new-path or --new-description")
}
JSON Output Contract
Pass the raw *build.Folder returned by UpdateFolder to opts.exporter.Write. No view struct.
If --new-path was set, use the new path; otherwise, use the original path (only the description was changed).
Command Wiring
Create internal/cmd/pipelines/folder/update/update.go with package update, factory func NewCmd(ctx util.CmdContext) *cobra.Command.
Add import "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/folder/update" to internal/cmd/pipelines/folder/folder.go.
Register in folder.go with cmd.AddCommand(update.NewCmd(ctx)).
API Surface
Vendored SDK calls (from vendor/.../v7/build/client.go:2490 and :3919):
// 1. Fetch the current folder (to preserve any fields the user did not set).list, err:=client.GetFolders(ctx, build.GetFoldersArgs{
Project: &opts.project,
Path: &opts.path,
})
iferr!=nil {
returnfmt.Errorf("failed to fetch folder %s: %w", opts.path, err)
}
folders:=*listiflen(folders) ==0 {
returnfmt.Errorf("folder %s not found in project %s", opts.path, opts.project)
}
iflen(folders) >1 {
returnfmt.Errorf("path %s matched %d folders; expected exactly 1", opts.path, len(folders))
}
current:=folders[0]
// 2. Mutate the fields the user wants to change.ifopts.newPath!="" {
current.Path=&opts.newPath
}
ifopts.newDescription!="" {
current.Description=&opts.newDescription
}
// 3. Send the full updated folder (the SDK's POST is full-replace, not PATCH).updated, err:=client.UpdateFolder(ctx, build.UpdateFolderArgs{
Folder: ¤t,
Project: &opts.project,
Path: &opts.path, // the ORIGINAL path, before any rename
})
iferr!=nil {
returnfmt.Errorf("failed to update folder %s: %w", opts.path, err)
}
The vendored SDK enforces args.Folder != nil, args.Project != "", args.Path != nil on UpdateFolder — will return ArgumentNilError / ArgumentNilOrEmptyError if missing. No extra validation needed in our wrapper.
Reference Existing Patterns
internal/cmd/pipelines/variablegroup/update/update.go — primary reference. Mirrors the Use, the Aliases, the fetch→mutate→PUT-style POST pattern, the success message, and the mutex check between two update flags.
internal/cmd/pipelines/folder/list/list.go — sibling for the GetFolders SDK call signature.
internal/cmd/pipelines/folder/create/create.go — sibling for the parsing of [ORGANIZATION/]PROJECT/PATH.
TDD Test Plan
Test name
Scenario
Expected
TestNewCmd_update
Inspect the command struct.
Use == "update [ORGANIZATION/]PROJECT/PATH", Aliases == ["u"], Args == cobra.ExactArgs(1).
Test_runUpdate_success_renameOnly
--new-path=P/NewName is set. Mock GetFolders returns []build.Folder{{Path: ptr("P/OldName"), Description: ptr("d")}}. Mock UpdateFolder returns the renamed folder.
args.Path == ptr("P/OldName") (the original path) is passed to UpdateFolder; the body has Path == ptr("P/NewName"). Output: Updated folder P/NewName.
Test_runUpdate_success_descriptionOnly
--new-description=newDesc is set. Mock GetFolders returns []build.Folder{{Path: ptr("P/Foo"), Description: ptr("oldDesc")}}.
The body has Path == ptr("P/Foo") (unchanged) and Description == ptr("newDesc"). Output: Updated folder P/Foo.
Test_runUpdate_success_both
Both --new-path and --new-description are set.
The body reflects both new values.
Test_runUpdate_mutexViolation
Neither --new-path nor --new-description is set.
Returns util.FlagErrorf("specify at least one of --new-path or --new-description"). SDK is not called.
Test_runUpdate_folderNotFound
Mock GetFolders returns []build.Folder{}.
Returns a clear "folder not found" error. UpdateFolder is not called.
Test_runUpdate_pathAmbiguous
Mock GetFolders returns []build.Folder{...} with 2 entries.
Returns a clear "path matched N folders" error. UpdateFolder is not called.
Test_runUpdate_getFoldersError
Mock GetFolders returns errors.New("boom").
Returns wrapped error. UpdateFolder is not called.
Sub-issue of #262. Hardened spec — do not re-derive decisions.
Command Description
Rename or re-describe a build definition folder at
PATHunderPROJECT. Mirrorsaz pipelines folder update.Locked Decisions
Use: "update [ORGANIZATION/]PROJECT/PATH".variable-group update'sUse: "update [ORGANIZATION/]PROJECT/GROUP"(internal/cmd/pipelines/variablegroup/update/update.go).Aliases: []string{"u"}.cobra.ExactArgs(1).util.ParseProjectPathTargetWithDefaultOrganization(ctx, args[0], ...). The wrapper is added as part of thecreatesub-issue's implementation.--new-pathand--new-descriptionare optional flags. At least one of the two must be set; if neither is, the command returns a flag error viautil.FlagErrorf("specify at least one of --new-path or --new-description").az pipelines folder update(requires at least one).PATHviaGetFolders(Project, Path), pick the unique match (the SDK returns a single folder when the path is exact), mutate the desired field(s), then callUpdateFolder(Project, Path, Folder)(the SDK's POST is full-replace, not PATCH — so the body must include the complete updatedFolder).internal/cmd/pipelines/variablegroup/update/update.go(fetch → mutate → PUT-style POST).Updated folder <PROJECT>/<new-path>to stdout on success, where<new-path>is the new path (or the original if only the description changed).variable-group update.*build.Folderreturned byUpdateFolderdirectly toopts.exporter.Write. No view struct.build.Client.GetFolders+build.Client.UpdateFolder.vendor/.../v7/build/client.go:2490and:3919.internal/mocks/build_client_mock.go:881-893(GetFolders) and:1420-1432(UpdateFolder) are already generated.createsub-issue (so theParseProjectPathTargetWithDefaultOrganizationwrapper exists) and thelistsub-issue (so theGetFolderscall to resolve the existing folder is well-understood).go mod tidy/go mod vendor/scripts/generate_mocks.shwork.Command Signature
Flags
--new-pathstring""--new-descriptionstring""--organizationstring$AZDO_ORGANIZATIONor default config--projectstring""Also add
util.AddJSONFlags(cmd, &opts.exporter, ...)registering every field of*build.Folder:createdBy,createdOn,description,lastChangedBy,lastChangedDate,path,project.Mutex Check
After flag parsing (in
runUpdate):JSON Output Contract
Pass the raw
*build.Folderreturned byUpdateFoldertoopts.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:
If
--new-pathwas set, use the new path; otherwise, use the original path (only the description was changed).Command Wiring
internal/cmd/pipelines/folder/update/update.gowithpackage update, factoryfunc NewCmd(ctx util.CmdContext) *cobra.Command.import "github.com/tmeckel/azdo-cli/internal/cmd/pipelines/folder/update"tointernal/cmd/pipelines/folder/folder.go.folder.gowithcmd.AddCommand(update.NewCmd(ctx)).API Surface
Vendored SDK calls (from
vendor/.../v7/build/client.go:2490and:3919):The vendored SDK enforces
args.Folder != nil,args.Project != "",args.Path != nilonUpdateFolder— will returnArgumentNilError/ArgumentNilOrEmptyErrorif missing. No extra validation needed in our wrapper.Reference Existing Patterns
internal/cmd/pipelines/variablegroup/update/update.go— primary reference. Mirrors theUse, theAliases, the fetch→mutate→PUT-style POST pattern, the success message, and the mutex check between two update flags.internal/cmd/pipelines/folder/list/list.go— sibling for theGetFoldersSDK call signature.internal/cmd/pipelines/folder/create/create.go— sibling for the parsing of[ORGANIZATION/]PROJECT/PATH.TDD Test Plan
TestNewCmd_updateUse == "update [ORGANIZATION/]PROJECT/PATH",Aliases == ["u"],Args == cobra.ExactArgs(1).Test_runUpdate_success_renameOnly--new-path=P/NewNameis set. MockGetFoldersreturns[]build.Folder{{Path: ptr("P/OldName"), Description: ptr("d")}}. MockUpdateFolderreturns the renamed folder.args.Path == ptr("P/OldName")(the original path) is passed toUpdateFolder; the body hasPath == ptr("P/NewName"). Output:Updated folder P/NewName.Test_runUpdate_success_descriptionOnly--new-description=newDescis set. MockGetFoldersreturns[]build.Folder{{Path: ptr("P/Foo"), Description: ptr("oldDesc")}}.Path == ptr("P/Foo")(unchanged) andDescription == ptr("newDesc"). Output:Updated folder P/Foo.Test_runUpdate_success_both--new-pathand--new-descriptionare set.Test_runUpdate_mutexViolation--new-pathnor--new-descriptionis set.util.FlagErrorf("specify at least one of --new-path or --new-description"). SDK is not called.Test_runUpdate_folderNotFoundGetFoldersreturns[]build.Folder{}.UpdateFolderis not called.Test_runUpdate_pathAmbiguousGetFoldersreturns[]build.Folder{...}with 2 entries.UpdateFolderis not called.Test_runUpdate_getFoldersErrorGetFoldersreturnserrors.New("boom").UpdateFolderis not called.Test_runUpdate_updateErrorGetFoldersreturns 1 folder. MockUpdateFolderreturnserrors.New("boom").Test_runUpdate_success_JSON--jsonflag set. MockUpdateFolderreturns the renamed folder.*build.Folderwith the newPath.Test_runUpdate_missingPathazdo pipelines folder updatewith no args.References
vendor/.../v7/build/client.go:2490(GetFolders),:2516(GetFoldersArgs),:3919(UpdateFolder),:3950(UpdateFolderArgs).vendor/.../v7/build/models.go:1401(Folder).internal/mocks/build_client_mock.go:881-893(GetFolders),:1420-1432(UpdateFolder).internal/azdo/factory.go:61—ClientFactory().Build(ctx, organization).a906531b-d2da-4f55-bda7-f3e676cc50d9, API version7.1-preview.2). Full replace (not PATCH).internal/cmd/pipelines/variablegroup/update/update.go(fetch → mutate → POST, mutex check between two update flags).