package github import ( "context" "database/sql" "encoding/json" "errors" "fmt" "strings" "time" "github.com/google/uuid" ) const ( AppRegistrationCredentialsSecretPrefix = "github:app-registration:" DeploymentAppCredentialsSecretPrefix = AppRegistrationCredentialsSecretPrefix DeploymentAppCredentialBundleVersion = 1 appRegistrationCredentialsSecretName = "GitHub App registration credentials" ) var ErrDeploymentAppFlowUnavailable = errors.New("GitHub App registration flow is expired, consumed, or missing") var ErrDeploymentAppInUse = errors.New("GitHub App registration is in use") var ErrAppRegistrationImportPreparationUnavailable = errors.New("GitHub App import preparation is expired, consumed, or invalid") var ErrAppRegistrationCredentialCleanup = errors.New("GitHub App credentials could not be deleted; retry the operation") var ErrAppRegistrationDeletionFailed = errors.New("GitHub App registration could not be deleted; retry the operation") type DeploymentAppCredentials struct { PrivateKey string `json:"private_key"` ClientSecret string `json:"client_secret"` WebhookSecret string `json:"webhook_secret"` } type DeploymentAppCredentialBundle struct { Version int `json:"version"` Generation int64 `json:"generation"` Credentials DeploymentAppCredentials `json:"credentials"` } type DeploymentAppRepository struct { store *Store secrets ConnectionSecretStore } type AppRegistrationRepository = DeploymentAppRepository func NewDeploymentAppRepository(store *Store, secrets ConnectionSecretStore) *DeploymentAppRepository { return &DeploymentAppRepository{store: store, secrets: secrets} } func NewAppRegistrationRepository(store *Store, secrets ConnectionSecretStore) *AppRegistrationRepository { return NewDeploymentAppRepository(store, secrets) } const appRegistrationSelect = ` SELECT id, source, display_name, github_host, app_id, client_id, slug, owner_login, owner_type, visibility, public_base_url, COALESCE(created_for_workspace_id, '') AS created_for_workspace_id, credential_generation, credential_secret_id, status, webhook_status, last_webhook_at, COALESCE(last_error, '') AS last_error, created_at, updated_at FROM github_app_registrations` const appRegistrationInsertSQL = ` INSERT INTO github_app_registrations ( id, source, display_name, github_host, app_id, client_id, slug, owner_login, owner_type, visibility, public_base_url, created_for_workspace_id, credential_generation, credential_secret_id, status, webhook_status, last_webhook_at, last_error, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` const appRegistrationUpsertSQL = appRegistrationInsertSQL + ` ON CONFLICT(id) DO UPDATE SET source = excluded.source, display_name = excluded.display_name, github_host = excluded.github_host, app_id = excluded.app_id, client_id = excluded.client_id, slug = excluded.slug, owner_login = excluded.owner_login, owner_type = excluded.owner_type, visibility = excluded.visibility, public_base_url = excluded.public_base_url, created_for_workspace_id = excluded.created_for_workspace_id, credential_generation = excluded.credential_generation, credential_secret_id = excluded.credential_secret_id, status = excluded.status, webhook_status = excluded.webhook_status, last_webhook_at = excluded.last_webhook_at, last_error = excluded.last_error, updated_at = excluded.updated_at` func (s *Store) GetAppRegistration(ctx context.Context, registrationID string) (*AppRegistration, error) { var registration AppRegistration err := s.ro.GetContext(ctx, ®istration, s.ro.Rebind(appRegistrationSelect+` WHERE id = ?`), registrationID) if errors.Is(err, sql.ErrNoRows) { return nil, nil } return ®istration, err } func (s *Store) GetAppRegistrationByGitHubApp( ctx context.Context, githubHost string, appID int64, ) (*AppRegistration, error) { var registration AppRegistration err := s.ro.GetContext(ctx, ®istration, s.ro.Rebind(appRegistrationSelect+` WHERE github_host = ? AND app_id = ?`), githubHost, appID) if errors.Is(err, sql.ErrNoRows) { return nil, nil } return ®istration, err } func (s *Store) ListAppRegistrations(ctx context.Context) ([]*AppRegistration, error) { var registrations []*AppRegistration err := s.ro.SelectContext(ctx, ®istrations, appRegistrationSelect+` ORDER BY display_name, id`) return registrations, err } func (s *Store) UpsertDeploymentAppRegistration( ctx context.Context, registration *DeploymentAppRegistration, ) error { arguments, err := appRegistrationWriteArguments(registration) if err != nil { return err } _, err = s.db.ExecContext(ctx, s.db.Rebind(appRegistrationUpsertSQL), arguments...) return err } func (s *Store) InsertAppRegistration(ctx context.Context, registration *AppRegistration) error { arguments, err := appRegistrationWriteArguments(registration) if err != nil { return err } _, err = s.db.ExecContext(ctx, s.db.Rebind(appRegistrationInsertSQL), arguments...) return err } func appRegistrationWriteArguments(registration *AppRegistration) ([]any, error) { if registration == nil || strings.TrimSpace(registration.ID) == "" { return nil, errors.New("GitHub App registration ID is required") } now := time.Now().UTC() if registration.CreatedAt.IsZero() { registration.CreatedAt = now } registration.UpdatedAt = now return []any{ registration.ID, registration.Source, registration.DisplayName, registration.GitHubHost, registration.AppID, registration.ClientID, registration.Slug, registration.OwnerLogin, registration.OwnerType, registration.Visibility, registration.PublicBaseURL, nullString(registration.CreatedForWorkspaceID), registration.CredentialGeneration, registration.CredentialSecretID, registration.Status, registration.WebhookStatus, registration.LastWebhookAt, nullString(registration.LastError), registration.CreatedAt, registration.UpdatedAt, }, nil } func (s *Store) DeleteAppRegistration(ctx context.Context, registrationID string) error { _, err := s.db.ExecContext(ctx, s.db.Rebind( `DELETE FROM github_app_registrations WHERE id = ?`), registrationID) return err } func (s *Store) CountAppRegistrationReferences(ctx context.Context, registrationID string) (int, error) { var count int err := s.ro.GetContext(ctx, &count, s.ro.Rebind(` SELECT (SELECT COUNT(*) FROM github_workspace_connections WHERE app_registration_id = ?) + (SELECT COUNT(*) FROM github_user_connections WHERE app_registration_id = ?) + (SELECT COUNT(*) FROM github_auth_flows WHERE app_registration_id = ?)`), registrationID, registrationID, registrationID) return count, err } func (s *Store) CountAppRegistrationBindings(ctx context.Context, registrationID string) (int, error) { var count int err := s.ro.GetContext(ctx, &count, s.ro.Rebind(` SELECT (SELECT COUNT(*) FROM github_workspace_connections WHERE app_registration_id = ?) + (SELECT COUNT(*) FROM github_user_connections WHERE app_registration_id = ?)`), registrationID, registrationID) return count, err } func (s *Store) CountAppRegistrationWorkspaceBindings( ctx context.Context, registrationID string, ) (int, error) { var count int err := s.ro.GetContext(ctx, &count, s.ro.Rebind(` SELECT COUNT(*) FROM github_workspace_connections WHERE app_registration_id = ?`), registrationID) return count, err } func (s *Store) RenameAppRegistration( ctx context.Context, registrationID, displayName string, ) (*AppRegistration, error) { result, err := s.db.ExecContext(ctx, s.db.Rebind(` UPDATE github_app_registrations SET display_name = ?, updated_at = ? WHERE id = ?`), displayName, time.Now().UTC(), registrationID) if err != nil { return nil, err } count, err := result.RowsAffected() if err != nil || count == 0 { return nil, err } return s.GetAppRegistration(ctx, registrationID) } func (s *Store) DeleteAppRegistrationData(ctx context.Context, registrationID string) error { tx, err := s.db.BeginTxx(ctx, nil) if err != nil { return err } defer func() { _ = tx.Rollback() }() for _, query := range []string{ `DELETE FROM github_auth_flows WHERE app_registration_id = ?`, `DELETE FROM github_webhook_deliveries WHERE app_registration_id = ?`, `DELETE FROM github_app_import_preparations WHERE registration_id = ?`, `DELETE FROM github_app_registration_flows WHERE registration_id = ?`, `DELETE FROM github_app_registrations WHERE id = ?`, } { if _, err := tx.ExecContext(ctx, tx.Rebind(query), registrationID); err != nil { return err } } return tx.Commit() } func (s *Store) MarkAppRegistrationInvalid( ctx context.Context, registrationID, reason string, ) error { _, err := s.db.ExecContext(ctx, s.db.Rebind(` UPDATE github_app_registrations SET status = ?, last_error = ?, updated_at = ? WHERE id = ?`), AppRegistrationStatusInvalid, reason, time.Now().UTC(), registrationID) return err } func (s *Store) CreateAppRegistrationImportPreparation( ctx context.Context, preparation *AppRegistrationImportPreparation, ) error { if preparation == nil || strings.TrimSpace(preparation.RegistrationID) == "" || strings.TrimSpace(preparation.WorkspaceID) == "" || strings.TrimSpace(preparation.UserID) == "" || strings.TrimSpace(preparation.PublicBaseURL) == "" { return errors.New("GitHub App import preparation is invalid") } tx, err := s.db.BeginTxx(ctx, nil) if err != nil { return err } defer func() { _ = tx.Rollback() }() if _, err := tx.ExecContext(ctx, tx.Rebind(` UPDATE github_app_import_preparations SET consumed_at = ? WHERE workspace_id = ? AND user_id = ? AND consumed_at IS NULL`), preparation.CreatedAt, preparation.WorkspaceID, preparation.UserID); err != nil { return err } _, err = tx.ExecContext(ctx, tx.Rebind(` INSERT INTO github_app_import_preparations ( registration_id, workspace_id, user_id, public_base_url, expires_at, consumed_at, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?)`), preparation.RegistrationID, preparation.WorkspaceID, preparation.UserID, preparation.PublicBaseURL, preparation.ExpiresAt, preparation.ConsumedAt, preparation.CreatedAt) if err != nil { return err } return tx.Commit() } func (s *Store) GetAppRegistrationImportPreparation( ctx context.Context, registrationID string, ) (*AppRegistrationImportPreparation, error) { var preparation AppRegistrationImportPreparation err := s.ro.GetContext(ctx, &preparation, s.ro.Rebind(` SELECT registration_id, workspace_id, user_id, public_base_url, expires_at, consumed_at, created_at FROM github_app_import_preparations WHERE registration_id = ?`), registrationID) if errors.Is(err, sql.ErrNoRows) { return nil, nil } return &preparation, err } func (s *Store) ConsumeAppRegistrationImportPreparation( ctx context.Context, registrationID, workspaceID, userID, publicBaseURL string, now time.Time, ) (*AppRegistrationImportPreparation, error) { result, err := s.db.ExecContext(ctx, s.db.Rebind(` UPDATE github_app_import_preparations SET consumed_at = ? WHERE registration_id = ? AND workspace_id = ? AND user_id = ? AND public_base_url = ? AND consumed_at IS NULL AND expires_at > ? AND NOT EXISTS ( SELECT 1 FROM github_app_registrations registration WHERE registration.id = github_app_import_preparations.registration_id )`), now, registrationID, workspaceID, userID, publicBaseURL, now) if err != nil { return nil, err } count, err := result.RowsAffected() if err != nil { return nil, err } if count != 1 { return nil, ErrAppRegistrationImportPreparationUnavailable } return s.GetAppRegistrationImportPreparation(ctx, registrationID) } func (s *Store) DeleteAppRegistrationImportPreparationsByWorkspace( ctx context.Context, workspaceID string, ) error { if strings.TrimSpace(workspaceID) == "" { _, err := s.db.ExecContext(ctx, `DELETE FROM github_app_import_preparations`) return err } _, err := s.db.ExecContext(ctx, s.db.Rebind( `DELETE FROM github_app_import_preparations WHERE workspace_id = ?`), workspaceID) return err } func (r *DeploymentAppRepository) SaveRegistration( ctx context.Context, registration *AppRegistration, credentials DeploymentAppCredentials, ) error { if r == nil || r.store == nil || r.secrets == nil || registration == nil { return errors.New("GitHub App registration repository is not configured") } if strings.TrimSpace(registration.ID) == "" { return errors.New("GitHub App registration ID is required") } if err := validateDeploymentAppCredentials(credentials); err != nil { return err } r.store.deploymentAppPersistenceMu.Lock() defer r.store.deploymentAppPersistenceMu.Unlock() previous, err := r.store.GetAppRegistration(ctx, registration.ID) if err != nil { return err } if previous != nil && registration.CredentialGeneration != previous.CredentialGeneration+1 { return errors.New("GitHub App credential generation is not the next generation") } if previous == nil && registration.CredentialGeneration != 1 { return errors.New("initial GitHub App credential generation must be 1") } return r.saveRegistrationLocked(ctx, previous, registration, credentials) } // CreateRegistration persists a new registration and never updates an // existing catalog ID, including when another writer wins after preflight. func (r *DeploymentAppRepository) CreateRegistration( ctx context.Context, registration *AppRegistration, credentials DeploymentAppCredentials, ) error { if r == nil || r.store == nil || r.secrets == nil || registration == nil { return errors.New("GitHub App registration repository is not configured") } if strings.TrimSpace(registration.ID) == "" || registration.CredentialGeneration != 1 { return errors.New("new GitHub App registration is invalid") } if err := validateDeploymentAppCredentials(credentials); err != nil { return err } r.store.deploymentAppPersistenceMu.Lock() defer r.store.deploymentAppPersistenceMu.Unlock() existing, err := r.store.GetAppRegistration(ctx, registration.ID) if err != nil { return err } if existing != nil { return errors.New("GitHub App registration already exists") } secretID := appRegistrationCredentialSecretID(registration.ID, registration.CredentialGeneration) bundle := DeploymentAppCredentialBundle{ Version: DeploymentAppCredentialBundleVersion, Generation: registration.CredentialGeneration, Credentials: credentials, } encoded, err := json.Marshal(bundle) if err != nil { return fmt.Errorf("encode GitHub App credential bundle: %w", err) } if err := r.secrets.Set(ctx, secretID, appRegistrationCredentialsSecretName, string(encoded)); err != nil { return fmt.Errorf("store GitHub App credential bundle: %w", err) } next := *registration next.CredentialSecretID = secretID if err := r.store.InsertAppRegistration(ctx, &next); err != nil { _ = r.secrets.Delete(context.WithoutCancel(ctx), secretID) return err } *registration = next return nil } func (r *DeploymentAppRepository) saveRegistrationLocked( ctx context.Context, previous *AppRegistration, registration *AppRegistration, credentials DeploymentAppCredentials, ) error { secretID := appRegistrationCredentialSecretID(registration.ID, registration.CredentialGeneration) bundle := DeploymentAppCredentialBundle{ Version: DeploymentAppCredentialBundleVersion, Generation: registration.CredentialGeneration, Credentials: credentials, } encoded, err := json.Marshal(bundle) if err != nil { return fmt.Errorf("encode GitHub App credential bundle: %w", err) } if err := r.secrets.Set(ctx, secretID, appRegistrationCredentialsSecretName, string(encoded)); err != nil { return fmt.Errorf("store GitHub App credential bundle: %w", err) } next := *registration next.CredentialSecretID = secretID if err := r.store.UpsertDeploymentAppRegistration(ctx, &next); err != nil { _ = r.secrets.Delete(context.WithoutCancel(ctx), secretID) return err } *registration = next if previous != nil && previous.CredentialSecretID != "" && previous.CredentialSecretID != secretID { _ = r.secrets.Delete(context.WithoutCancel(ctx), previous.CredentialSecretID) } return nil } func (r *DeploymentAppRepository) LoadRegistration( ctx context.Context, registrationID string, ) (*AppRegistration, DeploymentAppCredentials, error) { if r == nil || r.store == nil || r.secrets == nil { return nil, DeploymentAppCredentials{}, errors.New("GitHub App registration repository is not configured") } registration, err := r.store.GetAppRegistration(ctx, registrationID) if err != nil || registration == nil { return registration, DeploymentAppCredentials{}, err } if registration.CredentialSecretID == "" { return registration, DeploymentAppCredentials{}, errors.New("GitHub App credential pointer is missing") } encoded, err := r.secrets.Reveal(ctx, registration.CredentialSecretID) if err != nil { return registration, DeploymentAppCredentials{}, fmt.Errorf("load GitHub App credential bundle: %w", err) } bundle, err := decodeDeploymentAppCredentialBundle(encoded) if err != nil { return registration, DeploymentAppCredentials{}, err } if bundle.Generation != registration.CredentialGeneration { return registration, DeploymentAppCredentials{}, errors.New("GitHub App credential generation mismatch") } return registration, bundle.Credentials, nil } func (r *DeploymentAppRepository) DeleteRegistration(ctx context.Context, registrationID string) error { if r == nil || r.store == nil || r.secrets == nil { return errors.New("GitHub App registration repository is not configured") } r.store.deploymentAppPersistenceMu.Lock() defer r.store.deploymentAppPersistenceMu.Unlock() references, err := r.store.CountAppRegistrationBindings(ctx, registrationID) if err != nil { return err } if references > 0 { return ErrDeploymentAppInUse } registration, err := r.store.GetAppRegistration(ctx, registrationID) if err != nil { return err } if registration == nil { return r.store.DeleteAppRegistrationData(ctx, registrationID) } cleanupCtx := context.WithoutCancel(ctx) encoded, err := r.secrets.Reveal(cleanupCtx, registration.CredentialSecretID) if err != nil { return errors.Join( ErrAppRegistrationCredentialCleanup, r.invalidateRegistrationAfterCredentialFailure(cleanupCtx, registration.ID), ) } if err := r.secrets.Delete(cleanupCtx, registration.CredentialSecretID); err != nil { if restoreErr := r.ensureRegistrationCredential( cleanupCtx, registration.CredentialSecretID, encoded, ); restoreErr != nil { return errors.Join( ErrAppRegistrationCredentialCleanup, r.invalidateRegistrationAfterCredentialFailure(cleanupCtx, registration.ID), ) } return ErrAppRegistrationCredentialCleanup } if err := r.store.DeleteAppRegistrationData(ctx, registrationID); err != nil { if restoreErr := r.secrets.Set( cleanupCtx, registration.CredentialSecretID, appRegistrationCredentialsSecretName, encoded, ); restoreErr != nil { return errors.Join( ErrAppRegistrationDeletionFailed, ErrAppRegistrationCredentialCleanup, r.invalidateRegistrationAfterCredentialFailure(cleanupCtx, registration.ID), ) } return ErrAppRegistrationDeletionFailed } return nil } func (r *DeploymentAppRepository) ensureRegistrationCredential( ctx context.Context, secretID, encoded string, ) error { exists, err := r.secrets.Exists(ctx, secretID) if err == nil && exists { return nil } return r.secrets.Set(ctx, secretID, appRegistrationCredentialsSecretName, encoded) } func (r *DeploymentAppRepository) invalidateRegistrationAfterCredentialFailure( ctx context.Context, registrationID string, ) error { if err := r.store.MarkAppRegistrationInvalid( ctx, registrationID, "GitHub App credentials are unavailable after a failed cleanup", ); err != nil { return ErrAppRegistrationDeletionFailed } return nil } func (r *DeploymentAppRepository) CleanupOrphanedCredentialBundles(ctx context.Context) error { if r == nil || r.store == nil || r.secrets == nil { return errors.New("GitHub App registration repository is not configured") } r.store.deploymentAppPersistenceMu.Lock() defer r.store.deploymentAppPersistenceMu.Unlock() registrations, err := r.store.ListAppRegistrations(ctx) if err != nil { return err } activeIDs := make(map[string]struct{}, len(registrations)) for _, registration := range registrations { activeIDs[registration.CredentialSecretID] = struct{}{} } ids, err := r.secrets.ListIDs(ctx) if err != nil { return err } var cleanupErr error for _, id := range ids { if !isDeploymentAppCredentialSecretID(id) { continue } if _, active := activeIDs[id]; active { continue } if err := r.secrets.Delete(ctx, id); err != nil { cleanupErr = errors.Join(cleanupErr, err) } } return cleanupErr } func appRegistrationCredentialSecretID(registrationID string, generation int64) string { return fmt.Sprintf("%s%s:g%d:%s", AppRegistrationCredentialsSecretPrefix, registrationID, generation, uuid.NewString()) } func isDeploymentAppCredentialSecretID(id string) bool { return strings.HasPrefix(id, AppRegistrationCredentialsSecretPrefix) } func decodeDeploymentAppCredentialBundle(encoded string) (DeploymentAppCredentialBundle, error) { var bundle DeploymentAppCredentialBundle if err := json.Unmarshal([]byte(encoded), &bundle); err != nil { return bundle, errors.New("GitHub App credential bundle is invalid") } if bundle.Version != DeploymentAppCredentialBundleVersion || bundle.Generation < 1 { return bundle, errors.New("GitHub App credential bundle version is unsupported") } if err := validateDeploymentAppCredentials(bundle.Credentials); err != nil { return bundle, err } return bundle, nil } func validateDeploymentAppCredentials(credentials DeploymentAppCredentials) error { if strings.TrimSpace(credentials.PrivateKey) == "" || credentials.ClientSecret == "" || credentials.WebhookSecret == "" { return errors.New("GitHub App credentials are incomplete") } return nil } func (s *Store) CreateDeploymentAppRegistrationFlow( ctx context.Context, flow *DeploymentAppRegistrationFlow, ) error { if flow == nil || flow.WorkspaceID == "" || flow.RegistrationID == "" { return fmt.Errorf("workspace-bound GitHub App registration flow is required") } if flow.UserID == "" { flow.UserID = flow.OperatorUserID } if flow.CreatedAt.IsZero() { flow.CreatedAt = time.Now().UTC() } tx, err := s.db.BeginTxx(ctx, nil) if err != nil { return err } defer func() { _ = tx.Rollback() }() if _, err := tx.ExecContext(ctx, tx.Rebind(` UPDATE github_app_registration_flows SET consumed_at = ? WHERE workspace_id = ? AND consumed_at IS NULL`), flow.CreatedAt, flow.WorkspaceID); err != nil { return err } _, err = tx.ExecContext(ctx, tx.Rebind(` INSERT INTO github_app_registration_flows ( state_hash, registration_id, workspace_id, user_id, owner_type, owner_login, display_name, visibility, public_base_url, manifest_revision, expires_at, consumed_at, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), flow.StateHash, flow.RegistrationID, flow.WorkspaceID, flow.UserID, flow.OwnerType, flow.OwnerLogin, flow.DisplayName, flow.Visibility, flow.PublicBaseURL, flow.ManifestRevision, flow.ExpiresAt, flow.ConsumedAt, flow.CreatedAt) if err != nil { return err } return tx.Commit() } func (s *Store) GetDeploymentAppRegistrationFlow( ctx context.Context, stateHash string, ) (*DeploymentAppRegistrationFlow, error) { var flow DeploymentAppRegistrationFlow err := s.ro.GetContext(ctx, &flow, s.ro.Rebind(` SELECT state_hash, registration_id, workspace_id, user_id, owner_type, owner_login, display_name, visibility, public_base_url, manifest_revision, expires_at, consumed_at, created_at FROM github_app_registration_flows WHERE state_hash = ?`), stateHash) if errors.Is(err, sql.ErrNoRows) { return nil, nil } flow.OperatorUserID = flow.UserID return &flow, err } func (s *Store) ConsumeDeploymentAppRegistrationFlow( ctx context.Context, stateHash, registrationID string, now time.Time, ) (*DeploymentAppRegistrationFlow, error) { result, err := s.db.ExecContext(ctx, s.db.Rebind(` UPDATE github_app_registration_flows SET consumed_at = ? WHERE state_hash = ? AND registration_id = ? AND consumed_at IS NULL AND expires_at > ?`), now, stateHash, registrationID, now) if err != nil { return nil, err } count, err := result.RowsAffected() if err != nil { return nil, err } if count != 1 { return nil, ErrDeploymentAppFlowUnavailable } return s.GetDeploymentAppRegistrationFlow(ctx, stateHash) } func (s *Store) IsLatestDeploymentAppRegistrationFlow(ctx context.Context, stateHash string) (bool, error) { var count int err := s.ro.GetContext(ctx, &count, s.ro.Rebind(` SELECT COUNT(*) FROM github_app_registration_flows candidate WHERE candidate.state_hash = ? AND NOT EXISTS ( SELECT 1 FROM github_app_registration_flows newer WHERE newer.workspace_id = candidate.workspace_id AND newer.created_at > candidate.created_at )`), stateHash) return count == 1, err } func (s *Store) DeleteDeploymentAppRegistrationFlows(ctx context.Context) error { _, err := s.db.ExecContext(ctx, `DELETE FROM github_app_registration_flows`) return err } func (s *Store) DeleteAppRegistrationFlowsByWorkspace(ctx context.Context, workspaceID string) error { _, err := s.db.ExecContext(ctx, s.db.Rebind( `DELETE FROM github_app_registration_flows WHERE workspace_id = ?`), workspaceID) return err }