Skip to content

Commit cb92bac

Browse files
committed
feat(integrations): GA Slack, Github, Gitlab, and Bitbucket API Pipelines
1 parent 1aa6667 commit cb92bac

File tree

2 files changed

+83
-44
lines changed

2 files changed

+83
-44
lines changed

static/app/utils/integrations/useAddIntegration.spec.tsx

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {useAddIntegration} from 'sentry/utils/integrations/useAddIntegration';
1212

1313
describe('useAddIntegration', () => {
1414
const provider = GitHubIntegrationProviderFixture();
15+
const legacyProvider = GitHubIntegrationProviderFixture({
16+
key: 'custom_legacy',
17+
slug: 'custom_legacy',
18+
});
1519
const integration = GitHubIntegrationFixture();
1620
let configState: Config;
1721

@@ -58,7 +62,7 @@ describe('useAddIntegration', () => {
5862

5963
act(() =>
6064
result.current.startFlow({
61-
provider,
65+
provider: legacyProvider,
6266
organization: OrganizationFixture(),
6367
onInstall: jest.fn(),
6468
})
@@ -76,7 +80,7 @@ describe('useAddIntegration', () => {
7680

7781
act(() =>
7882
result.current.startFlow({
79-
provider,
83+
provider: legacyProvider,
8084
organization: OrganizationFixture(),
8185
onInstall: jest.fn(),
8286
account: 'my-account',
@@ -96,7 +100,7 @@ describe('useAddIntegration', () => {
96100

97101
act(() =>
98102
result.current.startFlow({
99-
provider,
103+
provider: legacyProvider,
100104
organization: OrganizationFixture(),
101105
onInstall: jest.fn(),
102106
urlParams: {custom_param: 'value'},
@@ -114,7 +118,7 @@ describe('useAddIntegration', () => {
114118

115119
act(() =>
116120
result.current.startFlow({
117-
provider,
121+
provider: legacyProvider,
118122
organization: OrganizationFixture(),
119123
onInstall,
120124
})
@@ -142,7 +146,7 @@ describe('useAddIntegration', () => {
142146

143147
act(() =>
144148
result.current.startFlow({
145-
provider,
149+
provider: legacyProvider,
146150
organization: OrganizationFixture(),
147151
onInstall: jest.fn(),
148152
})
@@ -159,7 +163,7 @@ describe('useAddIntegration', () => {
159163

160164
act(() =>
161165
result.current.startFlow({
162-
provider,
166+
provider: legacyProvider,
163167
organization: OrganizationFixture(),
164168
onInstall: jest.fn(),
165169
})
@@ -176,7 +180,7 @@ describe('useAddIntegration', () => {
176180

177181
act(() =>
178182
result.current.startFlow({
179-
provider,
183+
provider: legacyProvider,
180184
organization: OrganizationFixture(),
181185
onInstall: jest.fn(),
182186
})
@@ -195,7 +199,7 @@ describe('useAddIntegration', () => {
195199

196200
act(() =>
197201
result.current.startFlow({
198-
provider,
202+
provider: legacyProvider,
199203
organization: OrganizationFixture(),
200204
onInstall,
201205
})
@@ -212,6 +216,7 @@ describe('useAddIntegration', () => {
212216
await act(async () => {
213217
await new Promise(resolve => setTimeout(resolve, 50));
214218
});
219+
215220
expect(onInstall).not.toHaveBeenCalled();
216221
});
217222

@@ -222,7 +227,7 @@ describe('useAddIntegration', () => {
222227

223228
act(() =>
224229
result.current.startFlow({
225-
provider,
230+
provider: legacyProvider,
226231
organization: OrganizationFixture(),
227232
onInstall,
228233
})
@@ -241,7 +246,7 @@ describe('useAddIntegration', () => {
241246

242247
act(() =>
243248
result.current.startFlow({
244-
provider,
249+
provider: legacyProvider,
245250
organization: OrganizationFixture(),
246251
onInstall: jest.fn(),
247252
})
@@ -253,13 +258,11 @@ describe('useAddIntegration', () => {
253258
});
254259

255260
describe('API pipeline flow', () => {
256-
it('opens the pipeline modal when feature flag is enabled', () => {
261+
it('opens the pipeline modal for unconditionally API-driven providers', () => {
257262
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');
258263
const onInstall = jest.fn();
259264

260-
const organization = OrganizationFixture({
261-
features: ['integration-api-pipeline-github'],
262-
});
265+
const organization = OrganizationFixture({features: []});
263266

264267
const {result} = renderHookWithProviders(() => useAddIntegration());
265268

@@ -282,9 +285,7 @@ describe('useAddIntegration', () => {
282285
it('passes urlParams as initialData to the pipeline modal', () => {
283286
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');
284287

285-
const organization = OrganizationFixture({
286-
features: ['integration-api-pipeline-github'],
287-
});
288+
const organization = OrganizationFixture({features: []});
288289

289290
const {result} = renderHookWithProviders(() => useAddIntegration());
290291

@@ -308,9 +309,7 @@ describe('useAddIntegration', () => {
308309
jest.spyOn(pipelineModal, 'openPipelineModal');
309310
jest.spyOn(window, 'open');
310311

311-
const organization = OrganizationFixture({
312-
features: ['integration-api-pipeline-github'],
313-
});
312+
const organization = OrganizationFixture({features: []});
314313

315314
const {result} = renderHookWithProviders(() => useAddIntegration());
316315

@@ -325,7 +324,31 @@ describe('useAddIntegration', () => {
325324
expect(window.open).not.toHaveBeenCalled();
326325
});
327326

328-
it('falls back to legacy flow when feature flag is not enabled', () => {
327+
it('opens the pipeline modal for other unconditional providers without a flag', () => {
328+
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');
329+
const organization = OrganizationFixture({features: []});
330+
const gitlabProvider = GitHubIntegrationProviderFixture({
331+
key: 'gitlab',
332+
slug: 'gitlab',
333+
name: 'GitLab',
334+
});
335+
336+
const {result} = renderHookWithProviders(() => useAddIntegration());
337+
338+
act(() =>
339+
result.current.startFlow({
340+
provider: gitlabProvider,
341+
organization,
342+
onInstall: jest.fn(),
343+
})
344+
);
345+
346+
expect(openPipelineModalSpy).toHaveBeenCalledWith(
347+
expect.objectContaining({provider: 'gitlab'})
348+
);
349+
});
350+
351+
it('falls back to legacy flow when the provider is not API driven', () => {
329352
const openPipelineModalSpy = jest.spyOn(pipelineModal, 'openPipelineModal');
330353
jest
331354
.spyOn(window, 'open')
@@ -337,7 +360,7 @@ describe('useAddIntegration', () => {
337360

338361
act(() =>
339362
result.current.startFlow({
340-
provider,
363+
provider: legacyProvider,
341364
organization,
342365
onInstall: jest.fn(),
343366
})

static/app/utils/integrations/useAddIntegration.tsx

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,44 +35,60 @@ export interface AddIntegrationParams {
3535
}
3636

3737
/**
38-
* Per-provider feature flags that gate the new API-driven pipeline setup flow.
39-
* When enabled for a provider, the integration setup uses the React pipeline
40-
* modal instead of the legacy Django view popup window.
38+
* Providers that should always use the API-driven pipeline modal.
39+
*/
40+
const UNCONDITIONAL_API_PIPELINE_PROVIDERS = [
41+
'aws_lambda',
42+
'bitbucket',
43+
'github',
44+
'gitlab',
45+
'slack',
46+
] as const satisfies ReadonlyArray<ProvidersByType['integration']>;
47+
48+
type UnconditionalApiPipelineProvider =
49+
(typeof UNCONDITIONAL_API_PIPELINE_PROVIDERS)[number];
50+
51+
/**
52+
* Providers that support the API-driven pipeline modal but still require an
53+
* organization feature flag during rollout.
4154
*
42-
* Keys are provider identifiers (constrained to registered pipeline providers
43-
* via `satisfies`), values are feature flag names without the `organizations:`
44-
* prefix.
55+
* Keys are provider identifiers, values are feature flag names without the
56+
* `organizations:` prefix.
4557
*/
46-
const API_PIPELINE_FEATURE_FLAGS = {
47-
aws_lambda: 'integration-api-pipeline-aws-lambda',
48-
bitbucket: 'integration-api-pipeline-bitbucket',
49-
github: 'integration-api-pipeline-github',
50-
gitlab: 'integration-api-pipeline-gitlab',
51-
slack: 'integration-api-pipeline-slack',
52-
} as const satisfies Partial<Record<ProvidersByType['integration'], string>>;
58+
const API_PIPELINE_FEATURE_FLAGS = {} as const satisfies Partial<
59+
Record<ProvidersByType['integration'], string>
60+
>;
5361

54-
type ApiPipelineProvider = keyof typeof API_PIPELINE_FEATURE_FLAGS;
62+
type FlaggedApiPipelineProvider = keyof typeof API_PIPELINE_FEATURE_FLAGS;
63+
type ApiPipelineProvider = UnconditionalApiPipelineProvider | FlaggedApiPipelineProvider;
5564

5665
function getApiPipelineProvider(
5766
organization: Organization,
5867
providerKey: string
5968
): ApiPipelineProvider | null {
60-
if (!(providerKey in API_PIPELINE_FEATURE_FLAGS)) {
61-
return null;
69+
if (
70+
UNCONDITIONAL_API_PIPELINE_PROVIDERS.includes(
71+
providerKey as UnconditionalApiPipelineProvider
72+
)
73+
) {
74+
return providerKey as UnconditionalApiPipelineProvider;
6275
}
63-
const key = providerKey as ApiPipelineProvider;
64-
const flag = API_PIPELINE_FEATURE_FLAGS[key];
65-
if (!organization.features.includes(flag)) {
66-
return null;
76+
77+
if (providerKey in API_PIPELINE_FEATURE_FLAGS) {
78+
const key = providerKey as keyof typeof API_PIPELINE_FEATURE_FLAGS;
79+
if (organization.features.includes(API_PIPELINE_FEATURE_FLAGS[key])) {
80+
return key;
81+
}
6782
}
68-
return key;
83+
84+
return null;
6985
}
7086

7187
/**
7288
* Opens the integration setup flow. Accepts all parameters at call time via
7389
* `startFlow(params)`, so a single hook instance can launch flows for any
7490
* provider. Automatically selects between the API-driven pipeline modal and
75-
* the legacy popup-based flow depending on the organization's feature flags.
91+
* the legacy popup-based flow based on the provider's rollout state.
7692
*
7793
* The hook manages its own `message` event listener for the legacy popup flow.
7894
* No context provider is needed.

0 commit comments

Comments
 (0)