From 8107b2ca74625524d488ebf783bd51e1588521cc Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Tue, 14 Oct 2025 21:07:18 +0000 Subject: [PATCH 01/10] [CLC-2032] Block login and workspace operations for Classic PAYG sunset Implement feature flag-based blocking for Gitpod Classic PAYG users: Backend: - Add utility functions to check if user is blocked by sunset - Block login attempts in /login route handler, redirect to app.ona.com - Block workspace creation and start operations in workspace-service-api - Exempt users with roles/permissions and users in exempted organizations Frontend: - Update login page to show 'Login with Ona' button when sunset is enabled - Keep SSO login form visible for exempted organizations - Hide sunset notice banner when flag is enabled - Update heading to 'Gitpod Classic has sunset' Feature flag: classic_payg_sunset_enabled (JSON with enabled boolean and exemptedOrganizations array) Co-authored-by: Ona --- components/dashboard/src/Login.tsx | 24 ++++++++++--- .../dashboard/src/data/featureflag-query.ts | 1 + .../server/src/api/workspace-service-api.ts | 17 +++++++++ components/server/src/user/user-controller.ts | 14 ++++++++ components/server/src/util/featureflags.ts | 35 +++++++++++++++++++ 5 files changed, 87 insertions(+), 4 deletions(-) diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index f51c370024d73e..d6f17f1b04eb85 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -23,7 +23,7 @@ import { Button, ButtonProps } from "@podkit/buttons/Button"; import { cn } from "@podkit/lib/cn"; import { userClient } from "./service/public-api"; import { ProductLogo } from "./components/ProductLogo"; -import { useIsDataOps } from "./data/featureflag-query"; +import { useIsDataOps, useFeatureFlag } from "./data/featureflag-query"; import { LoadingState } from "@podkit/loading/LoadingState"; import { isGitpodIo } from "./utils"; import onaWordmark from "./images/ona-wordmark.svg"; @@ -217,6 +217,7 @@ const LoginContent = ({ const { setUser } = useContext(UserContext); const isDataOps = useIsDataOps(); const isGitpodIoUser = isGitpodIo(); + const classicSunsetEnabled = useFeatureFlag("classic_payg_sunset_enabled"); const authProviders = useAuthProviderDescriptions(); const [errorMessage, setErrorMessage] = useState(undefined); @@ -294,6 +295,11 @@ const LoginContent = ({ ) : !isGitpodIoUser ? ( Log in to Gitpod + ) : classicSunsetEnabled ? ( + <> + Gitpod Classic has sunset + Continue with Ona + ) : ( <> Log in to Gitpod Classic @@ -303,7 +309,17 @@ const LoginContent = ({
- {providerFromContext ? ( + {classicSunsetEnabled && isGitpodIoUser ? ( + { + window.location.href = "https://app.ona.com/login"; + }} + > + + Login with Ona + + + ) : providerFromContext ? ( openLogin(providerFromContext!.host)} @@ -327,8 +343,8 @@ const LoginContent = ({
{errorMessage && } - {/* Gitpod Classic sunset notice - only show for non-enterprise */} - {!enterprise && ( + {/* Gitpod Classic sunset notice - only show for non-enterprise and when NOT sunset */} + {!enterprise && !classicSunsetEnabled && (

Gitpod Classic sunsets Oct 15.{" "} diff --git a/components/dashboard/src/data/featureflag-query.ts b/components/dashboard/src/data/featureflag-query.ts index 653cc02694b34c..5aa26a8a353885 100644 --- a/components/dashboard/src/data/featureflag-query.ts +++ b/components/dashboard/src/data/featureflag-query.ts @@ -26,6 +26,7 @@ const featureFlags = { enabled_configuration_prebuild_full_clone: false, enterprise_onboarding_enabled: false, commit_annotation_setting_enabled: false, + classic_payg_sunset_enabled: false, }; type FeatureFlags = typeof featureFlags; diff --git a/components/server/src/api/workspace-service-api.ts b/components/server/src/api/workspace-service-api.ts index 4a99ba3890966d..56e5d19b01e1e8 100644 --- a/components/server/src/api/workspace-service-api.ts +++ b/components/server/src/api/workspace-service-api.ts @@ -61,6 +61,7 @@ import { UserService } from "../user/user-service"; import { ContextParser } from "../workspace/context-parser-service"; import { isWorkspaceId } from "@gitpod/gitpod-protocol/lib/util/parse-workspace-id"; import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; +import { isUserBlockedBySunset } from "../util/featureflags"; @injectable() export class WorkspaceServiceAPI implements ServiceImpl { @@ -70,6 +71,15 @@ export class WorkspaceServiceAPI implements ServiceImpl { + if (await isUserBlockedBySunset(user)) { + throw new ApplicationError( + ErrorCodes.PERMISSION_DENIED, + "Gitpod Classic PAYG has sunset. Please visit https://app.ona.com/login to continue.", + ); + } + } + async getWorkspace(req: GetWorkspaceRequest, _: HandlerContext): Promise { if (!isWorkspaceId(req.workspaceId)) { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "a valid workspaceId is required"); @@ -198,6 +208,9 @@ export class WorkspaceServiceAPI implements ServiceImpl GitpodServerImpl; @@ -69,6 +70,19 @@ export class UserController { router.get("/login", async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (req.isAuthenticated()) { log.info("(Auth) User is already authenticated.", { "login-flow": true }); + + // Check if authenticated user is blocked by sunset + const user = req.user as User; + if (await isUserBlockedBySunset(user)) { + log.info("(Auth) User blocked by Classic PAYG sunset", { + userId: user.id, + organizationId: user.organizationId, + "login-flow": true, + }); + res.redirect(302, "https://app.ona.com/login"); + return; + } + // redirect immediately const redirectTo = this.ensureSafeReturnToParam(req) || this.config.hostUrl.asDashboard().toString(); safeFragmentRedirect(res, redirectTo); diff --git a/components/server/src/util/featureflags.ts b/components/server/src/util/featureflags.ts index 479da1b09eda65..0f746b7d4b634d 100644 --- a/components/server/src/util/featureflags.ts +++ b/components/server/src/util/featureflags.ts @@ -5,9 +5,44 @@ */ import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; +import { User } from "@gitpod/gitpod-protocol"; export async function getFeatureFlagEnableExperimentalJBTB(userId: string): Promise { return getExperimentsClientForBackend().getValueAsync("enable_experimental_jbtb", false, { user: { id: userId }, }); } + +export interface ClassicPaygSunsetConfig { + enabled: boolean; + exemptedOrganizations: string[]; +} + +export async function getClassicPaygSunsetConfig(userId: string): Promise { + return getExperimentsClientForBackend().getValueAsync( + "classic_payg_sunset_enabled", + { enabled: false, exemptedOrganizations: [] }, + { user: { id: userId } }, + ); +} + +export async function isUserBlockedBySunset(user: User): Promise { + const config = await getClassicPaygSunsetConfig(user.id); + + if (!config.enabled) { + return false; + } + + // Users with roles/permissions are exempted (admins, etc.) + if (user.rolesOrPermissions && user.rolesOrPermissions.length > 0) { + return false; + } + + // If user has an org, check if it's exempted + if (user.organizationId) { + return !config.exemptedOrganizations.includes(user.organizationId); + } + + // Installation-owned users (no organizationId) are blocked + return true; +} From a8cda63ef5275b70685115c2ed06ad4dc2cc4a3d Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Wed, 15 Oct 2025 06:08:07 +0000 Subject: [PATCH 02/10] Add oldLogin parameter to show full login UI for exempted orgs When sunset is enabled on gitpod.io, users now see a simplified UI: - 'Continue with Ona' button (default) - Link to show all login options (?oldLogin=true) With ?oldLogin=true parameter: - Shows all OAuth provider buttons - Shows SSO login form - Full functionality for exempted organizations The link preserves returnToPath parameter if present. Co-authored-by: Ona --- components/dashboard/src/Login.tsx | 56 +++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index d6f17f1b04eb85..acc386af0f78db 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -225,6 +225,13 @@ const LoginContent = ({ const { data: installationConfig } = useInstallationConfiguration(); const enterprise = !!installationConfig?.isDedicatedInstallation; + // Check if user wants to see all login options (for exempted orgs) + const searchParams = useMemo(() => new URLSearchParams(window.location.search), []); + const oldLogin = searchParams.get("oldLogin") === "true"; + + // Show sunset UI only if: sunset enabled, on gitpod.io, and user hasn't requested old login + const showSunsetUI = classicSunsetEnabled && isGitpodIoUser && !oldLogin; + const updateUser = useCallback(async () => { await getGitpodService().reconnect(); const { user } = await userClient.getAuthenticatedUser({}); @@ -293,13 +300,13 @@ const LoginContent = ({ Open a cloud development environment for the repository {repoPathname?.slice(1)} - ) : !isGitpodIoUser ? ( - Log in to Gitpod - ) : classicSunsetEnabled ? ( + ) : showSunsetUI ? ( <> Gitpod Classic has sunset Continue with Ona + ) : !isGitpodIoUser ? ( + Log in to Gitpod ) : ( <> Log in to Gitpod Classic @@ -309,16 +316,33 @@ const LoginContent = ({

- {classicSunsetEnabled && isGitpodIoUser ? ( - { - window.location.href = "https://app.ona.com/login"; - }} - > - - Login with Ona - - + {showSunsetUI ? ( + <> + { + window.location.href = "https://app.ona.com/login"; + }} + > + + Continue with Ona + + +
+

+ Need to access your organization?{" "} + + Show all login options + +

+
+ ) : providerFromContext ? ( )) )} - + {!showSunsetUI && }
{errorMessage && } - {/* Gitpod Classic sunset notice - only show for non-enterprise and when NOT sunset */} - {!enterprise && !classicSunsetEnabled && ( + {/* Gitpod Classic sunset notice - only show for non-enterprise */} + {!enterprise && (

Gitpod Classic sunsets Oct 15.{" "} From 98981397ff110d184e0b61e67e9944bdbe91566d Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Wed, 15 Oct 2025 06:12:09 +0000 Subject: [PATCH 03/10] Refactor backend sunset checks into separate functions Split sunset blocking logic into two functions: - isUserLoginBlockedBySunset: checks roles/permissions exemption for login - isWorkspaceStartBlockedBySunset: checks org-level exemption for workspace ops Move ClassicPaygSunsetConfig interface to gitpod-protocol for reusability. Pass organizationId explicitly to workspace blocking checks. Co-authored-by: Ona --- .../src/experiments/configcat.ts | 5 ++++ .../server/src/api/workspace-service-api.ts | 11 +++++---- components/server/src/user/user-controller.ts | 4 ++-- components/server/src/util/featureflags.ts | 24 ++++++++++++++----- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/components/gitpod-protocol/src/experiments/configcat.ts b/components/gitpod-protocol/src/experiments/configcat.ts index ff707930167021..e1a456551c0e0e 100644 --- a/components/gitpod-protocol/src/experiments/configcat.ts +++ b/components/gitpod-protocol/src/experiments/configcat.ts @@ -60,3 +60,8 @@ export function attributesToUser(attributes: Attributes): ConfigCatUser { return new ConfigCatUser(userId, email, "", custom); } + +export interface ClassicPaygSunsetConfig { + enabled: boolean; + exemptedOrganizations: string[]; +} diff --git a/components/server/src/api/workspace-service-api.ts b/components/server/src/api/workspace-service-api.ts index 56e5d19b01e1e8..f45183e2c23910 100644 --- a/components/server/src/api/workspace-service-api.ts +++ b/components/server/src/api/workspace-service-api.ts @@ -61,7 +61,8 @@ import { UserService } from "../user/user-service"; import { ContextParser } from "../workspace/context-parser-service"; import { isWorkspaceId } from "@gitpod/gitpod-protocol/lib/util/parse-workspace-id"; import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; -import { isUserBlockedBySunset } from "../util/featureflags"; +import { isWorkspaceStartBlockedBySunset } from "../util/featureflags"; +import { User } from "@gitpod/gitpod-protocol"; @injectable() export class WorkspaceServiceAPI implements ServiceImpl { @@ -71,8 +72,8 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (await isUserBlockedBySunset(user)) { + private async checkClassicPaygSunset(user: User, organizationId: string): Promise { + if (await isWorkspaceStartBlockedBySunset(user, organizationId)) { throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, "Gitpod Classic PAYG has sunset. Please visit https://app.ona.com/login to continue.", @@ -210,7 +211,7 @@ export class WorkspaceServiceAPI implements ServiceImpl GitpodServerImpl; @@ -73,7 +73,7 @@ export class UserController { // Check if authenticated user is blocked by sunset const user = req.user as User; - if (await isUserBlockedBySunset(user)) { + if (await isUserLoginBlockedBySunset(user)) { log.info("(Auth) User blocked by Classic PAYG sunset", { userId: user.id, organizationId: user.organizationId, diff --git a/components/server/src/util/featureflags.ts b/components/server/src/util/featureflags.ts index 0f746b7d4b634d..fd65d6259ab176 100644 --- a/components/server/src/util/featureflags.ts +++ b/components/server/src/util/featureflags.ts @@ -5,6 +5,7 @@ */ import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; +import { ClassicPaygSunsetConfig } from "@gitpod/gitpod-protocol/lib/experiments/configcat"; import { User } from "@gitpod/gitpod-protocol"; export async function getFeatureFlagEnableExperimentalJBTB(userId: string): Promise { @@ -13,11 +14,6 @@ export async function getFeatureFlagEnableExperimentalJBTB(userId: string): Prom }); } -export interface ClassicPaygSunsetConfig { - enabled: boolean; - exemptedOrganizations: string[]; -} - export async function getClassicPaygSunsetConfig(userId: string): Promise { return getExperimentsClientForBackend().getValueAsync( "classic_payg_sunset_enabled", @@ -26,7 +22,23 @@ export async function getClassicPaygSunsetConfig(userId: string): Promise { +export async function isWorkspaceStartBlockedBySunset(user: User, organizationId: string): Promise { + const config = await getClassicPaygSunsetConfig(user.id); + + if (!config.enabled) { + return false; + } + + // If user has an org, check if it's exempted + if (organizationId) { + return !config.exemptedOrganizations.includes(organizationId); + } + + // Installation-owned users (no organizationId) are blocked + return true; +} + +export async function isUserLoginBlockedBySunset(user: User): Promise { const config = await getClassicPaygSunsetConfig(user.id); if (!config.enabled) { From 3a1c0e6bbc56afe5747e99910bf8d134c72c3122 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Wed, 15 Oct 2025 07:21:27 +0000 Subject: [PATCH 04/10] Use typed ClassicPaygSunsetConfig in frontend feature flag Import ClassicPaygSunsetConfig type from gitpod-protocol and use it as the default value for classic_payg_sunset_enabled feature flag. This leverages TypeScript's generic type inference in useFeatureFlag: - useFeatureFlag returns FeatureFlags[K] - For classic_payg_sunset_enabled, it now returns ClassicPaygSunsetConfig - Other flags continue to return their respective types (boolean, string, etc.) Updated Login.tsx to access .enabled property with type guard to handle the union type (ClassicPaygSunsetConfig | boolean) during loading state. This ensures type safety and consistency between frontend and backend. Co-authored-by: Ona --- components/dashboard/src/Login.tsx | 5 +++-- components/dashboard/src/data/featureflag-query.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index acc386af0f78db..940169a0c90693 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -217,7 +217,7 @@ const LoginContent = ({ const { setUser } = useContext(UserContext); const isDataOps = useIsDataOps(); const isGitpodIoUser = isGitpodIo(); - const classicSunsetEnabled = useFeatureFlag("classic_payg_sunset_enabled"); + const classicSunsetConfig = useFeatureFlag("classic_payg_sunset_enabled"); const authProviders = useAuthProviderDescriptions(); const [errorMessage, setErrorMessage] = useState(undefined); @@ -230,7 +230,8 @@ const LoginContent = ({ const oldLogin = searchParams.get("oldLogin") === "true"; // Show sunset UI only if: sunset enabled, on gitpod.io, and user hasn't requested old login - const showSunsetUI = classicSunsetEnabled && isGitpodIoUser && !oldLogin; + const showSunsetUI = + (typeof classicSunsetConfig === "object" ? classicSunsetConfig.enabled : false) && isGitpodIoUser && !oldLogin; const updateUser = useCallback(async () => { await getGitpodService().reconnect(); diff --git a/components/dashboard/src/data/featureflag-query.ts b/components/dashboard/src/data/featureflag-query.ts index 5aa26a8a353885..1fb95bd6294df5 100644 --- a/components/dashboard/src/data/featureflag-query.ts +++ b/components/dashboard/src/data/featureflag-query.ts @@ -9,6 +9,7 @@ import { useQuery } from "@tanstack/react-query"; import { getExperimentsClient } from "../experiments/client"; import { useCurrentUser } from "../user-context"; import { useCurrentOrg } from "./organizations/orgs-query"; +import { ClassicPaygSunsetConfig } from "@gitpod/gitpod-protocol/lib/experiments/configcat"; const featureFlags = { oidcServiceEnabled: false, @@ -26,7 +27,7 @@ const featureFlags = { enabled_configuration_prebuild_full_clone: false, enterprise_onboarding_enabled: false, commit_annotation_setting_enabled: false, - classic_payg_sunset_enabled: false, + classic_payg_sunset_enabled: { enabled: false, exemptedOrganizations: [] } as ClassicPaygSunsetConfig, }; type FeatureFlags = typeof featureFlags; From 8c014ff6d1756fbe91844580bab6e56b2f71e8d5 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Wed, 15 Oct 2025 07:39:23 +0000 Subject: [PATCH 05/10] Parse JSON string for classic_payg_sunset_enabled feature flag ConfigCat text flags return strings, so we need to parse JSON on both frontend and backend. Backend (featureflags.ts): - Send JSON.stringify(defaultConfig) to ConfigCat - Parse returned string with JSON.parse() - Handle errors gracefully with fallback to default Frontend (featureflag-query.ts): - Add parseFeatureFlagValue() helper for JSON flags - Send stringified default for classic_payg_sunset_enabled - Parse returned string value - Maintain type safety with generic return types This allows ConfigCat to store the flag as text while maintaining the typed object structure in our code. Co-authored-by: Ona --- .../dashboard/src/data/featureflag-query.ts | 34 +++++++++++++++++-- components/server/src/util/featureflags.ts | 17 ++++++++-- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/components/dashboard/src/data/featureflag-query.ts b/components/dashboard/src/data/featureflag-query.ts index 1fb95bd6294df5..570227c61a0ebd 100644 --- a/components/dashboard/src/data/featureflag-query.ts +++ b/components/dashboard/src/data/featureflag-query.ts @@ -11,6 +11,8 @@ import { useCurrentUser } from "../user-context"; import { useCurrentOrg } from "./organizations/orgs-query"; import { ClassicPaygSunsetConfig } from "@gitpod/gitpod-protocol/lib/experiments/configcat"; +const defaultClassicPaygSunsetConfig: ClassicPaygSunsetConfig = { enabled: false, exemptedOrganizations: [] }; + const featureFlags = { oidcServiceEnabled: false, // Default to true to enable on gitpod dedicated until ff support is added for dedicated @@ -27,11 +29,31 @@ const featureFlags = { enabled_configuration_prebuild_full_clone: false, enterprise_onboarding_enabled: false, commit_annotation_setting_enabled: false, - classic_payg_sunset_enabled: { enabled: false, exemptedOrganizations: [] } as ClassicPaygSunsetConfig, + classic_payg_sunset_enabled: defaultClassicPaygSunsetConfig, }; type FeatureFlags = typeof featureFlags; +// Helper to parse JSON feature flags +function parseFeatureFlagValue(flagName: string, rawValue: any, defaultValue: T): T { + // Special handling for JSON-based feature flags + if (flagName === "classic_payg_sunset_enabled") { + try { + if (typeof rawValue === "string") { + return JSON.parse(rawValue) as T; + } + // If it's already an object, return as-is + if (typeof rawValue === "object" && rawValue !== null) { + return rawValue as T; + } + } catch (error) { + console.error(`Failed to parse feature flag ${flagName}:`, error); + return defaultValue; + } + } + return rawValue; +} + export const useFeatureFlag = (featureFlag: K): FeatureFlags[K] | boolean => { const user = useCurrentUser(); const org = useCurrentOrg().data; @@ -39,7 +61,12 @@ export const useFeatureFlag = (featureFlag: K): Fe const queryKey = ["featureFlag", featureFlag, user?.id || "", org?.id || ""]; const query = useQuery(queryKey, async () => { - const flagValue = await getExperimentsClient().getValueAsync(featureFlag, featureFlags[featureFlag], { + const defaultValue = featureFlags[featureFlag]; + // For JSON flags, send stringified default to ConfigCat + const configCatDefault = + featureFlag === "classic_payg_sunset_enabled" ? JSON.stringify(defaultValue) : defaultValue; + + const rawValue = await getExperimentsClient().getValueAsync(featureFlag, configCatDefault, { user: user && { id: user.id, email: getPrimaryEmail(user), @@ -48,7 +75,8 @@ export const useFeatureFlag = (featureFlag: K): Fe teamName: org?.name, gitpodHost: window.location.host, }); - return flagValue; + + return parseFeatureFlagValue(featureFlag, rawValue, defaultValue); }); return query.data !== undefined ? query.data : featureFlags[featureFlag]; diff --git a/components/server/src/util/featureflags.ts b/components/server/src/util/featureflags.ts index fd65d6259ab176..43dac3a95ccb32 100644 --- a/components/server/src/util/featureflags.ts +++ b/components/server/src/util/featureflags.ts @@ -15,11 +15,24 @@ export async function getFeatureFlagEnableExperimentalJBTB(userId: string): Prom } export async function getClassicPaygSunsetConfig(userId: string): Promise { - return getExperimentsClientForBackend().getValueAsync( + const defaultConfig: ClassicPaygSunsetConfig = { enabled: false, exemptedOrganizations: [] }; + const rawValue = await getExperimentsClientForBackend().getValueAsync( "classic_payg_sunset_enabled", - { enabled: false, exemptedOrganizations: [] }, + JSON.stringify(defaultConfig), { user: { id: userId } }, ); + + // Parse JSON string from ConfigCat + try { + if (typeof rawValue === "string") { + return JSON.parse(rawValue) as ClassicPaygSunsetConfig; + } + // Fallback if somehow we get an object (shouldn't happen with ConfigCat text flags) + return rawValue as ClassicPaygSunsetConfig; + } catch (error) { + console.error("Failed to parse classic_payg_sunset_enabled feature flag:", error); + return defaultConfig; + } } export async function isWorkspaceStartBlockedBySunset(user: User, organizationId: string): Promise { From 1c079f8c501d4fa5a60abf5e9328bd3aa1b75811 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Wed, 15 Oct 2025 08:24:48 +0000 Subject: [PATCH 06/10] Exempt dedicated installations from sunset blocking Add isDedicatedInstallation parameter to sunset check functions. Dedicated installations always return false (not blocked) regardless of feature flag state. Changes: - isUserLoginBlockedBySunset: add isDedicatedInstallation param - isWorkspaceStartBlockedBySunset: add isDedicatedInstallation param - UserController: pass config.isDedicatedInstallation to login check - WorkspaceServiceAPI: inject Config and pass isDedicatedInstallation This ensures the sunset only affects gitpod.io (PAYG) and not dedicated installations. Co-authored-by: Ona --- .../server/src/api/workspace-service-api.ts | 4 +++- components/server/src/user/user-controller.ts | 2 +- components/server/src/util/featureflags.ts | 18 ++++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/components/server/src/api/workspace-service-api.ts b/components/server/src/api/workspace-service-api.ts index f45183e2c23910..febe968c558134 100644 --- a/components/server/src/api/workspace-service-api.ts +++ b/components/server/src/api/workspace-service-api.ts @@ -63,6 +63,7 @@ import { isWorkspaceId } from "@gitpod/gitpod-protocol/lib/util/parse-workspace- import { SYSTEM_USER, SYSTEM_USER_ID } from "../authorization/authorizer"; import { isWorkspaceStartBlockedBySunset } from "../util/featureflags"; import { User } from "@gitpod/gitpod-protocol"; +import { Config } from "../config"; @injectable() export class WorkspaceServiceAPI implements ServiceImpl { @@ -71,9 +72,10 @@ export class WorkspaceServiceAPI implements ServiceImpl { - if (await isWorkspaceStartBlockedBySunset(user, organizationId)) { + if (await isWorkspaceStartBlockedBySunset(user, organizationId, this.config.isDedicatedInstallation)) { throw new ApplicationError( ErrorCodes.PERMISSION_DENIED, "Gitpod Classic PAYG has sunset. Please visit https://app.ona.com/login to continue.", diff --git a/components/server/src/user/user-controller.ts b/components/server/src/user/user-controller.ts index 1f0d5064d4274f..789fe9042010d3 100644 --- a/components/server/src/user/user-controller.ts +++ b/components/server/src/user/user-controller.ts @@ -73,7 +73,7 @@ export class UserController { // Check if authenticated user is blocked by sunset const user = req.user as User; - if (await isUserLoginBlockedBySunset(user)) { + if (await isUserLoginBlockedBySunset(user, this.config.isDedicatedInstallation)) { log.info("(Auth) User blocked by Classic PAYG sunset", { userId: user.id, organizationId: user.organizationId, diff --git a/components/server/src/util/featureflags.ts b/components/server/src/util/featureflags.ts index 43dac3a95ccb32..67df69bd1e6682 100644 --- a/components/server/src/util/featureflags.ts +++ b/components/server/src/util/featureflags.ts @@ -35,7 +35,16 @@ export async function getClassicPaygSunsetConfig(userId: string): Promise { +export async function isWorkspaceStartBlockedBySunset( + user: User, + organizationId: string, + isDedicatedInstallation: boolean, +): Promise { + // Dedicated installations are never blocked + if (isDedicatedInstallation) { + return false; + } + const config = await getClassicPaygSunsetConfig(user.id); if (!config.enabled) { @@ -51,7 +60,12 @@ export async function isWorkspaceStartBlockedBySunset(user: User, organizationId return true; } -export async function isUserLoginBlockedBySunset(user: User): Promise { +export async function isUserLoginBlockedBySunset(user: User, isDedicatedInstallation: boolean): Promise { + // Dedicated installations are never blocked + if (isDedicatedInstallation) { + return false; + } + const config = await getClassicPaygSunsetConfig(user.id); if (!config.enabled) { From 8b90c707830191031fd537a9e5c18fef193ca822 Mon Sep 17 00:00:00 2001 From: Siddhant Khare Date: Tue, 14 Oct 2025 12:43:22 +0000 Subject: [PATCH 07/10] update login page for Gitpod classic `gitpod.io` users --- components/dashboard/src/Login.tsx | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index 940169a0c90693..208dbcf20d56bf 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -310,8 +310,8 @@ const LoginContent = ({ Log in to Gitpod ) : ( <> - Log in to Gitpod Classic - Hosted by us + Start building with Ona + What do you want to get done today? )}

@@ -372,9 +372,19 @@ const LoginContent = ({ {!enterprise && (

- Gitpod Classic sunsets Oct 15.{" "} + Gitpod Classic has been sunset on Oct 15.{" "} + {" "} + Gitpod is now Ona + + ,{" "} + { > Start for free {" "} - and get $100 credits.
+ and get $100 in credits.

- Gitpod Classic sunsets Oct 15 |{" "} + Gitpod Classic has been sunset on Oct 15 |{" "} { Delegate software tasks to Ona. It writes code, runs tests, and opens a pull request. Or jump in to inspect output or pair program in your IDE.

-

- Ona runs inside your infrastructure (VPC), with full audit trails, zero data exposure, and - support for any LLM. -

From d672fd4df9980115e19c827b4d1d3961e9994cf0 Mon Sep 17 00:00:00 2001 From: Siddhant Khare Date: Tue, 14 Oct 2025 12:44:53 +0000 Subject: [PATCH 08/10] fix --- components/dashboard/src/Login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index 208dbcf20d56bf..36d66c02e65efe 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -389,7 +389,7 @@ const LoginContent = ({ rel="noopener noreferrer" className="gp-link hover:text-gray-600" > - Start here for free + start for free {" "} and get $100 credits.

From 8afabf7ae4c44b67955f4335f7ef2f81e0c906d5 Mon Sep 17 00:00:00 2001 From: "Cornelius A. Ludmann" Date: Wed, 15 Oct 2025 11:42:35 +0000 Subject: [PATCH 09/10] Improve sunset UI: use primary button and remove redundant subheading Co-authored-by: Ona --- components/dashboard/src/Login.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index 36d66c02e65efe..4c1dbc1ec95b57 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -304,7 +304,6 @@ const LoginContent = ({ ) : showSunsetUI ? ( <> Gitpod Classic has sunset - Continue with Ona ) : !isGitpodIoUser ? ( Log in to Gitpod @@ -319,15 +318,14 @@ const LoginContent = ({
{showSunsetUI ? ( <> - { window.location.href = "https://app.ona.com/login"; }} > - - Continue with Ona - - + Continue with Ona +

Need to access your organization?{" "} From 2216437af0cb2130bce4bf7df81c576b9c8de7bb Mon Sep 17 00:00:00 2001 From: "Cornelius A. Ludmann" Date: Wed, 15 Oct 2025 12:18:04 +0000 Subject: [PATCH 10/10] Update sunset UI heading to focus on Ona value proposition Co-authored-by: Ona --- components/dashboard/src/Login.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index 4c1dbc1ec95b57..ca001cdeba877d 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -303,7 +303,8 @@ const LoginContent = ({ ) : showSunsetUI ? ( <> - Gitpod Classic has sunset + Start building with Ona + What do you want to get done today? ) : !isGitpodIoUser ? ( Log in to Gitpod