Default first workspace for picker access sources

Picker providers (GitHub, Sentry, GitLab, Bitbucket, Heroku, Asana,
Netlify, ClickUp, DocuSign) require the user to pick an org/workspace in
a follow-up step after connecting. When that step is skipped, the source
stays connected but unconfigured, and its first campaign silently
resolves no users because the driver needs an org — the same "connects
fine, campaign fetches nobody" symptom seen on Sentry.

When a connector is linked to a source, auto-select the first workspace
the connection can list, so the source is usable immediately. An org the
user already chose is never overridden, and the picker stays visible
(the frontend shows the selector whenever an org is selected) so they
can switch when several are listed.

Best-effort: a provider that is unreachable or lists nothing leaves the
source in its existing needs-configuration state rather than failing the
create/update mutation.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-07-22 14:04:54 +02:00
parent c50d2657f4
commit aeb7a1c8e6

View File

@@ -628,6 +628,8 @@ func (r *mutationResolver) CreateAccessReviewSource(ctx context.Context, input t
return nil, gqlutils.Internal(ctx)
}
r.autoSelectDefaultOrganization(ctx, scope, source)
return &types.CreateAccessReviewSourcePayload{
AccessReviewSourceEdge: types.NewAccessReviewSourceEdge(source, coredata.AccessReviewSourceOrderFieldCreatedAt),
}, nil
@@ -660,6 +662,13 @@ func (r *mutationResolver) UpdateAccessReviewSource(ctx context.Context, input t
return nil, gqlutils.Internal(ctx)
}
// A connector was just (re)linked: default its org so the source is usable
// right away. Skipped on name/CSV-only updates to avoid a needless
// provider round-trip.
if input.ConnectorID.IsSet() {
r.autoSelectDefaultOrganization(ctx, scope, source)
}
return &types.UpdateAccessReviewSourcePayload{
AccessReviewSource: types.NewAccessReviewSource(source),
}, nil
@@ -717,6 +726,80 @@ func (r *mutationResolver) ConfigureAccessReviewSource(ctx context.Context, inpu
}, nil
}
// autoSelectDefaultOrganization picks the first workspace/org the connector can
// see for a freshly linked picker-provider source that has none selected yet.
// Without it a connected source stays "needs configuration" until the user
// completes the picker step; if they skip it, the first campaign silently
// resolves no users (the driver requires an org). Defaulting to the first
// available makes the source immediately usable; the picker UI stays available
// to switch when several are listed.
//
// Best-effort: any failure (provider unreachable, nothing listed) leaves the
// source in its existing "needs configuration" state, where the picker is the
// fallback. It never fails the create/update mutation that triggered it.
func (r *mutationResolver) autoSelectDefaultOrganization(
ctx context.Context,
scope coredata.Scoper,
source *coredata.AccessReviewSource,
) {
if source == nil || source.ConnectorID == nil {
return
}
httpClient, dbConnector, err := r.accessReview.ConnectorHTTPClient(ctx, scope, *source.ConnectorID)
if err != nil {
// A missing connector is not an error worth logging: the picker
// simply never surfaces a default.
if !errors.Is(err, coredata.ErrResourceNotFound) {
r.logger.WarnCtx(ctx, "cannot load connector for default organization", log.Error(err))
}
return
}
cfg, ok := providerOrgConfigs[dbConnector.Provider]
if !ok || !cfg.NeedsPicker || cfg.ListOrgs == nil {
return
}
// Never override an org the user (or an earlier default) already picked.
if cfg.SelectedSlug(dbConnector) != "" {
return
}
orgs, err := cfg.ListOrgs(ctx, httpClient)
if err != nil {
r.logger.WarnCtx(
ctx,
"cannot list provider organizations for default selection",
log.String("provider", dbConnector.Provider.String()),
log.Error(err),
)
return
}
if len(orgs) == 0 {
return
}
if _, err := r.accessReview.ConfigureAccessReviewSource(
ctx,
scope,
accessreview.ConfigureAccessReviewSourceRequest{
AccessReviewSourceID: source.ID,
OrganizationSlug: orgs[0].Slug,
},
); err != nil {
r.logger.WarnCtx(
ctx,
"cannot apply default provider organization",
log.String("provider", dbConnector.Provider.String()),
log.Error(err),
)
}
}
// CreateAccessReviewCampaign is the resolver for the createAccessReviewCampaign field.
func (r *mutationResolver) CreateAccessReviewCampaign(ctx context.Context, input types.CreateAccessReviewCampaignInput) (*types.CreateAccessReviewCampaignPayload, error) {
scope, err := r.authorize(ctx, input.OrganizationID, accessreview.ActionCampaignCreate)