feat: Filter projects by Duo licensed feature
What does this MR do and why?
This change adds a new filtering option called duoLicensedFeature to GitLab's project search functionality. The filter allows users to find only projects that are eligible for specific GitLab Duo AI features based on their subscription plan or available credits.
The implementation works differently depending on the GitLab deployment type:
- For self-managed instances: It checks if the feature is available through the instance license and returns projects with Duo features enabled
- For GitLab.com (SaaS): It checks the namespace's subscription plan or available GitLab credits to determine eligibility
The filter is marked as experimental and was introduced in GitLab version 18.11.
NOTE: Changes on the scope Namespace.with_ai_supported_plan is behind the feature flag filter_projects_by_duo_licensed_feature, here is the FF roll out issue - #595308
References
How to set up and validate locally
Prerequisites:
- We need groups with Ultimate, Premium, and Free plans set up
- Some projects have
duo_features_enabled: true, othersfalse. You can toggle this at Project > Settings > General > Gitlab Duo - A
gitlab_creditsadd-on purchase exists on the Free group's namespace (You can use below script on rails console to add purchase)
Script to add gitlab_credits for a free plan project
group = Group.find_by_full_path('your-group-path')
add_on = GitlabSubscriptions::AddOn.find_or_create_by!(name: 'gitlab_credits', description: 'Gitlab Credits')
GitlabSubscriptions::AddOnPurchase.create!(
subscription_add_on_id: add_on.id,
namespace: group,
quantity: 1000,
started_at: Time.current,
expires_on: 1.year.from_now,
purchase_xid: "DEV-CREDITS-#{SecureRandom.hex(4)}"
)
Test cases
This changes are behind the filter_projects_by_duo_licensed_feature feature flag.
Open http://gdk.test:3000/-/graphql-explorer
Test Cases (SaaS)
Case 1 — No filter (baseline)
query fetch_projects {
projects {
nodes { name fullPath }
count
}
}Expected: All projects returned.
Case 2 — ai_features (Ultimate-only)
query fetch_projects {
projects(duoLicensedFeature: "ai_features") {
nodes { name fullPath }
count
}
}Expected: Only Duo-enabled projects under Ultimate namespaces.
Case 3 — agentic_chat (Premium+ or credits)
query fetch_projects {
projects(duoLicensedFeature: "agentic_chat") {
nodes { name fullPath }
count
}
}Expected: Duo-enabled projects under Premium/Ultimate namespaces, plus Duo-enabled projects in the Free namespace with active gitlab_credits in top level group.
Case 4 — Combined filters (namespace scoped, active, sorted)
query fetch_projects {
projects(
duoLicensedFeature: "agentic_chat"
namespacePath: "your-group-path"
active: true
sort: "name_asc"
) {
nodes { name fullPath }
count
}
}Expected: Only eligible Duo-enabled projects within the specified Premium+ group.
Test Cases (Self-Managed)
Case 1 - STARTER Plan
You can update below line to pass STARTER_PLAN or ULTIMATE_PLAN and verify below cases.
File - ee/app/models/license.rb
def features
@features ||= GitlabSubscriptions::Features.features(plan: <STARTER_PLAN OR ULTIMATE_PLAN>, add_ons: add_ons)
endquery fetch_projects {
projects(duoLicensedFeature: "ai_features") {
nodes { name fullPath }
count
}
}Expected: Empty result (count: 0) when the feature is not available on the instance license.
Case 2 - ULTIMATE Plan
query fetch_projects {
projects(duoLicensedFeature: "ai_features") {
nodes { name fullPath }
count
}
}Expected: All Duo-enabled projects regardless of namespace plan.
Database Query
Usage of with_ai_supported_plan in ProjectFinder
group = Group.last
ProjectsFinder.new(
current_user: User.find_by(username: 'root'),
params: { duo_licensed_feature: :agentic_chat, namespace_path: group.full_path },
).executePostgres ai plan - https://console.postgres.ai/gitlab/gitlab-production-main/sessions/50319/commands/149364
SQL Query
SELECT
"projects"."name"
FROM "projects"
INNER JOIN "project_settings" ON "project_settings"."project_id" = "projects"."id"
INNER JOIN "namespaces" ON "namespaces"."id" = "projects"."namespace_id"
LEFT OUTER JOIN "gitlab_subscriptions" ON "gitlab_subscriptions"."namespace_id" = "namespaces".traversal_ids[1]
LEFT OUTER JOIN "plans" ON "plans"."id" = "gitlab_subscriptions"."hosted_plan_id"
WHERE
(
EXISTS (
SELECT 1
FROM "project_authorizations"
WHERE "project_authorizations"."user_id" = 26121709 -- @Imjaydip
AND project_authorizations.project_id = projects.id
)
OR projects.visibility_level IN (0, 10, 20)
)
AND "projects"."namespace_id" = 9970 -- gitlab-org
AND "projects"."pending_delete" = FALSE
AND "projects"."hidden" = FALSE
AND "project_settings"."duo_features_enabled" = TRUE
AND (
plans.name IN ('silver', 'premium', 'premium_trial', 'gold', 'ultimate', 'ultimate_trial', 'ultimate_trial_paid_customer', 'opensource')
OR EXISTS (
SELECT 1
FROM "subscription_add_on_purchases"
WHERE "subscription_add_on_purchases"."subscription_add_on_id" IN (
SELECT "subscription_add_ons"."id"
FROM "subscription_add_ons"
WHERE "subscription_add_ons"."name" = 7
)
AND (started_at IS NULL OR started_at <= '2026-03-30')
AND ('2026-04-30' < expires_on)
AND namespace_id = namespaces.traversal_ids[1]
)
)
ORDER BY "projects"."id" DESCMR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related to #573615 (closed)