Gitlab: Rebase Without Pipeline
Problem
I’ve recently been in a quite interesting situation: developers have been complaining (not a new one, right?) about the fact that the development process was being slowed down by the frequent need to perform a rebase in a feature branch which in turn triggered a pipeline.
This usually happens when developers merge (quite frequently) smaller updates to a main branch. This is how trunk-based development looks like. Despite the fact that this is aimed to streamline the merging and integration phases, when the frequency of these updates becomes too high, we might end up in a situation when it actually causes more attrition.
Solution
GitLab 15.3 (which was released in August 2022) introduced the option to skip the pipeline when rebasing from the GitLab UI.
This seems a viable approach for the above problem. However, I found the docs not clear enough on how this button is displayed.
I started digging a bit more and understood that the Rebase without pipeline
button would be enabled when:
Merge commit with semi-linear history
orFast-forward merge merge
methods are enabled.Pipelines must succeed
andSkipped pipelines are considered successful
are enabled.
Ticking only one of these or neither will not work. I have tried it out.
- Fast-forward merge is not possible, but a conflict-free rebase is possible.
So far, so good. These were actually the easy bits.
What happens when there are multiple groups in GitLab, each of these having numerous (sub)projects? Updating the Merge Request method & checks is a project level setting. Considering this, there would be a few options to advance with this:
- Supposing developers have enough privileges, they could update the settings themselves, or in case this is out of question,
- Manually go over all the projects (oh, no!), or
- Automate the process using the GitLab API (a sparkle of hope).
Well, as you already imagine, I did resort to some sort of automation…
Exploring the GitLab API
In order to be able to interact with the GitLab API, we firstly need to be able to authenticate. There are several methods to do so. However, I usually prefer using a Personal Access Token. Thus, let’s start by generating one such token.
Getting a Personal Access Token
Under your user profile, you should select Access Tokens
. Pick a name for the new token (that makes most sense to you) and select the api
scope.
Using the GitLab API
Let’s start by retrieving the existing groups:
]$ curl -s -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" -H "Content-Type: application/json" -X GET "https://scm.ntf.ro/api/v4/groups" | jq
[
{
"id": 28,
"web_url": "https://scm.ntf.ro/groups/elf",
"name": "ELF",
"path": "elf",
"description": "For Testing Purposes",
"visibility": "private",
"share_with_group_lock": false,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"project_creation_level": "developer",
"auto_devops_enabled": null,
"subgroup_creation_level": "maintainer",
"emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
"avatar_url": "https://scm.ntf.ro/uploads/-/system/group/avatar/28/grinch.jpeg",
"request_access_enabled": true,
"full_name": "ELF",
"full_path": "elf",
"created_at": "2022-10-28T11:26:19.796Z",
"parent_id": null
}
]
Each group can have none, one or more subgroups:
$ curl -s -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" -H "Content-Type: application/json" -X GET "https://scm.ntf.ro/api/v4/groups/28/subgroups" | jq
[
{
"id": 30,
"web_url": "https://scm.ntf.ro/groups/elf/backend",
"name": "Backend",
"path": "backend",
"description": "",
"visibility": "private",
"share_with_group_lock": false,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"project_creation_level": "developer",
"auto_devops_enabled": null,
"subgroup_creation_level": "maintainer",
"emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
"avatar_url": null,
"request_access_enabled": true,
"full_name": "ELF / Backend",
"full_path": "elf/backend",
"created_at": "2022-10-28T11:32:50.152Z",
"parent_id": 28
},
{
"id": 29,
"web_url": "https://scm.ntf.ro/groups/elf/frontend",
"name": "Frontend",
"path": "frontend",
"description": "",
"visibility": "private",
"share_with_group_lock": false,
"require_two_factor_authentication": false,
"two_factor_grace_period": 48,
"project_creation_level": "developer",
"auto_devops_enabled": null,
"subgroup_creation_level": "maintainer",
"emails_disabled": null,
"mentions_disabled": null,
"lfs_enabled": true,
"default_branch_protection": 2,
"avatar_url": null,
"request_access_enabled": true,
"full_name": "ELF / Frontend",
"full_path": "elf/frontend",
"created_at": "2022-10-28T11:30:06.340Z",
"parent_id": 28
}
]
In this case, the group ELF
has two subgroups: Backend
& Frontend
, each having some projects.
$ curl -s -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" -H "Content-Type: application/json" -X GET "https://scm.ntf.ro/api/v4/groups/30/projects" | jq
[
{
"id": 24,
"description": null,
"name": "Project-C",
"name_with_namespace": "ELF / Backend / Project-C",
"path": "project-c",
"path_with_namespace": "elf/backend/project-c",
"created_at": "2022-10-28T12:16:00.069Z",
"default_branch": "master",
"tag_list": [],
"topics": [],
"ssh_url_to_repo": "git@scm.ntf.ro:elf/backend/project-c.git",
"http_url_to_repo": "https://scm.ntf.ro/elf/backend/project-c.git",
"web_url": "https://scm.ntf.ro/elf/backend/project-c",
"readme_url": "https://scm.ntf.ro/elf/backend/project-c/-/blob/master/README.md",
"avatar_url": null,
"forks_count": 0,
"star_count": 0,
"last_activity_at": "2022-10-28T12:16:00.069Z",
"namespace": {
"id": 30,
"name": "Backend",
"path": "backend",
"kind": "group",
"full_path": "elf/backend",
"parent_id": 28,
"avatar_url": null,
"web_url": "https://scm.ntf.ro/groups/elf/backend"
},
"container_registry_image_prefix": "scm.ntf.ro:5050/elf/backend/project-c",
"_links": {
"self": "https://scm.ntf.ro/api/v4/projects/24",
"issues": "https://scm.ntf.ro/api/v4/projects/24/issues",
"merge_requests": "https://scm.ntf.ro/api/v4/projects/24/merge_requests",
"repo_branches": "https://scm.ntf.ro/api/v4/projects/24/repository/branches",
"labels": "https://scm.ntf.ro/api/v4/projects/24/labels",
"events": "https://scm.ntf.ro/api/v4/projects/24/events",
"members": "https://scm.ntf.ro/api/v4/projects/24/members",
"cluster_agents": "https://scm.ntf.ro/api/v4/projects/24/cluster_agents"
},
"packages_enabled": true,
"empty_repo": false,
"archived": false,
"visibility": "private",
"resolve_outdated_diff_discussions": false,
"container_expiration_policy": {
"cadence": "1d",
"enabled": false,
"keep_n": 10,
"older_than": "90d",
"name_regex": ".*",
"name_regex_keep": null,
"next_run_at": "2022-10-29T12:16:00.084Z"
},
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"jobs_enabled": true,
"snippets_enabled": true,
"container_registry_enabled": true,
"service_desk_enabled": false,
"service_desk_address": null,
"can_create_merge_request_in": true,
"issues_access_level": "enabled",
"repository_access_level": "enabled",
"merge_requests_access_level": "enabled",
"forking_access_level": "enabled",
"wiki_access_level": "enabled",
"builds_access_level": "enabled",
"snippets_access_level": "enabled",
"pages_access_level": "private",
"operations_access_level": "enabled",
"analytics_access_level": "enabled",
"container_registry_access_level": "enabled",
"security_and_compliance_access_level": "private",
"emails_disabled": null,
"shared_runners_enabled": false,
"lfs_enabled": true,
"creator_id": 7,
"import_url": null,
"import_type": null,
"import_status": "none",
"open_issues_count": 0,
"ci_default_git_depth": 20,
"ci_forward_deployment_enabled": true,
"ci_job_token_scope_enabled": false,
"ci_separated_caches": true,
"ci_opt_in_jwt": false,
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"public_jobs": true,
"build_timeout": 3600,
"auto_cancel_pending_pipelines": "enabled",
"ci_config_path": "",
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": true,
"allow_merge_on_skipped_pipeline": true,
"restrict_user_defined_variables": false,
"request_access_enabled": true,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": true,
"printing_merge_request_link_enabled": true,
"merge_method": "rebase_merge",
"squash_option": "default_off",
"enforce_auth_checks_on_uploads": true,
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"auto_devops_enabled": false,
"auto_devops_deploy_strategy": "continuous",
"autoclose_referenced_issues": true,
"keep_latest_artifact": true,
"runner_token_expiration_interval": null
}
]
Similarly, the subgroup Frontend
has two projects: Project-D
& Project-E
.
The aim is to get the IDs of all projects within the parent group including the ones which reside in any of the subgroups.
$ curl -s -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" -H "Content-Type: application/json" -X GET "https://scm.ntf.ro/api/v4/groups/28/projects/?include_subgroups=true" | jq .[].id
26
25
24
23
22
Having now the IDs, we are able to iterate over these and update the:
allow_merge_on_skipped_pipeline
totrue
. This allows Merge Requests to be merged with skipped jobs, as these are considered successful.merge_method
torebase_merge
. This is equivalent toMerge commit with semi-linear history
.only_allow_merge_if_pipeline_succeeds
totrue
. This enforces Merge Requests to be merged with successful jobs.
$ for PROJECT in `curl -s -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" -H "Content-Type: application/json" -X GET "https://scm.ntf.ro/api/v4/groups/28/projects/?include_subgroups=true" | jq .[].id`; do curl -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" -H "Content-Type: application/json" -X PUT --data '{"allow_merge_on_skipped_pipeline": true,"merge_method": "rebase_merge", "only_allow_merge_if_pipeline_succeeds": true}' "https://scm.ntf.ro/api/v4/projects/$PROJECT" | jq; done
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3726 0 3607 100 119 19631 647 --:--:-- --:--:-- --:--:-- 20250
{
"id": 26,
"description": null,
"name": "Project-E",
"name_with_namespace": "ELF / Frontend / Project-E",
"path": "project-e",
"path_with_namespace": "elf/frontend/project-e",
"created_at": "2022-10-28T12:16:35.687Z",
"default_branch": "master",
"tag_list": [],
"topics": [],
"ssh_url_to_repo": "git@scm.ntf.ro:elf/frontend/project-e.git",
"http_url_to_repo": "https://scm.ntf.ro/elf/frontend/project-e.git",
"web_url": "https://scm.ntf.ro/elf/frontend/project-e",
"readme_url": "https://scm.ntf.ro/elf/frontend/project-e/-/blob/master/README.md",
"avatar_url": null,
"forks_count": 0,
"star_count": 0,
"last_activity_at": "2022-10-28T12:16:35.687Z",
"namespace": {
"id": 29,
"name": "Frontend",
"path": "frontend",
"kind": "group",
"full_path": "elf/frontend",
"parent_id": 28,
"avatar_url": null,
"web_url": "https://scm.ntf.ro/groups/elf/frontend"
},
"container_registry_image_prefix": "scm.ntf.ro:5050/elf/frontend/project-e",
"_links": {
"self": "https://scm.ntf.ro/api/v4/projects/26",
"issues": "https://scm.ntf.ro/api/v4/projects/26/issues",
"merge_requests": "https://scm.ntf.ro/api/v4/projects/26/merge_requests",
"repo_branches": "https://scm.ntf.ro/api/v4/projects/26/repository/branches",
"labels": "https://scm.ntf.ro/api/v4/projects/26/labels",
"events": "https://scm.ntf.ro/api/v4/projects/26/events",
"members": "https://scm.ntf.ro/api/v4/projects/26/members",
"cluster_agents": "https://scm.ntf.ro/api/v4/projects/26/cluster_agents"
},
"packages_enabled": true,
"empty_repo": false,
"archived": false,
"visibility": "private",
"resolve_outdated_diff_discussions": false,
"container_expiration_policy": {
"cadence": "1d",
"enabled": false,
"keep_n": 10,
"older_than": "90d",
"name_regex": ".*",
"name_regex_keep": null,
"next_run_at": "2022-10-29T12:16:35.701Z"
},
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"jobs_enabled": true,
"snippets_enabled": true,
"container_registry_enabled": true,
"service_desk_enabled": false,
"service_desk_address": null,
"can_create_merge_request_in": true,
"issues_access_level": "enabled",
"repository_access_level": "enabled",
"merge_requests_access_level": "enabled",
"forking_access_level": "enabled",
"wiki_access_level": "enabled",
"builds_access_level": "enabled",
"snippets_access_level": "enabled",
"pages_access_level": "private",
"operations_access_level": "enabled",
"analytics_access_level": "enabled",
"container_registry_access_level": "enabled",
"security_and_compliance_access_level": "private",
"emails_disabled": null,
"shared_runners_enabled": false,
"lfs_enabled": true,
"creator_id": 7,
"import_url": null,
"import_type": null,
"import_status": "none",
"import_error": null,
"open_issues_count": 0,
"runners_token": "GR1348941KfJgQp7MPrVeNycLd9bs",
"ci_default_git_depth": 20,
"ci_forward_deployment_enabled": true,
"ci_job_token_scope_enabled": false,
"ci_separated_caches": true,
"ci_opt_in_jwt": false,
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"public_jobs": true,
"build_git_strategy": "fetch",
"build_timeout": 3600,
"auto_cancel_pending_pipelines": "enabled",
"ci_config_path": "",
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": true,
"allow_merge_on_skipped_pipeline": true,
"restrict_user_defined_variables": false,
"request_access_enabled": true,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": true,
"printing_merge_request_link_enabled": true,
"merge_method": "rebase_merge",
"squash_option": "default_off",
"enforce_auth_checks_on_uploads": true,
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"auto_devops_enabled": false,
"auto_devops_deploy_strategy": "continuous",
"autoclose_referenced_issues": true,
"keep_latest_artifact": true,
"runner_token_expiration_interval": null
}
<OMITTED OUTPUT>
In case there would be multiple groups as well, the above example could be extended with a nested for
(wink-wink).
Additionally, one could also skip the pipeline while rebasing with the API, or by using Git push options, or by adding [ci skip]
or [skip ci]
(using any capitalization) in a commit message.
Conclusion
Having the possibility to perform a rebase
without triggering a pipeline contributes to a higher velocity in terms of development process and the latest versions of GitLab do offer this functionality.
I did enjoy playing around with the GitLab API in order to achieve this with the least effort, especially in the context of having a considerable amount of projects.