Recognize the self-managed Secrets Manager beta cohort
What does this MR do and why?
Self-managed (SM) instances that joined the Secrets Manager open beta lose UI access to their secrets, and cannot create or edit secrets, as soon as secrets_manager_paid_experience is enabled instance-wide. Equivalent GitLab.com beta groups keep full access and see a transition banner.
Cause: SM enrollment (SecretsManagement::InstanceEnrollment) is a plain boolean application setting with no beta marker, unlike SecretsManagement::NamespaceEnrollment on GitLab.com, which has a beta column (see !249180 (merged)). Because of this, SecretsManagement::Entitlement::Resolver#beta_enrolled? was always false on SM, beta_window_eligible was always false, and the group/project policies (which queried the namespace enrollment directly) hid subgroup and project secrets.
This MR:
- Adds a
secrets_manager_instance_beta_enrolledboolean column onapplication_settings(migration20260908193413, defaultfalse, not null), plus a regular data migration20260908193414(BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled) that backfills it withUPDATE application_settings SET secrets_manager_instance_beta_enrolled = secrets_manager_instance_enrolled. This is correct becausesecrets_manager_paid_experiencehas never defaulted to on for self-managed, so every instance enrolled today enrolled during the free beta. The backfill ships in 19.4, the same release in whichsecrets_manager_paid_experienceis planned to default on for self-managed (https://gitlab.com/gitlab-org/gitlab/-/work_items/616379, Phase 3a); the beta program end flag follows in 19.5 (Phase 3b). It is a regular migration as an explicit exception: the marker must exist before flag-on code serves requests, including on zero-downtime upgrades that defer post-deployment migrations. SecretsManagement::InstanceEnrollmentService#enrollwrites the marker as!Feature.enabled?(:secrets_manager_paid_experience, :instance)at enrollment time, re-evaluated on every enroll, mirroringNamespaceEnrollmentService#beta_enrollment?on GitLab.com.InstanceEnrollment.beta_enrolled?now readsbeta? && Gitlab::CurrentSettings.secrets_manager_instance_beta_enrolled, with no queries against secrets manager tables.- Makes
Entitlement::Resolver#beta_enrolled?branch onGitlab::Saas.feature_available?(:gitlab_com_subscriptions): SaaS keeps readingNamespaceEnrollment.beta_enrolled?, self-managed reads the newInstanceEnrollment.beta_enrolled?. The beta window still only opens intrial_eligible/ineligibleand still closes whenend_secrets_manager_beta_programis enabled, same as on SaaS. - Updates
GroupPolicy/ProjectPolicyto readbeta_window_eligiblefrom the resolved entitlement instead of a private helper that queriedNamespaceEnrollmentdirectly; the helpers are removed. On SaaS this is equivalent because the condition only fires intrial_eligible, wherebeta_window_eligibleequals the old enrollment lookup. - Adds a nullable
betaWindowEligibleGraphQL field onEntitlementType(experiment, milestone 19.4) so the frontend can read one realm-aware signal.
Notes for reviewers:
- The marker is instance-wide, not per group, because self-managed entitlement is instance-wide: CustomersDot is asked per instance and the resolver caches one entitlement per install, so a per-group answer would leak between groups through that cache. Instance-wide also covers projects in personal namespaces.
- Why a stored marker instead of deriving it from existing secrets manager rows (the first iteration of this MR): a derived answer moves when CustomersDot fails or returns
ineligible, when the instance unenrolls and re-enrolls, or when the last manager is deleted. A value written at enrollment time does not. See the review discussion at !254012 (comment 3803023229). - The migrations were going to be a separate MR; folded into this one by agreement with the reviewers. The backfill is a regular migration, agreed in !254012 (comment 3806869696) as an exception to the rule that data migrations are post-deployment: with the flag flip in the same release, a zero-downtime upgrade with
SKIP_POST_DEPLOYMENT_MIGRATIONSwould otherwise serve flag-on code before the marker exists and treat beta instances as new customers until the deferred migrations run. It is a single-row update (0.7 ms, plan below) that depends only on the precedingadd_column, and the old code never reads the column, so it carries none of the compatibility risk post-deployment migrations guard against. - The setting is deliberately absent from the Admin Area form, the REST settings API, and GraphQL; only
InstanceEnrollmentServiceand the backfill write it. - Out of scope: offline (air-gapped) and trial licenses take resolver paths that skip the beta attributes and are unchanged. The self-managed beta cutoff date and its communication are a product decision tracked separately.
Database
application_settings has exactly one row, so every statement below touches a single row. Plans are EXPLAIN (ANALYZE, BUFFERS) from a local GDK database; the table has the same single-row shape on every installation, so a Database Lab plan would not differ in kind.
Migration output
$ bin/rails db:migrate:down:main VERSION=20260908193414
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: reverting
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: reverted (0.0108s)
$ bin/rails db:migrate:down:main VERSION=20260908193413
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: reverting
main: -- remove_column(:application_settings, :secrets_manager_instance_beta_enrolled, {:if_exists=>true})
main: -> 0.2354s
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: reverted (0.2441s)
$ bin/rails db:migrate
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: migrating
main: -- add_column(:application_settings, :secrets_manager_instance_beta_enrolled, :boolean, {:default=>false, :null=>false, :if_not_exists=>true})
main: -> 0.2652s
main: == 20260908193413 AddSecretsManagerInstanceBetaEnrolledToApplicationSettings: migrated (0.2782s)
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: migrating
main: == 20260908193414 BackfillApplicationSettingsSecretsManagerInstanceBetaEnrolled: migrated (0.0136s)Backfill (regular migration 20260908193414, update_all)
UPDATE application_settings SET secrets_manager_instance_beta_enrolled = secrets_manager_instance_enrolledUpdate on application_settings (cost=0.00..4.01 rows=0 width=0) (actual time=0.355..0.355 rows=0 loops=1)
Buffers: shared hit=170 dirtied=1
-> Seq Scan on application_settings (cost=0.00..4.01 rows=1 width=7) (actual time=0.013..0.013 rows=1 loops=1)
Buffers: shared hit=4
Planning:
Buffers: shared hit=153
Planning Time: 0.316 ms
Execution Time: 0.671 msEnrollment service (InstanceEnrollmentService#enroll, changed query)
Gitlab::CurrentSettings.update! now writes two columns instead of one. Exact statement emitted by ActiveRecord:
UPDATE "application_settings" SET "updated_at" = '2026-09-08 20:39:36.961037', "secrets_manager_instance_enrolled" = FALSE, "secrets_manager_instance_beta_enrolled" = FALSE WHERE "application_settings"."id" = 1Update on application_settings (cost=0.12..2.15 rows=0 width=0) (actual time=0.216..0.216 rows=0 loops=1)
Buffers: shared hit=76
-> Index Scan using application_settings_pkey on application_settings (cost=0.12..2.15 rows=1 width=16) (actual time=0.009..0.009 rows=1 loops=1)
Index Cond: (id = 1)
Buffers: shared hit=5
Planning:
Buffers: shared hit=7
Planning Time: 0.040 ms
Execution Time: 0.223 msRemoved queries
The first iteration of this MR added SELECT 1 AS one FROM "group_secrets_managers" LIMIT 1 and the same for project_secrets_managers in InstanceEnrollment.beta_enrolled?. Both are gone; the method now reads the cached application settings row and issues no query.
Rollback
20260908193413 drops the column; the backfill's down is a no-op because the preceding migration's down already removes the column. Output above shows both down steps and the re-run.
References
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/627946 (closes when the frontend MR below lands)
- Beta marker on SaaS enrollment: !249180 (merged)
- Beta program end flag: !249902 (merged)
- Rollout issue for the two flags: https://gitlab.com/gitlab-org/gitlab/-/work_items/616379
- Frontend banner follow-up work: https://gitlab.com/gitlab-org/gitlab/-/work_items/623322
- Frontend MR (stacked on this branch): !254013 (merged)
Screenshots or screen recordings
This is a backend-only change with no UI impact.
How to set up and validate locally
-
Run GDK as self-managed (no SaaS simulation) with an online cloud license that includes
native_secrets_management, with CustomersDot reachable (staging) or the CustomersDot client stubbed to returntrial_eligibleandblocked: false. -
In Admin area > Settings > General, enable Secrets Manager instance enrollment. Create a top-level group and provision a group secrets manager (Settings > General > Permissions and group features > Secrets manager toggle).
-
Run
bin/rails db:migrate. For an instance enrolled before this change,Gitlab::CurrentSettings.secrets_manager_instance_beta_enrolledreturnstrue(backfilled). -
In a Rails console, enable the flag and check eligibility:
Feature.enable(:secrets_manager_paid_experience) SecretsManagement::Entitlement.for(group).beta_window_eligible # => true SecretsManagement::Entitlement.for(group).permits_writes? # => true -
End the beta program and re-check:
Feature.enable(:end_secrets_manager_beta_program) SecretsManagement::Entitlement.for(group).permits_writes? # => false SecretsManagement::Entitlement.for(group).permits_direct_read? # => false SecretsManagement::Entitlement.for(group).permits_read? # => true -
Query GraphQL:
group(fullPath:) { secretsManagerEntitlement { state betaWindowEligible betaProgramEnded } }and checkbetaWindowEligible: true. -
Counter check: in the Rails console, run
Gitlab::CurrentSettings.update!(secrets_manager_instance_beta_enrolled: false)and confirmSecretsManagement::Entitlement.for(group).beta_window_eligiblereturnsfalse; restore it withGitlab::CurrentSettings.update!(secrets_manager_instance_beta_enrolled: true). -
Counter check: unenroll and re-enroll the instance in Admin area > Settings > General while
secrets_manager_paid_experienceis enabled, and confirm both the marker andbeta_window_eligiblearefalse.
MR 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.