From d25650efb5f7cf45d6e1ce8d0456a48c506516b2 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 25 Nov 2025 18:24:14 +0000 Subject: [PATCH] Add ENABLE_AZURE_DEVOPS feature flag to disable Azure DevOps UI on cloud - Add ENABLE_AZURE_DEVOPS constant to enterprise/server/auth/constants.py - Add AZURE_DEVOPS_APP_CLIENT_ID and AZURE_DEVOPS_APP_CLIENT_SECRET constants - Update SaaSServerConfig to include enable_azure_devops field - Add ENABLE_AZURE_DEVOPS to FEATURE_FLAGS and PROVIDERS_CONFIGURED in get_config() - Update frontend git-settings.tsx to conditionally render Azure DevOps section based on FEATURE_FLAGS.ENABLE_AZURE_DEVOPS - Import ENABLE_AZURE_DEVOPS in enterprise/saas_server.py - Add ENABLE_AZURE_DEVOPS to frontend TypeScript types, mocks, and test files This ensures the 'Connect Azure DevOps Account' interface only appears when Azure DevOps OAuth credentials are configured, similar to existing patterns for Jira and Linear integrations. --- enterprise/saas_server.py | 1 + enterprise/server/auth/constants.py | 3 +++ enterprise/server/config.py | 7 +++++++ .../__tests__/components/chat/expandable-message.test.tsx | 1 + .../components/features/payment/payment-form.test.tsx | 1 + frontend/__tests__/routes/_oh.test.tsx | 3 +++ frontend/__tests__/routes/git-settings.test.tsx | 2 ++ frontend/__tests__/routes/home-screen.test.tsx | 2 ++ frontend/__tests__/routes/settings-with-payment.test.tsx | 3 +++ frontend/src/api/option-service/option.types.ts | 1 + frontend/src/mocks/handlers.ts | 1 + frontend/src/routes/git-settings.tsx | 5 ++++- 12 files changed, 29 insertions(+), 1 deletion(-) diff --git a/enterprise/saas_server.py b/enterprise/saas_server.py index 4c3c7c49ba..8e1afa186f 100644 --- a/enterprise/saas_server.py +++ b/enterprise/saas_server.py @@ -10,6 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware # noqa: E402 from fastapi.responses import JSONResponse # noqa: E402 from server.auth.auth_error import ExpiredError, NoCredentialsError # noqa: E402 from server.auth.constants import ( # noqa: E402 + ENABLE_AZURE_DEVOPS, ENABLE_JIRA, ENABLE_JIRA_DC, ENABLE_LINEAR, diff --git a/enterprise/server/auth/constants.py b/enterprise/server/auth/constants.py index 15d3b0f704..c1d2cd8e8b 100644 --- a/enterprise/server/auth/constants.py +++ b/enterprise/server/auth/constants.py @@ -17,7 +17,10 @@ GITLAB_APP_CLIENT_ID = os.getenv('GITLAB_APP_CLIENT_ID', '').strip() GITLAB_APP_CLIENT_SECRET = os.getenv('GITLAB_APP_CLIENT_SECRET', '').strip() BITBUCKET_APP_CLIENT_ID = os.getenv('BITBUCKET_APP_CLIENT_ID', '').strip() BITBUCKET_APP_CLIENT_SECRET = os.getenv('BITBUCKET_APP_CLIENT_SECRET', '').strip() +AZURE_DEVOPS_APP_CLIENT_ID = os.getenv('AZURE_DEVOPS_APP_CLIENT_ID', '').strip() +AZURE_DEVOPS_APP_CLIENT_SECRET = os.getenv('AZURE_DEVOPS_APP_CLIENT_SECRET', '').strip() ENABLE_ENTERPRISE_SSO = os.getenv('ENABLE_ENTERPRISE_SSO', '').strip() +ENABLE_AZURE_DEVOPS = os.environ.get('ENABLE_AZURE_DEVOPS', 'false') == 'true' ENABLE_JIRA = os.environ.get('ENABLE_JIRA', 'false') == 'true' ENABLE_JIRA_DC = os.environ.get('ENABLE_JIRA_DC', 'false') == 'true' ENABLE_LINEAR = os.environ.get('ENABLE_LINEAR', 'false') == 'true' diff --git a/enterprise/server/config.py b/enterprise/server/config.py index b978aee268..e5b997c0d7 100644 --- a/enterprise/server/config.py +++ b/enterprise/server/config.py @@ -8,7 +8,9 @@ import jwt import requests # type: ignore from fastapi import HTTPException from server.auth.constants import ( + AZURE_DEVOPS_APP_CLIENT_ID, BITBUCKET_APP_CLIENT_ID, + ENABLE_AZURE_DEVOPS, ENABLE_ENTERPRISE_SSO, ENABLE_JIRA, ENABLE_JIRA_DC, @@ -84,6 +86,7 @@ class SaaSServerConfig(ServerConfig): maintenance_start_time: str = os.environ.get( 'MAINTENANCE_START_TIME', '' ) # Timestamp in EST e.g 2025-07-29T14:18:01.219616-04:00 + enable_azure_devops = ENABLE_AZURE_DEVOPS enable_jira = ENABLE_JIRA enable_jira_dc = ENABLE_JIRA_DC enable_linear = ENABLE_LINEAR @@ -160,6 +163,9 @@ class SaaSServerConfig(ServerConfig): if BITBUCKET_APP_CLIENT_ID: providers_configured.append(ProviderType.BITBUCKET) + if AZURE_DEVOPS_APP_CLIENT_ID: + providers_configured.append(ProviderType.AZURE_DEVOPS) + if ENABLE_ENTERPRISE_SSO: providers_configured.append(ProviderType.ENTERPRISE_SSO) @@ -171,6 +177,7 @@ class SaaSServerConfig(ServerConfig): 'FEATURE_FLAGS': { 'ENABLE_BILLING': self.enable_billing, 'HIDE_LLM_SETTINGS': self.hide_llm_settings, + 'ENABLE_AZURE_DEVOPS': self.enable_azure_devops, 'ENABLE_JIRA': self.enable_jira, 'ENABLE_JIRA_DC': self.enable_jira_dc, 'ENABLE_LINEAR': self.enable_linear, diff --git a/frontend/__tests__/components/chat/expandable-message.test.tsx b/frontend/__tests__/components/chat/expandable-message.test.tsx index 0b25ef1f92..3b7610ddfd 100644 --- a/frontend/__tests__/components/chat/expandable-message.test.tsx +++ b/frontend/__tests__/components/chat/expandable-message.test.tsx @@ -123,6 +123,7 @@ describe("ExpandableMessage", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }); const RouterStub = createRoutesStub([ diff --git a/frontend/__tests__/components/features/payment/payment-form.test.tsx b/frontend/__tests__/components/features/payment/payment-form.test.tsx index 2e8d00c6f2..a4a70f4eab 100644 --- a/frontend/__tests__/components/features/payment/payment-form.test.tsx +++ b/frontend/__tests__/components/features/payment/payment-form.test.tsx @@ -38,6 +38,7 @@ describe("PaymentForm", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }); }); diff --git a/frontend/__tests__/routes/_oh.test.tsx b/frontend/__tests__/routes/_oh.test.tsx index 7737eaa7f7..21c4740655 100644 --- a/frontend/__tests__/routes/_oh.test.tsx +++ b/frontend/__tests__/routes/_oh.test.tsx @@ -80,6 +80,7 @@ describe("frontend/routes/_oh", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }); @@ -118,6 +119,7 @@ describe("frontend/routes/_oh", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }); @@ -202,6 +204,7 @@ describe("frontend/routes/_oh", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }); diff --git a/frontend/__tests__/routes/git-settings.test.tsx b/frontend/__tests__/routes/git-settings.test.tsx index 0c3f77bed0..b8d6653a62 100644 --- a/frontend/__tests__/routes/git-settings.test.tsx +++ b/frontend/__tests__/routes/git-settings.test.tsx @@ -24,6 +24,7 @@ const VALID_OSS_CONFIG: GetConfigResponse = { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }; @@ -37,6 +38,7 @@ const VALID_SAAS_CONFIG: GetConfigResponse = { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }; diff --git a/frontend/__tests__/routes/home-screen.test.tsx b/frontend/__tests__/routes/home-screen.test.tsx index a515f670be..6279270b44 100644 --- a/frontend/__tests__/routes/home-screen.test.tsx +++ b/frontend/__tests__/routes/home-screen.test.tsx @@ -404,6 +404,7 @@ describe("Settings 404", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }); const error = createAxiosNotFoundErrorObject(); @@ -429,6 +430,7 @@ describe("Setup Payment modal", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }); const error = createAxiosNotFoundErrorObject(); diff --git a/frontend/__tests__/routes/settings-with-payment.test.tsx b/frontend/__tests__/routes/settings-with-payment.test.tsx index d07567c905..f55ded2569 100644 --- a/frontend/__tests__/routes/settings-with-payment.test.tsx +++ b/frontend/__tests__/routes/settings-with-payment.test.tsx @@ -73,6 +73,7 @@ describe("Settings Billing", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }, isLoading: false, @@ -128,6 +129,7 @@ describe("Settings Billing", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }, isLoading: false, @@ -152,6 +154,7 @@ describe("Settings Billing", () => { ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, }, isLoading: false, diff --git a/frontend/src/api/option-service/option.types.ts b/frontend/src/api/option-service/option.types.ts index fa84227f0d..58247d20b1 100644 --- a/frontend/src/api/option-service/option.types.ts +++ b/frontend/src/api/option-service/option.types.ts @@ -13,6 +13,7 @@ export interface GetConfigResponse { ENABLE_JIRA: boolean; ENABLE_JIRA_DC: boolean; ENABLE_LINEAR: boolean; + ENABLE_AZURE_DEVOPS: boolean; }; MAINTENANCE?: { startTime: string; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 04f4c03ca9..b46c954950 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -180,6 +180,7 @@ export const handlers = [ ENABLE_JIRA: false, ENABLE_JIRA_DC: false, ENABLE_LINEAR: false, + ENABLE_AZURE_DEVOPS: false, }, // Uncomment the following to test the maintenance banner // MAINTENANCE: { diff --git a/frontend/src/routes/git-settings.tsx b/frontend/src/routes/git-settings.tsx index 89a25b9828..b7a353d93f 100644 --- a/frontend/src/routes/git-settings.tsx +++ b/frontend/src/routes/git-settings.tsx @@ -128,6 +128,9 @@ function GitSettingsScreen() { !bitbucketHostInputHasValue && !azureDevOpsHostInputHasValue; const shouldRenderExternalConfigureButtons = isSaas && config.APP_SLUG; + const shouldRenderAzureDevOpsSection = + shouldRenderExternalConfigureButtons && + config?.FEATURE_FLAGS?.ENABLE_AZURE_DEVOPS; const shouldRenderProjectManagementIntegrations = config?.FEATURE_FLAGS?.ENABLE_JIRA || config?.FEATURE_FLAGS?.ENABLE_JIRA_DC || @@ -153,7 +156,7 @@ function GitSettingsScreen() { )} - {shouldRenderExternalConfigureButtons && !isLoading && ( + {shouldRenderAzureDevOpsSection && !isLoading && ( <>