Skip to content

Commit 21d1258

Browse files
committed
feat: add create / delete repository action
1 parent 26ecd86 commit 21d1258

File tree

1 file changed

+363
-0
lines changed

1 file changed

+363
-0
lines changed

pkg/connector/repository.go

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"strconv"
88
"strings"
99

10+
config "github.com/conductorone/baton-sdk/pb/c1/config/v1"
1011
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
12+
"github.com/conductorone/baton-sdk/pkg/actions"
1113
"github.com/conductorone/baton-sdk/pkg/annotations"
1214
"github.com/conductorone/baton-sdk/pkg/pagination"
1315
"github.com/conductorone/baton-sdk/pkg/types/entitlement"
@@ -18,6 +20,7 @@ import (
1820
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
1921
"go.uber.org/zap"
2022
"google.golang.org/grpc/codes"
23+
"google.golang.org/protobuf/types/known/structpb"
2124
)
2225

2326
// outside collaborators are given one of these roles too.
@@ -433,3 +436,363 @@ func skipGrantsForResourceType(bag *pagination.Bag) (string, error) {
433436
}
434437
return pageToken, nil
435438
}
439+
440+
// ResourceActions registers the resource actions for the repository resource type.
441+
// This implements the ResourceActionProvider interface.
442+
func (o *repositoryResourceType) ResourceActions(ctx context.Context, registry actions.ResourceTypeActionRegistry) error {
443+
if err := o.registerCreateRepositoryAction(ctx, registry); err != nil {
444+
return err
445+
}
446+
if err := o.registerDeleteRepositoryAction(ctx, registry); err != nil {
447+
return err
448+
}
449+
return nil
450+
}
451+
452+
func (o *repositoryResourceType) registerCreateRepositoryAction(ctx context.Context, registry actions.ResourceTypeActionRegistry) error {
453+
return registry.Register(ctx, &v2.ResourceActionSchema{
454+
Name: "create",
455+
DisplayName: "Create Repository",
456+
Description: "Create a new repository in a GitHub organization",
457+
ActionType: []v2.ActionType{v2.ActionType_ACTION_TYPE_RESOURCE_CREATE},
458+
Arguments: []*config.Field{
459+
{
460+
Name: "name",
461+
DisplayName: "Repository Name",
462+
Description: "The name of the repository to create",
463+
Field: &config.Field_StringField{},
464+
IsRequired: true,
465+
},
466+
{
467+
Name: "parent",
468+
DisplayName: "Parent Organization",
469+
Description: "The organization to create the repository in",
470+
Field: &config.Field_ResourceIdField{},
471+
IsRequired: true,
472+
},
473+
{
474+
Name: "description",
475+
DisplayName: "Description",
476+
Description: "A description of the repository",
477+
Field: &config.Field_StringField{},
478+
},
479+
{
480+
Name: "private",
481+
DisplayName: "Private",
482+
Description: "Whether the repository should be private (true/false)",
483+
Field: &config.Field_BoolField{},
484+
},
485+
{
486+
Name: "visibility",
487+
DisplayName: "Visibility",
488+
Description: "The visibility level: 'public', 'private', or 'internal'",
489+
Field: &config.Field_StringField{},
490+
},
491+
{
492+
Name: "has_issues",
493+
DisplayName: "Has Issues",
494+
Description: "Enable issues for this repository (true/false)",
495+
Field: &config.Field_BoolField{},
496+
},
497+
{
498+
Name: "has_projects",
499+
DisplayName: "Has Projects",
500+
Description: "Enable projects for this repository (true/false)",
501+
Field: &config.Field_BoolField{},
502+
},
503+
{
504+
Name: "has_wiki",
505+
DisplayName: "Has Wiki",
506+
Description: "Enable wiki for this repository (true/false)",
507+
Field: &config.Field_BoolField{},
508+
},
509+
{
510+
Name: "has_discussions",
511+
DisplayName: "Has Discussions",
512+
Description: "Enable discussions for this repository (true/false)",
513+
Field: &config.Field_BoolField{},
514+
},
515+
{
516+
Name: "auto_init",
517+
DisplayName: "Auto Initialize",
518+
Description: "Create an initial commit with empty README (true/false)",
519+
Field: &config.Field_BoolField{},
520+
},
521+
{
522+
Name: "gitignore_template",
523+
DisplayName: "Gitignore Template",
524+
Description: "Gitignore template to apply (e.g., 'Go', 'Python', 'Node')",
525+
Field: &config.Field_StringField{},
526+
},
527+
{
528+
Name: "license_template",
529+
DisplayName: "License Template",
530+
Description: "License template to apply (e.g., 'mit', 'apache-2.0', 'gpl-3.0')",
531+
Field: &config.Field_StringField{},
532+
},
533+
{
534+
Name: "allow_squash_merge",
535+
DisplayName: "Allow Squash Merge",
536+
Description: "Allow squash-merging pull requests (true/false)",
537+
Field: &config.Field_BoolField{},
538+
},
539+
{
540+
Name: "allow_merge_commit",
541+
DisplayName: "Allow Merge Commit",
542+
Description: "Allow merging pull requests with a merge commit (true/false)",
543+
Field: &config.Field_BoolField{},
544+
},
545+
{
546+
Name: "allow_rebase_merge",
547+
DisplayName: "Allow Rebase Merge",
548+
Description: "Allow rebase-merging pull requests (true/false)",
549+
Field: &config.Field_BoolField{},
550+
},
551+
{
552+
Name: "allow_auto_merge",
553+
DisplayName: "Allow Auto Merge",
554+
Description: "Allow auto-merge on pull requests (true/false)",
555+
Field: &config.Field_BoolField{},
556+
},
557+
{
558+
Name: "delete_branch_on_merge",
559+
DisplayName: "Delete Branch on Merge",
560+
Description: "Automatically delete head branches after pull requests are merged (true/false)",
561+
Field: &config.Field_BoolField{},
562+
},
563+
{
564+
Name: "is_template",
565+
DisplayName: "Is Template",
566+
Description: "Make this repository available as a template (true/false)",
567+
Field: &config.Field_BoolField{},
568+
},
569+
},
570+
ReturnTypes: []*config.Field{
571+
{Name: "success", Field: &config.Field_BoolField{}},
572+
{Name: "resource", Field: &config.Field_ResourceField{}},
573+
},
574+
}, o.handleCreateRepositoryAction)
575+
}
576+
577+
func (o *repositoryResourceType) registerDeleteRepositoryAction(ctx context.Context, registry actions.ResourceTypeActionRegistry) error {
578+
return registry.Register(ctx, &v2.ResourceActionSchema{
579+
Name: "delete",
580+
DisplayName: "Delete Repository",
581+
Description: "Delete a repository from a GitHub organization",
582+
ActionType: []v2.ActionType{v2.ActionType_ACTION_TYPE_RESOURCE_DELETE},
583+
Arguments: []*config.Field{
584+
{
585+
Name: "resource",
586+
DisplayName: "Repository Resource",
587+
Description: "The repository resource to delete",
588+
Field: &config.Field_ResourceIdField{},
589+
IsRequired: true,
590+
},
591+
{
592+
Name: "parent",
593+
DisplayName: "Parent Organization",
594+
Description: "The organization the repository belongs to",
595+
Field: &config.Field_ResourceIdField{},
596+
IsRequired: true,
597+
},
598+
},
599+
ReturnTypes: []*config.Field{
600+
{Name: "success", Field: &config.Field_BoolField{}},
601+
},
602+
}, o.handleDeleteRepositoryAction)
603+
}
604+
605+
func (o *repositoryResourceType) handleCreateRepositoryAction(ctx context.Context, args *structpb.Struct) (*structpb.Struct, annotations.Annotations, error) {
606+
l := ctxzap.Extract(ctx)
607+
608+
// Extract required arguments using SDK helpers
609+
name, err := actions.RequireStringArg(args, "name")
610+
if err != nil {
611+
return nil, nil, err
612+
}
613+
614+
parentResourceID, err := actions.RequireResourceIDArg(args, "parent")
615+
if err != nil {
616+
return nil, nil, err
617+
}
618+
619+
// Get the organization name from the parent resource ID
620+
orgName, err := o.orgCache.GetOrgName(ctx, parentResourceID)
621+
if err != nil {
622+
return nil, nil, fmt.Errorf("failed to get organization name: %w", err)
623+
}
624+
625+
l.Info("github-connector: creating repository via action",
626+
zap.String("repo_name", name),
627+
zap.String("org_name", orgName),
628+
)
629+
630+
// Build the Repository request
631+
newRepo := &github.Repository{
632+
Name: github.Ptr(name),
633+
}
634+
635+
// Extract optional fields using SDK helpers
636+
if description, ok := actions.GetStringArg(args, "description"); ok && description != "" {
637+
newRepo.Description = github.Ptr(description)
638+
}
639+
640+
if private, ok := actions.GetBoolArg(args, "private"); ok {
641+
newRepo.Private = github.Ptr(private)
642+
}
643+
644+
if visibility, ok := actions.GetStringArg(args, "visibility"); ok && visibility != "" {
645+
if visibility == "public" || visibility == "private" || visibility == "internal" {
646+
newRepo.Visibility = github.Ptr(visibility)
647+
} else {
648+
l.Warn("github-connector: invalid visibility value, using default",
649+
zap.String("provided_visibility", visibility),
650+
)
651+
}
652+
}
653+
654+
if hasIssues, ok := actions.GetBoolArg(args, "has_issues"); ok {
655+
newRepo.HasIssues = github.Ptr(hasIssues)
656+
}
657+
658+
if hasProjects, ok := actions.GetBoolArg(args, "has_projects"); ok {
659+
newRepo.HasProjects = github.Ptr(hasProjects)
660+
}
661+
662+
if hasWiki, ok := actions.GetBoolArg(args, "has_wiki"); ok {
663+
newRepo.HasWiki = github.Ptr(hasWiki)
664+
}
665+
666+
if hasDiscussions, ok := actions.GetBoolArg(args, "has_discussions"); ok {
667+
newRepo.HasDiscussions = github.Ptr(hasDiscussions)
668+
}
669+
670+
if autoInit, ok := actions.GetBoolArg(args, "auto_init"); ok {
671+
newRepo.AutoInit = github.Ptr(autoInit)
672+
}
673+
674+
if gitignoreTemplate, ok := actions.GetStringArg(args, "gitignore_template"); ok && gitignoreTemplate != "" {
675+
newRepo.GitignoreTemplate = github.Ptr(gitignoreTemplate)
676+
}
677+
678+
if licenseTemplate, ok := actions.GetStringArg(args, "license_template"); ok && licenseTemplate != "" {
679+
newRepo.LicenseTemplate = github.Ptr(licenseTemplate)
680+
}
681+
682+
if allowSquashMerge, ok := actions.GetBoolArg(args, "allow_squash_merge"); ok {
683+
newRepo.AllowSquashMerge = github.Ptr(allowSquashMerge)
684+
}
685+
686+
if allowMergeCommit, ok := actions.GetBoolArg(args, "allow_merge_commit"); ok {
687+
newRepo.AllowMergeCommit = github.Ptr(allowMergeCommit)
688+
}
689+
690+
if allowRebaseMerge, ok := actions.GetBoolArg(args, "allow_rebase_merge"); ok {
691+
newRepo.AllowRebaseMerge = github.Ptr(allowRebaseMerge)
692+
}
693+
694+
if allowAutoMerge, ok := actions.GetBoolArg(args, "allow_auto_merge"); ok {
695+
newRepo.AllowAutoMerge = github.Ptr(allowAutoMerge)
696+
}
697+
698+
if deleteBranchOnMerge, ok := actions.GetBoolArg(args, "delete_branch_on_merge"); ok {
699+
newRepo.DeleteBranchOnMerge = github.Ptr(deleteBranchOnMerge)
700+
}
701+
702+
if isTemplate, ok := actions.GetBoolArg(args, "is_template"); ok {
703+
newRepo.IsTemplate = github.Ptr(isTemplate)
704+
}
705+
706+
// Create the repository via GitHub API
707+
createdRepo, resp, err := o.client.Repositories.Create(ctx, orgName, newRepo)
708+
if err != nil {
709+
return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to create repository %s in org %s", name, orgName))
710+
}
711+
712+
// Extract rate limit data for annotations
713+
var annos annotations.Annotations
714+
if rateLimitData, err := extractRateLimitData(resp); err == nil {
715+
annos.WithRateLimiting(rateLimitData)
716+
}
717+
718+
l.Info("github-connector: repository created successfully via action",
719+
zap.String("repo_name", createdRepo.GetName()),
720+
zap.Int64("repo_id", createdRepo.GetID()),
721+
zap.String("repo_full_name", createdRepo.GetFullName()),
722+
)
723+
724+
// Create the resource representation of the newly created repository
725+
repoResource, err := repositoryResource(ctx, createdRepo, parentResourceID)
726+
if err != nil {
727+
return nil, annos, fmt.Errorf("failed to create resource representation: %w", err)
728+
}
729+
730+
// Build return values using SDK helpers
731+
resourceRv, err := actions.NewResourceReturnField("resource", repoResource)
732+
if err != nil {
733+
return nil, annos, err
734+
}
735+
736+
return actions.NewReturnValues(true, resourceRv), annos, nil
737+
}
738+
739+
func (o *repositoryResourceType) handleDeleteRepositoryAction(ctx context.Context, args *structpb.Struct) (*structpb.Struct, annotations.Annotations, error) {
740+
l := ctxzap.Extract(ctx)
741+
742+
// Extract the repository resource ID using SDK helper
743+
resourceID, err := actions.RequireResourceIDArg(args, "resource")
744+
if err != nil {
745+
return nil, nil, err
746+
}
747+
748+
// Extract the parent org resource ID using SDK helper
749+
parentResourceID, err := actions.RequireResourceIDArg(args, "parent")
750+
if err != nil {
751+
return nil, nil, err
752+
}
753+
754+
// Parse the repo ID from the resource
755+
repoID, err := strconv.ParseInt(resourceID.Resource, 10, 64)
756+
if err != nil {
757+
return nil, nil, fmt.Errorf("invalid repository ID %s: %w", resourceID.Resource, err)
758+
}
759+
760+
// Get the organization name from the parent resource ID
761+
orgName, err := o.orgCache.GetOrgName(ctx, parentResourceID)
762+
if err != nil {
763+
return nil, nil, fmt.Errorf("failed to get organization name: %w", err)
764+
}
765+
766+
// First, get the repository to find its name (needed for deletion)
767+
repo, resp, err := o.client.Repositories.GetByID(ctx, repoID)
768+
if err != nil {
769+
return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to get repository %d", repoID))
770+
}
771+
772+
repoName := repo.GetName()
773+
774+
l.Info("github-connector: deleting repository via action",
775+
zap.Int64("repo_id", repoID),
776+
zap.String("repo_name", repoName),
777+
zap.String("org_name", orgName),
778+
)
779+
780+
// Delete the repository via GitHub API
781+
resp, err = o.client.Repositories.Delete(ctx, orgName, repoName)
782+
if err != nil {
783+
return nil, nil, wrapGitHubError(err, resp, fmt.Sprintf("failed to delete repository %s in org %s", repoName, orgName))
784+
}
785+
786+
var annos annotations.Annotations
787+
if rateLimitData, err := extractRateLimitData(resp); err == nil {
788+
annos.WithRateLimiting(rateLimitData)
789+
}
790+
791+
l.Info("github-connector: repository deleted successfully via action",
792+
zap.Int64("repo_id", repoID),
793+
zap.String("repo_name", repoName),
794+
zap.String("org_name", orgName),
795+
)
796+
797+
return actions.NewReturnValues(true), annos, nil
798+
}

0 commit comments

Comments
 (0)