Add owned pipeline schedules section to user account page
What does this MR do and why?
Add owned pipeline schedules section to user account page
Users need a way to view the scheduled pipelines they own, across all projects. A primary use case is for users leaving an organization who need to identify scheduled pipelines they own before their account gets blocked, to prevent operational disruptions.
This adds a simple list of active pipeline schedules to the Profile > Account page.
This list could also be viewed by an Administrator who uses GitLab's Impersonation feature.
The implementation:
- Adds
has_many :pipeline_schedulesassociation to User model - Displays active pipeline schedules in account settings
- Links directly to project pipeline schedules pages
- Uses existing database indexes for efficient querying
Addresses the user visibility gap described in Allow a user to see all their owned scheduled p... (#558979 - closed) • Nick Malcolm • 18.4 • On track
Changelog: added
Design Decisions / FAQ
- Q: Why add it to this page?
- No other page fit better
- This page already shows a list of solely owned groups. Owned pipelines feels like a close fit for a very similar use case. However, we don't want to have Owned Scheduled Pipelines be blockers to account deletion, so I put them just above
- Q: Why not group by Project? Vs. individual links
- So that you can, at a glance, see the different descriptions of the pipelines. Some might be not worth transferring or updating
- It's likely more database performant most of the time, since we can rely on the existing
owner + activeindex and not GROUP_BY
- Q: Why link to the Project Scheduled Pipelines page, then?
- I don't want to assume you want to edit the pipeline. You might want to view the latest job, or delete it. Linking to the Project page allows the user to make those choices.
- Q: Why no pagination?
- I don't expect people to own a lot of pipelines.
Database Calls
This adds six queries to the page, but none of them are N+1:
- Pipeline schedules: 2 queries (existence + load)
Index Only Scan using index_ci_pipeline_schedules_on_owner_id_and_id_and_active on public.ci_pipeline_schedules (cost=0.14..3.16 rows=1 width=4) (actual time=0.007..0.007 rows=0 loops=1)` - Projects: 1 batched query
Index Scan using projects_pkey on public.projects (cost=0.56..6.90 rows=2 width=821) (actual time=2.356..2.356 rows=0 loops=1) Index Cond: (projects.id = ANY ('{53,42}'::integer[])) - Namespaces: 1 batched query
- Project routes: 1 batched query
- Namespace routes: 1 batched query
Why take this approach?
- Most users probably have 0 scheduled pipelines. For them, 1 query vs 5 queries is a huge difference
- For users with schedules, 6 vs 5 queries is negligible. We use includes to reduce N+1 queries.
Click to expand SQL logs
Ci::PipelineSchedule Exists? (2.1ms) SELECT 1 AS one FROM "ci_pipeline_schedules" WHERE "ci_pipeline_schedules"."owner_id" = 22 AND "ci_pipeline_schedules"."active" = TRUE LIMIT 1
↳ app/views/profiles/accounts/show.html.haml:79
Ci::PipelineSchedule Load (2.6ms) SELECT "ci_pipeline_schedules".* FROM "ci_pipeline_schedules" WHERE "ci_pipeline_schedules"."owner_id" = 22 AND "ci_pipeline_schedules"."active" = TRUE
↳ app/views/profiles/accounts/show.html.haml:82
Project Load (7.5ms) SELECT "projects"."id", "projects"."name", "projects"."path", "projects"."description", "projects"."created_at", "projects"."updated_at", "projects"."creator_id", "projects"."namespace_id", "projects"."last_activity_at", "projects"."import_url", "projects"."visibility_level", "projects"."archived", "projects"."avatar", "projects"."merge_requests_template", "projects"."star_count", "projects"."merge_requests_rebase_enabled", "projects"."import_type", "projects"."import_source", "projects"."approvals_before_merge", "projects"."reset_approvals_on_push", "projects"."merge_requests_ff_only_enabled", "projects"."issues_template", "projects"."mirror", "projects"."mirror_last_update_at", "projects"."mirror_last_successful_update_at", "projects"."mirror_user_id", "projects"."shared_runners_enabled", "projects"."runners_token", "projects"."build_allow_git_fetch", "projects"."build_timeout", "projects"."mirror_trigger_builds", "projects"."pending_delete", "projects"."public_builds", "projects"."last_repository_check_failed", "projects"."last_repository_check_at", "projects"."only_allow_merge_if_pipeline_succeeds", "projects"."has_external_issue_tracker", "projects"."repository_storage", "projects"."repository_read_only", "projects"."request_access_enabled", "projects"."has_external_wiki", "projects"."ci_config_path", "projects"."lfs_enabled", "projects"."description_html", "projects"."only_allow_merge_if_all_discussions_are_resolved", "projects"."repository_size_limit", "projects"."printing_merge_request_link_enabled", "projects"."auto_cancel_pending_pipelines", "projects"."service_desk_enabled", "projects"."cached_markdown_version", "projects"."delete_error", "projects"."last_repository_updated_at", "projects"."disable_overriding_approvers_per_merge_request", "projects"."storage_version", "projects"."resolve_outdated_diff_discussions", "projects"."remote_mirror_available_overridden", "projects"."only_mirror_protected_branches", "projects"."pull_mirror_available_overridden", "projects"."jobs_cache_index", "projects"."external_authorization_classification_label", "projects"."mirror_overwrites_diverged_branches", "projects"."pages_https_only", "projects"."external_webhook_token", "projects"."packages_enabled", "projects"."merge_requests_author_approval", "projects"."pool_repository_id", "projects"."runners_token_encrypted", "projects"."bfg_object_map", "projects"."detected_repository_languages", "projects"."merge_requests_disable_committers_approval", "projects"."require_password_to_approve", "projects"."max_pages_size", "projects"."max_artifacts_size", "projects"."pull_mirror_branch_prefix", "projects"."remove_source_branch_after_merge", "projects"."marked_for_deletion_at", "projects"."marked_for_deletion_by_user_id", "projects"."autoclose_referenced_issues", "projects"."suggestion_commit_message", "projects"."project_namespace_id", "projects"."hidden", "projects"."organization_id" FROM "projects" WHERE "projects"."id" IN (53, 42)
↳ app/models/concerns/use_sql_function_for_primary_key_lookups.rb:10:in `_query_by_sql'
Namespace Load (2.0ms) SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id", "namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level", "namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error", "namespaces"."ldap_sync_last_update_at", "namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at", "namespaces"."lfs_enabled", "namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit", "namespaces"."require_two_factor_authentication", "namespaces"."two_factor_grace_period", "namespaces"."project_creation_level", "namespaces"."runners_token", "namespaces"."file_template_project_id", "namespaces"."saml_discovery_token", "namespaces"."runners_token_encrypted", "namespaces"."custom_project_templates_group_id", "namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit", "namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level", "namespaces"."subgroup_creation_level", "namespaces"."max_pages_size", "namespaces"."max_artifacts_size", "namespaces"."mentions_disabled", "namespaces"."default_branch_protection", "namespaces"."max_personal_access_token_lifetime", "namespaces"."push_rule_id", "namespaces"."shared_runners_enabled", "namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids", "namespaces"."organization_id" FROM "namespaces" WHERE "namespaces"."id" IN (101, 44)
↳ app/models/concerns/use_sql_function_for_primary_key_lookups.rb:10:in `_query_by_sql'
Route Load (1.6ms) SELECT "routes".* FROM "routes" WHERE "routes"."source_type" = 'Project' AND "routes"."source_id" IN (42, 53)
↳ app/views/profiles/accounts/show.html.haml:82
Route Load (1.6ms) SELECT "routes".* FROM "routes" WHERE "routes"."source_type" = 'Namespace' AND "routes"."source_id" IN (44, 101)
↳ app/views/profiles/accounts/show.html.haml:82
References
Screenshots or screen recordings
| What | Image |
|---|---|
| With active owned pipelines | ![]() |
| No owned active pipelines | ![]() |
| Added documentation | ![]() |
How to set up and validate locally
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.


