Files
kandev/apps/backend/internal/github/workspace_defaults.go
T
2026-08-02 14:13:04 +01:00

154 lines
4.8 KiB
Go

package github
import (
"context"
"errors"
"fmt"
"strings"
"time"
"go.uber.org/zap"
)
var errWorkspaceDefaultsUnavailable = errors.New("workspace defaults unavailable")
const (
workspaceDefaultsCLIValidationTimeout = 5 * time.Second
freshWorkspaceDefaultsTimeout = 30 * time.Second
)
// InitializeWorkspaceDefaults persists the task Git policy for a newly
// created workspace and, when the caller is allowed to use the deployment
// host identity, snapshots the active gh CLI account as a named connection.
// Account discovery and validation are deliberately best-effort: a fresh
// install without an authenticated gh CLI still gets executor credential
// inheritance and can be configured later from workspace settings.
func (s *Service) InitializeWorkspaceDefaults(ctx context.Context, workspaceID string) error {
if s == nil || s.store == nil {
return errWorkspaceDefaultsUnavailable
}
workspaceID = strings.TrimSpace(workspaceID)
if workspaceID == "" {
return fmt.Errorf("workspace_id is required")
}
if err := s.authorizeWorkspaceAccess(ctx, workspaceID); err != nil {
return err
}
if err := s.store.EnsureWorkspaceExecutorDefaults(ctx, workspaceID); err != nil {
return fmt.Errorf("persist workspace GitHub defaults: %w", err)
}
connection, err := s.store.GetWorkspaceConnection(ctx, workspaceID)
if err != nil {
return fmt.Errorf("load existing workspace GitHub connection: %w", err)
}
if connection != nil {
return nil
}
// An authenticated member may create a workspace, but must not receive the
// operator's host gh identity. Keep the authorization boundary before any
// account listing so member requests cannot observe operator accounts.
if err := requireGHCLIOperator(ctx); err != nil {
if errors.Is(err, ErrGHCLIOperatorRequired) {
return nil
}
return err
}
accounts, err := s.ListGHAccounts(ctx)
if err != nil {
s.logWorkspaceDefaultsFallback("unable to discover host gh account", workspaceID, err)
return nil
}
account, ok := activeGHAccount(accounts)
if !ok {
return nil
}
if s.connectionSecrets == nil {
s.logWorkspaceDefaultsFallback("connection secret store is unavailable", workspaceID, errWorkspaceDefaultsUnavailable)
return nil
}
bindCtx, cancel := context.WithTimeout(ctx, workspaceDefaultsCLIValidationTimeout)
defer cancel()
if _, err := s.SetWorkspaceConnection(bindCtx, workspaceID, SetWorkspaceConnectionRequest{
Source: ConnectionSourceGHCLI,
Host: account.Host,
Login: account.Login,
}); err != nil {
s.logWorkspaceDefaultsFallback("unable to bind host gh account", workspaceID, err)
}
return nil
}
// InitializeFreshWorkspaceDefaults handles the repository-seeded initial
// workspace. The store records whether its GitHub schema was absent before
// startup; existing installations are intentionally left untouched.
func (s *Service) InitializeFreshWorkspaceDefaults(ctx context.Context) error {
if s == nil || s.store == nil || !s.store.freshInstall {
return nil
}
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, freshWorkspaceDefaultsTimeout)
defer cancel()
}
s.freshDefaultsMu.Lock()
defer s.freshDefaultsMu.Unlock()
if s.freshDefaultsDone {
return nil
}
workspaceIDs, err := s.store.listWorkspaceIDs(ctx)
if err != nil {
return fmt.Errorf("list initial workspaces: %w", err)
}
for _, workspaceID := range workspaceIDs {
if err := s.InitializeWorkspaceDefaults(ctx, workspaceID); err != nil {
return err
}
}
s.freshDefaultsDone = true
return nil
}
func activeGHAccount(accounts []GHAccount) (GHAccount, bool) {
// Prefer the canonical GitHub.com account when gh reports multiple active
// hosts. Other active hosts are passed through the existing connection
// validation path, where unsupported hosts degrade to disconnected access.
for _, account := range accounts {
if validActiveGHAccount(account) && strings.EqualFold(strings.TrimSpace(account.Host), defaultGitHubHost) {
account.Host = strings.TrimSpace(account.Host)
account.Login = strings.TrimSpace(account.Login)
return account, true
}
}
for _, account := range accounts {
if validActiveGHAccount(account) {
account.Host = strings.TrimSpace(account.Host)
account.Login = strings.TrimSpace(account.Login)
return account, true
}
}
return GHAccount{}, false
}
func validActiveGHAccount(account GHAccount) bool {
if !account.Active || strings.TrimSpace(account.Host) == "" || strings.TrimSpace(account.Login) == "" {
return false
}
switch strings.ToLower(strings.TrimSpace(account.State)) {
case "active", checkStatusSuccess:
return true
default:
return false
}
}
func (s *Service) logWorkspaceDefaultsFallback(message, workspaceID string, err error) {
if s == nil || s.logger == nil {
return
}
s.logger.Warn(message, zap.String("workspace_id", workspaceID), zap.Error(err))
}