-
Notifications
You must be signed in to change notification settings - Fork 67
[FEAT]: Add team command group
#727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mcncl
wants to merge
6
commits into
main
Choose a base branch
from
SUP-6704/team_commands
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e48679f
chore: support pagination with teams list
mcncl d4f894d
chore: allow teams list pagination
mcncl abf3c67
Merge branch 'main' into SUP-6704/team_commands
mcncl 6055150
Merge remote-tracking branch 'origin/main' into SUP-6704/team_commands
mcncl 9fd2106
fix(general): refactor code with new version shape
mcncl 5e13e68
chore(feedback): fix pagination edge cases
mcncl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package team | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
|
|
||
| "github.com/alecthomas/kong" | ||
| "github.com/buildkite/cli/v3/internal/cli" | ||
| bkIO "github.com/buildkite/cli/v3/internal/io" | ||
| "github.com/buildkite/cli/v3/internal/team" | ||
| "github.com/buildkite/cli/v3/pkg/cmd/factory" | ||
| "github.com/buildkite/cli/v3/pkg/cmd/validation" | ||
| "github.com/buildkite/cli/v3/pkg/output" | ||
| buildkite "github.com/buildkite/go-buildkite/v5" | ||
| ) | ||
|
|
||
| type CreateCmd struct { | ||
| Name string `arg:"" help:"Name of the team" name:"name"` | ||
| Description string `help:"Description of the team" optional:""` | ||
| Privacy string `help:"Privacy setting for the team: visible or secret" optional:"" default:"visible" enum:"visible,secret"` | ||
| Default bool `help:"Whether this is the default team for new members" optional:"" name:"default"` | ||
| DefaultMemberRole string `help:"Default role for new members: member or maintainer" optional:"" name:"default-member-role" default:"member" enum:"member,maintainer"` | ||
| MembersCanCreatePipelines bool `help:"Whether members can create pipelines" optional:"" name:"members-can-create-pipelines"` | ||
| output.OutputFlags | ||
| } | ||
|
|
||
| func (c *CreateCmd) Help() string { | ||
| return ` | ||
| Create a new team in the organization. | ||
|
|
||
| Examples: | ||
| # Create a team with default settings | ||
| $ bk team create my-team | ||
|
|
||
| # Create a private team with a description | ||
| $ bk team create my-team --description "My team" --privacy secret | ||
|
|
||
| # Create a default team where members can create pipelines | ||
| $ bk team create my-team --default --members-can-create-pipelines | ||
| ` | ||
| } | ||
|
|
||
| func (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { | ||
| f, err := factory.New(factory.WithDebug(globals.EnableDebug())) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| f.SkipConfirm = globals.SkipConfirmation() | ||
| f.NoInput = globals.DisableInput() | ||
| f.Quiet = globals.IsQuiet() | ||
| f.NoPager = f.NoPager || globals.DisablePager() | ||
|
|
||
| if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| format := output.ResolveFormat(c.Output, f.Config.OutputFormat()) | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| input := buildkite.CreateTeam{ | ||
| Name: c.Name, | ||
| Description: c.Description, | ||
| Privacy: c.Privacy, | ||
| IsDefaultTeam: c.Default, | ||
| DefaultMemberRole: c.DefaultMemberRole, | ||
| MembersCanCreatePipelines: c.MembersCanCreatePipelines, | ||
| } | ||
|
|
||
| var t buildkite.Team | ||
| spinErr := bkIO.SpinWhile(f, "Creating team", func() error { | ||
| var err error | ||
| t, _, err = f.RestAPIClient.Teams.CreateTeam(ctx, f.Config.OrganizationSlug(), input) | ||
| return err | ||
| }) | ||
| if spinErr != nil { | ||
| return fmt.Errorf("error creating team: %v", spinErr) | ||
| } | ||
|
|
||
| teamView := output.Viewable[buildkite.Team]{ | ||
| Data: t, | ||
| Render: team.RenderTeamText, | ||
| } | ||
|
|
||
| if format != output.FormatText { | ||
| return output.Write(os.Stdout, teamView, format) | ||
| } | ||
|
|
||
| fmt.Fprintf(os.Stdout, "Team %s created successfully\n\n", t.Name) | ||
| return output.Write(os.Stdout, teamView, format) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package team | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
|
|
||
| "github.com/alecthomas/kong" | ||
| "github.com/buildkite/cli/v3/internal/cli" | ||
| bkIO "github.com/buildkite/cli/v3/internal/io" | ||
| "github.com/buildkite/cli/v3/pkg/cmd/factory" | ||
| "github.com/buildkite/cli/v3/pkg/cmd/validation" | ||
| ) | ||
|
|
||
| type DeleteCmd struct { | ||
| TeamUUID string `arg:"" help:"UUID of the team to delete" name:"team-uuid"` | ||
| } | ||
|
|
||
| func (c *DeleteCmd) Help() string { | ||
| return ` | ||
| Delete a team from the organization. | ||
|
|
||
| You will be prompted to confirm deletion unless --yes is set. | ||
|
|
||
| Examples: | ||
| # Delete a team (with confirmation prompt) | ||
| $ bk team delete my-team-uuid | ||
|
|
||
| # Delete a team without confirmation | ||
| $ bk team delete my-team-uuid --yes | ||
| ` | ||
| } | ||
|
|
||
| func (c *DeleteCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { | ||
| f, err := factory.New(factory.WithDebug(globals.EnableDebug())) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| f.SkipConfirm = globals.SkipConfirmation() | ||
| f.NoInput = globals.DisableInput() | ||
| f.Quiet = globals.IsQuiet() | ||
|
|
||
| if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| confirmed, err := bkIO.Confirm(f, fmt.Sprintf("Are you sure you want to delete team %s?", c.TeamUUID)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !confirmed { | ||
| fmt.Fprintln(os.Stderr, "Deletion cancelled.") | ||
| return nil | ||
| } | ||
|
|
||
| spinErr := bkIO.SpinWhile(f, "Deleting team", func() error { | ||
| _, err := f.RestAPIClient.Teams.DeleteTeam(ctx, f.Config.OrganizationSlug(), c.TeamUUID) | ||
| return err | ||
| }) | ||
| if spinErr != nil { | ||
| return fmt.Errorf("error deleting team: %v", spinErr) | ||
| } | ||
|
|
||
| fmt.Fprintln(os.Stderr, "Team deleted successfully.") | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| package team | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
|
|
||
| "github.com/alecthomas/kong" | ||
| "github.com/buildkite/cli/v3/internal/cli" | ||
| bkIO "github.com/buildkite/cli/v3/internal/io" | ||
| "github.com/buildkite/cli/v3/internal/team" | ||
| "github.com/buildkite/cli/v3/pkg/cmd/factory" | ||
| "github.com/buildkite/cli/v3/pkg/cmd/validation" | ||
| "github.com/buildkite/cli/v3/pkg/output" | ||
| buildkite "github.com/buildkite/go-buildkite/v5" | ||
| ) | ||
|
|
||
| type ListCmd struct { | ||
| PerPage int `help:"Number of teams per page" default:"30"` | ||
| Limit int `help:"Maximum number of teams to return" default:"100"` | ||
| output.OutputFlags | ||
| } | ||
|
|
||
| func (c *ListCmd) Validate() error { | ||
| if c.PerPage < 1 { | ||
| return fmt.Errorf("invalid --per-page %d: must be greater than 0", c.PerPage) | ||
| } | ||
|
|
||
| if c.Limit < 0 { | ||
| return fmt.Errorf("invalid --limit %d: must be greater than or equal to 0", c.Limit) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (c *ListCmd) Help() string { | ||
| return ` | ||
| List the teams for an organization. By default, shows up to 100 teams. | ||
|
|
||
| Examples: | ||
| # List all teams | ||
| $ bk team list | ||
|
|
||
| # List teams in JSON format | ||
| $ bk team list -o json | ||
|
|
||
| # List up to 200 teams | ||
| $ bk team list --limit 200 | ||
| ` | ||
| } | ||
|
|
||
| func (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { | ||
| f, err := factory.New(factory.WithDebug(globals.EnableDebug())) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| f.SkipConfirm = globals.SkipConfirmation() | ||
| f.NoInput = globals.DisableInput() | ||
| f.Quiet = globals.IsQuiet() | ||
| f.NoPager = f.NoPager || globals.DisablePager() | ||
|
|
||
| if err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| format := output.ResolveFormat(c.Output, f.Config.OutputFormat()) | ||
|
|
||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| teams, hasMore, err := listTeams(ctx, f, c.PerPage, c.Limit) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if format != output.FormatText { | ||
| return output.Write(os.Stdout, teams, format) | ||
| } | ||
|
|
||
| if len(teams) == 0 { | ||
| fmt.Fprintln(os.Stdout, "No teams found.") | ||
| return nil | ||
| } | ||
|
|
||
| summary := team.TeamViewTable(teams...) | ||
|
|
||
| writer, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager()) | ||
| defer func() { _ = cleanup() }() | ||
|
|
||
| totalDisplay := fmt.Sprintf("%d", len(teams)) | ||
| if hasMore { | ||
| totalDisplay = fmt.Sprintf("%d+", len(teams)) | ||
| } | ||
| fmt.Fprintf(writer, "Showing %s teams in %s\n\n", totalDisplay, f.Config.OrganizationSlug()) | ||
| fmt.Fprintf(writer, "%v\n", summary) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func listTeams(ctx context.Context, f *factory.Factory, perPage, limit int) ([]buildkite.Team, bool, error) { | ||
| var all []buildkite.Team | ||
| page := 1 | ||
| hasMore := false | ||
| var previousFirstTeamID string | ||
|
|
||
| for len(all) < limit { | ||
| opts := &buildkite.TeamsListOptions{ | ||
| ListOptions: buildkite.ListOptions{ | ||
| Page: page, | ||
| PerPage: perPage, | ||
| }, | ||
| } | ||
|
|
||
| var pageTeams []buildkite.Team | ||
| spinErr := bkIO.SpinWhile(f, "Loading teams", func() error { | ||
| var err error | ||
| pageTeams, _, err = f.RestAPIClient.Teams.List(ctx, f.Config.OrganizationSlug(), opts) | ||
| return err | ||
| }) | ||
| if spinErr != nil { | ||
| return nil, false, fmt.Errorf("error fetching team list: %v", spinErr) | ||
| } | ||
|
|
||
| if len(pageTeams) == 0 { | ||
| break | ||
| } | ||
|
|
||
| if page > 1 && pageTeams[0].ID == previousFirstTeamID { | ||
| return nil, false, fmt.Errorf("API returned duplicate page content at page %d, stopping pagination to prevent infinite loop", page) | ||
| } | ||
| previousFirstTeamID = pageTeams[0].ID | ||
|
|
||
| all = append(all, pageTeams...) | ||
|
|
||
| if len(all) > limit { | ||
| hasMore = true | ||
| } | ||
|
|
||
| if len(pageTeams) < perPage { | ||
|
mcncl marked this conversation as resolved.
|
||
| break | ||
| } | ||
|
|
||
| if len(all) >= limit { | ||
| hasMore = true | ||
| break | ||
| } | ||
|
|
||
| page++ | ||
| } | ||
|
|
||
| if len(all) > limit { | ||
| all = all[:limit] | ||
| } | ||
|
|
||
| return all, hasMore, nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.