Files
kandev/apps/backend/internal/plugins/repository_provider.go
T

149 lines
5.2 KiB
Go

package plugins
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/kandev/kandev/internal/plugins/manifest"
"github.com/kandev/kandev/internal/plugins/store"
"github.com/kandev/kandev/pkg/pluginsdk"
)
const (
repositoryBranchesActionKey = "repositories.branches"
repositoryActionBodyKey = "repository"
maxRepositoryBranchResponseSize = 1 << 20
maxRepositoryBranchCount = 10_000
repositoryBranchActionTimeout = 15 * time.Second
)
// RepositoryProviderSource is the host-verified identity sent to a plugin
// that owns the repository's provider. Browser input never populates it.
type RepositoryProviderSource struct {
Provider string
ProviderHost string
ProviderScope string
ProviderRepositoryID string
OwnerOrProject string
Name string
CloneURL string
DefaultBranch string
}
type RepositoryProviderBranch struct {
Name string `json:"name"`
Commit string `json:"commit,omitempty"`
IsDefault bool `json:"is_default,omitempty"`
}
type repositoryActionInvoker func(
context.Context,
string,
pluginDispatchGeneration,
*pluginsdk.PluginActionRequest,
) (*pluginsdk.PluginActionResponse, error)
// ListRepositoryProviderBranches invokes the standardized workspace-scoped
// branch action on the active plugin that owns provider.
func (s *Service) ListRepositoryProviderBranches(
ctx context.Context,
workspaceID string,
source RepositoryProviderSource,
) ([]RepositoryProviderBranch, error) {
return s.listRepositoryProviderBranches(ctx, workspaceID, source, s.InvokeAction)
}
func (s *Service) listRepositoryProviderBranches(
ctx context.Context,
workspaceID string,
source RepositoryProviderSource,
invoke repositoryActionInvoker,
) ([]RepositoryProviderBranch, error) {
if strings.TrimSpace(workspaceID) == "" || strings.TrimSpace(source.Provider) == "" {
return nil, fmt.Errorf("plugins: workspace and repository provider are required")
}
record, action, err := s.repositoryProviderAction(source.Provider, repositoryBranchesActionKey)
if err != nil {
return nil, err
}
body, err := json.Marshal(map[string]any{repositoryActionBodyKey: map[string]any{
"provider_id": source.Provider,
"provider_host": source.ProviderHost,
"provider_scope": source.ProviderScope,
"provider_repository_id": source.ProviderRepositoryID,
"owner_or_project": source.OwnerOrProject,
"name": source.Name,
"clone_url": source.CloneURL,
"default_branch": source.DefaultBranch,
}})
if err != nil {
return nil, fmt.Errorf("plugins: encode repository branch request: %w", err)
}
if len(body) > action.MaxBodyBytes {
return nil, fmt.Errorf("plugins: repository branch request exceeds declared action limit")
}
invokeCtx, cancel := context.WithTimeout(ctx, repositoryBranchActionTimeout)
defer cancel()
response, err := invoke(invokeCtx, record.ID, dispatchGeneration(record), &pluginsdk.PluginActionRequest{
ActionKey: repositoryBranchesActionKey,
Context: pluginsdk.VerifiedActionContext{WorkspaceID: workspaceID},
Body: body,
})
if err != nil {
return nil, fmt.Errorf("plugins: list %s repository branches: %w", source.Provider, err)
}
return parseRepositoryProviderBranches(response)
}
func (s *Service) repositoryProviderAction(provider, key string) (*store.Record, manifest.Action, error) {
owner, found := s.registry.activeRepositoryProviderOwner(provider, "")
if !found {
return nil, manifest.Action{}, fmt.Errorf("plugins: no active plugin owns repository provider %q", provider)
}
record, err := s.Get(owner)
if err != nil {
return nil, manifest.Action{}, err
}
for _, action := range record.Actions {
if action.Key == key && action.ResourceScope == manifest.ActionScopeWorkspace {
return record, action, nil
}
}
return nil, manifest.Action{}, fmt.Errorf("plugins: repository provider %q does not declare %s", provider, key)
}
func parseRepositoryProviderBranches(response *pluginsdk.PluginActionResponse) ([]RepositoryProviderBranch, error) {
if response == nil || len(response.Body) == 0 {
return nil, fmt.Errorf("plugins: repository branch action returned an empty response")
}
if len(response.Body) > maxRepositoryBranchResponseSize {
return nil, fmt.Errorf("plugins: repository branch action response exceeds maximum size")
}
var payload struct {
Branches []RepositoryProviderBranch `json:"branches"`
}
if err := json.Unmarshal(response.Body, &payload); err != nil {
return nil, fmt.Errorf("plugins: decode repository branch response: %w", err)
}
if len(payload.Branches) > maxRepositoryBranchCount {
return nil, fmt.Errorf("plugins: repository branch action returned too many branches")
}
seen := make(map[string]struct{}, len(payload.Branches))
branches := make([]RepositoryProviderBranch, 0, len(payload.Branches))
for _, branch := range payload.Branches {
branch.Name = strings.TrimSpace(branch.Name)
if branch.Name == "" {
return nil, fmt.Errorf("plugins: repository branch action returned an empty branch name")
}
if _, found := seen[branch.Name]; found {
continue
}
seen[branch.Name] = struct{}{}
branches = append(branches, branch)
}
return branches, nil
}