gitlabform 5.4.0__tar.gz → 5.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/_releases.yml +6 -2
  2. {gitlabform-5.4.0 → gitlabform-5.5.1}/PKG-INFO +1 -1
  3. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/changelog.md +21 -0
  4. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/protected_branches.md +46 -4
  5. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/configuration/groups.py +1 -2
  6. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/__init__.py +4 -0
  7. gitlabform-5.5.1/gitlabform/processors/group/group_protected_branches_processor.py +146 -0
  8. gitlabform-5.5.1/gitlabform/processors/project/branches_processor.py +277 -0
  9. gitlabform-5.5.1/gitlabform/processors/util/branch_protection.py +263 -0
  10. {gitlabform-5.4.0 → gitlabform-5.5.1}/pyproject.toml +10 -10
  11. {gitlabform-5.4.0 → gitlabform-5.5.1}/tbump.toml +1 -1
  12. gitlabform-5.5.1/tests/acceptance/premium/test_group_protected_branches.py +152 -0
  13. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/processors/test_branches_processor.py +13 -12
  14. gitlabform-5.5.1/tests/unit/processors/test_group_protected_branches_processor.py +283 -0
  15. {gitlabform-5.4.0 → gitlabform-5.5.1}/uv.lock +257 -178
  16. gitlabform-5.4.0/gitlabform/processors/project/branches_processor.py +0 -514
  17. {gitlabform-5.4.0 → gitlabform-5.5.1}/.coveragerc +0 -0
  18. {gitlabform-5.4.0 → gitlabform-5.5.1}/.dockerignore +0 -0
  19. {gitlabform-5.4.0 → gitlabform-5.5.1}/.git-blame-ignore-revs +0 -0
  20. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/CODEOWNERS +0 -0
  21. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/ISSUE_TEMPLATE/question-or-other-issue.md +0 -0
  24. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/actions/setup-uv-local/README.md +0 -0
  25. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/actions/setup-uv-local/action.yml +0 -0
  26. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/dependabot.yml +0 -0
  27. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/_main.yml +0 -0
  28. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/docs.yml +0 -0
  29. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/linters.yml +0 -0
  30. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/pr_on_fork.yml +0 -0
  31. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/pr_on_main_repo.yml +0 -0
  32. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/prs.yml +0 -0
  33. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/tests-premium.yml +0 -0
  34. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/tests-standard.yml +0 -0
  35. {gitlabform-5.4.0 → gitlabform-5.5.1}/.github/workflows/tests-ultimate.yml +0 -0
  36. {gitlabform-5.4.0 → gitlabform-5.5.1}/.gitignore +0 -0
  37. {gitlabform-5.4.0 → gitlabform-5.5.1}/.lgtm.yml +0 -0
  38. {gitlabform-5.4.0 → gitlabform-5.5.1}/.mypy.ini +0 -0
  39. {gitlabform-5.4.0 → gitlabform-5.5.1}/.overrides/gitlabform-logo-favicon.png +0 -0
  40. {gitlabform-5.4.0 → gitlabform-5.5.1}/.overrides/gitlabform-logo-favicon.svg +0 -0
  41. {gitlabform-5.4.0 → gitlabform-5.5.1}/.python-version +0 -0
  42. {gitlabform-5.4.0 → gitlabform-5.5.1}/CONTRIBUTING.md +0 -0
  43. {gitlabform-5.4.0 → gitlabform-5.5.1}/Dockerfile +0 -0
  44. {gitlabform-5.4.0 → gitlabform-5.5.1}/LICENSE +0 -0
  45. {gitlabform-5.4.0 → gitlabform-5.5.1}/README.md +0 -0
  46. {gitlabform-5.4.0 → gitlabform-5.5.1}/codecov.yml +0 -0
  47. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/__init__.py +0 -0
  48. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/common.py +0 -0
  49. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/docs.py +0 -0
  50. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/env.py +0 -0
  51. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/gitlab/await-healthy.sh +0 -0
  52. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/gitlab/gitlab.rb +0 -0
  53. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/gitlab/healthcheck-and-setup.sh +0 -0
  54. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/gitlab/run_gitlab_in_docker.sh +0 -0
  55. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/gitlab/tests.Dockerfile +0 -0
  56. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/infra.py +0 -0
  57. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/main.py +0 -0
  58. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/qa.py +0 -0
  59. {gitlabform-5.4.0 → gitlabform-5.5.1}/dev/release.py +0 -0
  60. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/automation.md +0 -0
  61. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/contrib/coding_guidelines.md +0 -0
  62. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/contrib/implementation_design.md +0 -0
  63. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/contrib/index.md +0 -0
  64. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/contrib/local_development.md +0 -0
  65. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/contrib/releases.md +0 -0
  66. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/contrib/workflows.md +0 -0
  67. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/features.md +0 -0
  68. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/egnyte-logo.svg +0 -0
  69. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/elastic-path-logo.svg +0 -0
  70. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/gitlabform-logo-square.png +0 -0
  71. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/gitlabform-logo.png +0 -0
  72. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/gitlabform-logo.svg +0 -0
  73. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/group-ldap-links-provider.png +0 -0
  74. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/liquidlight-logo.svg +0 -0
  75. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/roche-logo.png +0 -0
  76. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/images/stroeer-online-marketing-logo.svg +0 -0
  77. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/index.md +0 -0
  78. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/installation.md +0 -0
  79. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/archive_unarchive.md +0 -0
  80. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/avatar.md +0 -0
  81. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/badges.md +0 -0
  82. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/ci_cd_variables.md +0 -0
  83. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/deploy_keys.md +0 -0
  84. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/files.md +0 -0
  85. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/group_ldap_links.md +0 -0
  86. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/group_saml_links.md +0 -0
  87. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/index.md +0 -0
  88. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/integrations.md +0 -0
  89. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/job_token_scope.md +0 -0
  90. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/labels.md +0 -0
  91. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/members.md +0 -0
  92. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/merge_requests.md +0 -0
  93. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/pipeline_schedules.md +0 -0
  94. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/project_security_settings.md +0 -0
  95. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/project_transfer.md +0 -0
  96. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/protected_environments.md +0 -0
  97. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/push_mirrors.md +0 -0
  98. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/push_rules.md +0 -0
  99. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/resource_groups.md +0 -0
  100. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/settings.md +0 -0
  101. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/tags_protection.md +0 -0
  102. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/reference/webhooks.md +0 -0
  103. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/requirements.md +0 -0
  104. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/running.md +0 -0
  105. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/similar_apps.md +0 -0
  106. {gitlabform-5.4.0 → gitlabform-5.5.1}/docs/upgrade.md +0 -0
  107. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlab-config/config.yml +0 -0
  108. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlab-config/gitlab-config.md +0 -0
  109. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/__init__.py +0 -0
  110. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/configuration/__init__.py +0 -0
  111. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/configuration/common.py +0 -0
  112. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/configuration/core.py +0 -0
  113. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/configuration/projects.py +0 -0
  114. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/configuration/transform.py +0 -0
  115. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/constants.py +0 -0
  116. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/__init__.py +0 -0
  117. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/commits.py +0 -0
  118. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/core.py +0 -0
  119. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/group_badges.py +0 -0
  120. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/group_ldap_links.py +0 -0
  121. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/groups.py +0 -0
  122. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/merge_requests.py +0 -0
  123. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/pipelines.py +0 -0
  124. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/project_badges.py +0 -0
  125. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/project_deploy_keys.py +0 -0
  126. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  127. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/project_protected_environments.py +0 -0
  128. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/projects.py +0 -0
  129. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/python_gitlab.py +0 -0
  130. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/gitlab/variables.py +0 -0
  131. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/lists/__init__.py +0 -0
  132. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/lists/filter.py +0 -0
  133. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/lists/groups.py +0 -0
  134. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/lists/projects.py +0 -0
  135. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/output.py +0 -0
  136. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/__init__.py +0 -0
  137. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/abstract_processor.py +0 -0
  138. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/application/__init__.py +0 -0
  139. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/application/application_settings_processor.py +0 -0
  140. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/defining_keys.py +0 -0
  141. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_badges_processor.py +0 -0
  142. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_hooks_processor.py +0 -0
  143. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_labels_processor.py +0 -0
  144. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
  145. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_members_processor.py +0 -0
  146. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
  147. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
  148. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_settings_processor.py +0 -0
  149. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/group/group_variables_processor.py +0 -0
  150. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/multiple_entities_processor.py +0 -0
  151. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/__init__.py +0 -0
  152. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/badges_processor.py +0 -0
  153. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  154. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/files_processor.py +0 -0
  155. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/hooks_processor.py +0 -0
  156. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/integrations_processor.py +0 -0
  157. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  158. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/members_processor.py +0 -0
  159. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  160. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  161. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/project_labels_processor.py +0 -0
  162. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/project_processor.py +0 -0
  163. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  164. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/project_security_settings.py +0 -0
  165. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/project_settings_processor.py +0 -0
  166. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/project_variables_processor.py +0 -0
  167. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/remote_mirrors_processor.py +0 -0
  168. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  169. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/schedules_processor.py +0 -0
  170. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/project/tags_processor.py +0 -0
  171. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/shared/__init__.py +0 -0
  172. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
  173. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/util/__init__.py +0 -0
  174. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/util/decorators.py +0 -0
  175. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/util/difference_logger.py +0 -0
  176. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/util/labels_processor.py +0 -0
  177. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/processors/util/variables_processor.py +0 -0
  178. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/run.py +0 -0
  179. {gitlabform-5.4.0 → gitlabform-5.5.1}/gitlabform/util.py +0 -0
  180. {gitlabform-5.4.0 → gitlabform-5.5.1}/mkdocs.yml +0 -0
  181. {gitlabform-5.4.0 → gitlabform-5.5.1}/prek.toml +0 -0
  182. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/__init__.py +0 -0
  183. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/__init__.py +0 -0
  184. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/conftest.py +0 -0
  185. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_branches.py +0 -0
  186. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_branches_users_case_insensitive.py +0 -0
  187. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_group_ldap_links.py +0 -0
  188. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_group_push_rules.py +0 -0
  189. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_group_saml_links.py +0 -0
  190. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_group_settings.py +0 -0
  191. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_group_variables.py +0 -0
  192. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_merge_request_approval_rules.py +0 -0
  193. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_merge_request_approvals_settings.py +0 -0
  194. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_protected_environments.py +0 -0
  195. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_push_rules.py +0 -0
  196. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/premium/test_tags.py +0 -0
  197. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/__init__.py +0 -0
  198. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_application_settings.py +0 -0
  199. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_archive_project.py +0 -0
  200. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_badges.py +0 -0
  201. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_branches.py +0 -0
  202. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_deploy_keys.py +0 -0
  203. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_deploy_keys_all_projects.py +0 -0
  204. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_files.py +0 -0
  205. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_files_templates.py +0 -0
  206. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_avatar.py +0 -0
  207. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_badges.py +0 -0
  208. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_hooks.py +0 -0
  209. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_labels.py +0 -0
  210. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_members_case_insensitive.py +0 -0
  211. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_members_groups.py +0 -0
  212. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_members_users.py +0 -0
  213. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_settings.py +0 -0
  214. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_group_variables.py +0 -0
  215. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_hooks.py +0 -0
  216. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_integrations.py +0 -0
  217. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_job_token_scope.py +0 -0
  218. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_members.py +0 -0
  219. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_members_add_group.py +0 -0
  220. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_members_enforce.py +0 -0
  221. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_project_avatar.py +0 -0
  222. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_project_group_members_case_insensitive.py +0 -0
  223. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_project_labels.py +0 -0
  224. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_project_members_case_insensitve.py +0 -0
  225. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_project_settings.py +0 -0
  226. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_project_variables.py +0 -0
  227. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_remote_mirrors.py +0 -0
  228. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_resource_groups.py +0 -0
  229. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_running.py +0 -0
  230. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_scheduled_for_deletion_project.py +0 -0
  231. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_schedules.py +0 -0
  232. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_tags.py +0 -0
  233. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_token_from_config.py +0 -0
  234. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/standard/test_transfer_project.py +0 -0
  235. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/ultimate/test_group_members.py +0 -0
  236. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/ultimate/test_group_settings.py +0 -0
  237. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/ultimate/test_members.py +0 -0
  238. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/ultimate/test_project_security_settings.py +0 -0
  239. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/acceptance/ultimate/test_project_settings.py +0 -0
  240. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/__init__.py +0 -0
  241. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/__init__.py +0 -0
  242. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_case_sensitivity.py +0 -0
  243. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_inheritance_break_projects_and_groups.py +0 -0
  244. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_inheritance_break_subgroups.py +0 -0
  245. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_inheritance_break_validation.py +0 -0
  246. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_projects_and_groups.py +0 -0
  247. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_skip_groups_skip_projects.py +0 -0
  248. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_subgroups.py +0 -0
  249. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/test_yaml_version.py +0 -0
  250. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/transform/test_access_level_transformer.py +0 -0
  251. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/transform/test_group_transformer.py +0 -0
  252. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/transform/test_implicit_name_transformer.py +0 -0
  253. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/transform/test_user_and_group_transformers.py +0 -0
  254. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/configuration/transform/test_user_transformer.py +0 -0
  255. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/gitlab/test_core.py +0 -0
  256. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/gitlab/test_python_gitlab.py +0 -0
  257. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/processors/__init__.py +0 -0
  258. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/processors/test_abstract_processor.py +0 -0
  259. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/processors/test_abstract_processors.py +0 -0
  260. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/processors/test_difference_logger.py +0 -0
  261. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/processors/test_schedules_processor_extended_cron_pattern.py +0 -0
  262. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/test_access_levels.py +0 -0
  263. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/test_non_empty_configs_provider.py +0 -0
  264. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/test_parse_args.py +0 -0
  265. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/test_projects_provider.py +0 -0
  266. {gitlabform-5.4.0 → gitlabform-5.5.1}/tests/unit/test_utils.py +0 -0
@@ -4,6 +4,8 @@ on:
4
4
  push:
5
5
  tags:
6
6
  - 'v*'
7
+ # Allow manual triggering of the workflow
8
+ workflow_dispatch:
7
9
 
8
10
  permissions:
9
11
  contents: write
@@ -122,8 +124,10 @@ jobs:
122
124
  run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
123
125
  - name: Wait for PyPI release
124
126
  run: |
125
- uv pip download gitlabform==${{ env.VERSION }} > /dev/null
126
- while [ $? -ne 0 ]; do sleep 10; uv pip download gitlabform==${{ env.VERSION }} > /dev/null ; done
127
+ until uv pip install gitlabform==${{ env.VERSION }} --dry-run > /dev/null 2>&1; do
128
+ echo "Waiting for gitlabform==${{ env.VERSION }} to be available on PyPI..."
129
+ sleep 10
130
+ done
127
131
  shell: bash {0}
128
132
  - name: Docker metadata
129
133
  id: metadata
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlabform
3
- Version: 5.4.0
3
+ Version: 5.5.1
4
4
  Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
5
5
  Project-URL: Homepage, https://gitlabform.github.io/gitlabform/
6
6
  Project-URL: Repository, https://github.com/gitlabform/gitlabform.git
@@ -5,6 +5,27 @@ This changelog follows industry-standard best practices. Each release includes t
5
5
  For details on how to migrate between major versions, please refer to the [upgrade guide](upgrade.md), which contains important instructions for breaking changes.
6
6
 
7
7
  ---
8
+
9
+ ## 5.5.1
10
+
11
+ ### Build
12
+
13
+ * Fix issue where tagged images were no longer being published to Github Container Registry [#1291](https://github.com/gitlabform/gitlabform/pull/1291). ([amimas](https://github.com/amimas))
14
+
15
+ Thanks to all the contributors of this release!
16
+
17
+ ## 5.5.0
18
+
19
+ ### Features
20
+
21
+ * added support for group level branch protection settings. Resolves [#1049](https://github.com/gitlabform/gitlabform/issues/1049). [#1251](https://github.com/gitlabform/gitlabform/pull/1251), ([rickbrouwer](https://github.com/rickbrouwer)).
22
+
23
+ ### Dependencies
24
+
25
+ * Update various dependencies to newer versions.
26
+
27
+ Thanks to all the contributors of this release!
28
+
8
29
  ## 5.4.0
9
30
 
10
31
  ### Build
@@ -2,7 +2,49 @@
2
2
 
3
3
  This section purpose is to manage the [protected branches](https://docs.gitlab.com/user/project/repository/branches/protected/).
4
4
 
5
- ## Community Edition vs Enterprise Edition
5
+ GitLabForm supports two different ways to manage branch protection:
6
+
7
+ * **Group-level protected branches** (`group_protected_branches`) — Uses GitLab's [Group Protected Branches API](https://docs.gitlab.com/ee/api/group_protected_branches.html) to set branch protection rules that are inherited by all projects in a top-level group. Requires GitLab Premium.
8
+ * **Project-level protected branches** (`branches`) — Sets branch protection rules on individual projects. When configured under a group wildcard (e.g. `group/*`), GitLabForm applies the rules to each project separately. Works with all GitLab tiers.
9
+
10
+ ## Group-level protected branches
11
+
12
+ This section purpose is to manage the [group-level protected branches](https://docs.gitlab.com/ee/api/group_protected_branches.html).
13
+
14
+ !!! info
15
+
16
+ This section requires GitLab Premium (paid). (This is a GitLab limitation, not GitLabForm's.)
17
+
18
+ !!! warning
19
+
20
+ Protected branch settings for groups are restricted to top-level groups only. (This is a GitLab limitation, not GitLabForm's.) GitLabForm applies this section only to the top-level group; when the configuration is inherited by subgroups via the `group_1/*` wildcard, GitLabForm skips the subgroups.
21
+
22
+ Group protected branches only support access levels. Individual users and groups cannot be specified. (This is a GitLab API limitation).
23
+
24
+ Example:
25
+
26
+ ```yaml
27
+ projects_and_groups:
28
+ group_1/*:
29
+ group_protected_branches:
30
+ main:
31
+ protected: true
32
+ push_access_level: no access
33
+ merge_access_level: maintainer
34
+ release/*:
35
+ protected: true
36
+ push_access_level: no access
37
+ merge_access_level: maintainer
38
+ unprotect_access_level: maintainer
39
+ ```
40
+
41
+ !!! note
42
+
43
+ A group-level protected branch also appears under the protected branches list of every project in the group, marked with a lock icon. Inherited rules cannot be edited or removed from the project. Projects can still define their own additional protected branch rules on top of the inherited ones via the project-level [`branches`](#project-level-protected-branches) section.
44
+
45
+ ## Project-level protected branches
46
+
47
+ ### Community Edition vs Enterprise Edition
6
48
  Note: that Gitlab Community Edition does not support setting `unprotect_access_level` and will always return `None` from the API, and there is also no way to manually set this through the UI.
7
49
 
8
50
  ### Functionality Differences
@@ -13,7 +55,7 @@ In Gitlab EE versions <=15.6.0 and Gitlab Community Edition, GitLabForm uses old
13
55
 
14
56
  In later versions of Gitlab EE, GitLabForm will modify the Branch Protection rules in-place, see the [V4->V5 upgrade notes](../upgrade.md)
15
57
 
16
- ## Common features
58
+ ### Common features
17
59
 
18
60
  The key names here may be:
19
61
 
@@ -63,7 +105,7 @@ projects_and_groups:
63
105
  allow_force_push: true
64
106
  ```
65
107
 
66
- ## Premium-only features
108
+ ### Premium-only features
67
109
 
68
110
  !!! info
69
111
 
@@ -106,4 +148,4 @@ projects_and_groups:
106
148
  - group_id: 456 # ...or group ids, if you know them...
107
149
  allowed_to_unprotect:
108
150
  - access_level: maintainer # ...or the whole access levels
109
- ```
151
+ ```
@@ -24,8 +24,7 @@ class ConfigurationGroups(ConfigurationCommon, ABC):
24
24
  for element in projects_and_groups.keys():
25
25
  if element.endswith("/*"):
26
26
  # cut off that "/*"
27
- group_name = element[:-2]
28
- groups.append(group_name)
27
+ groups.append(element[:-2])
29
28
  return sorted(groups)
30
29
 
31
30
  def is_group_skipped(self, group):
@@ -30,6 +30,9 @@ from gitlabform.processors.group.group_push_rules_processor import (
30
30
  from gitlabform.processors.group.group_hooks_processor import (
31
31
  GroupHooksProcessor,
32
32
  )
33
+ from gitlabform.processors.group.group_protected_branches_processor import (
34
+ GroupProtectedBranchesProcessor,
35
+ )
33
36
 
34
37
 
35
38
  class GroupProcessors(AbstractProcessors):
@@ -45,4 +48,5 @@ class GroupProcessors(AbstractProcessors):
45
48
  GroupLabelsProcessor(gitlab),
46
49
  GroupHooksProcessor(gitlab),
47
50
  GroupPushRulesProcessor(gitlab),
51
+ GroupProtectedBranchesProcessor(gitlab, strict),
48
52
  ]
@@ -0,0 +1,146 @@
1
+ import sys
2
+ from typing import Optional
3
+
4
+ from logging import info, warning, error, critical, debug
5
+ from gitlab import GitlabGetError, GitlabDeleteError, GitlabOperationError
6
+ from gitlab.v4.objects.branches import GroupProtectedBranch
7
+ from gitlab.v4.objects.groups import Group
8
+
9
+ from gitlabform.constants import EXIT_INVALID_INPUT, EXIT_PROCESSING_ERROR
10
+ from gitlabform.gitlab import GitLab
11
+ from gitlabform.processors.abstract_processor import AbstractProcessor
12
+ from gitlabform.processors.util.branch_protection import BranchProtection
13
+
14
+
15
+ class GroupProtectedBranchesProcessor(AbstractProcessor):
16
+
17
+ def __init__(self, gitlab: GitLab, strict: bool):
18
+ super().__init__("group_protected_branches", gitlab)
19
+ self.strict = strict
20
+
21
+ self.custom_diff_analyzers["merge_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
22
+ self.custom_diff_analyzers["push_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
23
+ self.custom_diff_analyzers["unprotect_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
24
+
25
+ def _can_proceed(self, group: str, configuration: dict):
26
+ if "/" in group:
27
+ debug(f"Skipping group_protected_branches for '{group}' - only supported for top-level groups")
28
+ return False
29
+
30
+ for branch in sorted(configuration["group_protected_branches"]):
31
+ branch_config = configuration["group_protected_branches"][branch]
32
+ if branch_config.get("protected") is None:
33
+ critical(
34
+ f"The Protected key is mandatory in group_protected_branches configuration, fix {branch} YAML config"
35
+ )
36
+ sys.exit(EXIT_INVALID_INPUT)
37
+
38
+ for key in ("allowed_to_push", "allowed_to_merge", "allowed_to_unprotect"):
39
+ if BranchProtection.branch_protection_config_contains_user_or_group(branch_config, key):
40
+ warning(
41
+ f"Group protected branches do not support individual users or groups in '{key}' for branch '{branch}'. "
42
+ f"Only access_level is supported. The user/group entry will be ignored by GitLab."
43
+ )
44
+
45
+ return True
46
+
47
+ def _process_configuration(self, group: str, configuration: dict):
48
+
49
+ gitlab_group: Group = self.gl.get_group_by_path_cached(group)
50
+
51
+ for branch in sorted(configuration["group_protected_branches"]):
52
+ branch_configuration: dict = configuration["group_protected_branches"][branch]
53
+
54
+ self.process_branch_protection(gitlab_group, branch, branch_configuration)
55
+
56
+ def process_branch_protection(self, group: Group, branch_name: str, branch_config: dict):
57
+ protected_branch: Optional[GroupProtectedBranch] = None
58
+
59
+ try:
60
+ protected_branch = group.protectedbranches.get(branch_name)
61
+ except GitlabGetError:
62
+ info(f"The branch '{branch_name}' is not protected at group level!")
63
+
64
+ if branch_config.get("protected"):
65
+ if not protected_branch:
66
+ info(f"Creating group-level branch protection for {branch_name}")
67
+ self.protect_branch(group, branch_name, branch_config, False)
68
+ return
69
+
70
+ transformed_branch_config = BranchProtection.map_config_to_protected_branch_get_data(branch_config)
71
+
72
+ protected_branch_api_patch_data: dict = {}
73
+
74
+ special_list_keys = [
75
+ "merge_access_levels",
76
+ "push_access_levels",
77
+ "unprotect_access_levels",
78
+ ]
79
+ for key, value in transformed_branch_config.items():
80
+ if key not in special_list_keys:
81
+ existing_value = getattr(protected_branch, key, None)
82
+ if existing_value != value:
83
+ info(f"Creating data to update {key} as necessary")
84
+ protected_branch_api_patch_data[key] = value
85
+
86
+ info("Creating data to update merge_access_levels as necessary")
87
+ merge_access_items_patch_data = BranchProtection.build_patch_request_data(
88
+ transformed_access_levels=transformed_branch_config.get("merge_access_levels"),
89
+ existing_records=tuple(BranchProtection.get_list_attribute(protected_branch, "merge_access_levels")),
90
+ )
91
+ if len(merge_access_items_patch_data) > 0:
92
+ protected_branch_api_patch_data["allowed_to_merge"] = merge_access_items_patch_data
93
+
94
+ info("Creating data to update push_access_levels as necessary")
95
+ push_access_items_patch_data = BranchProtection.build_patch_request_data(
96
+ transformed_access_levels=transformed_branch_config.get("push_access_levels"),
97
+ existing_records=tuple(BranchProtection.get_list_attribute(protected_branch, "push_access_levels")),
98
+ )
99
+ if len(push_access_items_patch_data) > 0:
100
+ protected_branch_api_patch_data["allowed_to_push"] = push_access_items_patch_data
101
+
102
+ info("Creating data to update unprotect_access_levels as necessary")
103
+ unprotect_access_items_patch_data = BranchProtection.build_patch_request_data(
104
+ transformed_access_levels=transformed_branch_config.get("unprotect_access_levels"),
105
+ existing_records=tuple(
106
+ BranchProtection.get_list_attribute(protected_branch, "unprotect_access_levels")
107
+ ),
108
+ )
109
+ if len(unprotect_access_items_patch_data) > 0:
110
+ protected_branch_api_patch_data["allowed_to_unprotect"] = unprotect_access_items_patch_data
111
+
112
+ if protected_branch_api_patch_data != {}:
113
+ info(f"Updating group-level protected branch {branch_name} with {protected_branch_api_patch_data}")
114
+ self.protect_branch(group, branch_name, protected_branch_api_patch_data, True)
115
+
116
+ elif protected_branch and not branch_config.get("protected"):
117
+ info(f"Removing group-level branch protection for {branch_name}")
118
+ self.unprotect_branch(protected_branch)
119
+
120
+ def protect_branch(self, group: Group, branch_name: str, branch_config: dict, update_only: bool = False):
121
+ try:
122
+ if update_only:
123
+ group.protectedbranches.update(branch_name, branch_config)
124
+ else:
125
+ group.protectedbranches.create({"name": branch_name, **branch_config})
126
+ except GitlabOperationError as e:
127
+ message = f"Protecting branch '{branch_name}' at group level failed! Error '{e.error_message}"
128
+
129
+ if self.strict:
130
+ critical(message)
131
+ sys.exit(EXIT_PROCESSING_ERROR)
132
+ else:
133
+ error(message)
134
+
135
+ def unprotect_branch(self, protected_branch: GroupProtectedBranch):
136
+ try:
137
+ protected_branch.delete()
138
+ except GitlabDeleteError as e:
139
+ message = (
140
+ f"Branch '{protected_branch.name}' could not be unprotected at group level! Error '{e.error_message}'"
141
+ )
142
+ if self.strict:
143
+ critical(message)
144
+ sys.exit(EXIT_PROCESSING_ERROR)
145
+ else:
146
+ warning(message)
@@ -0,0 +1,277 @@
1
+ import sys
2
+ from typing import Optional, Any
3
+
4
+ from logging import info, warning, error, critical
5
+ from gitlab import (
6
+ GitlabGetError,
7
+ GitlabDeleteError,
8
+ GitlabOperationError,
9
+ )
10
+ from gitlab.v4.objects import Project, ProjectProtectedBranch
11
+
12
+ from gitlabform.constants import EXIT_INVALID_INPUT, EXIT_PROCESSING_ERROR
13
+ from gitlabform.gitlab import GitLab
14
+ from gitlabform.processors.abstract_processor import AbstractProcessor
15
+ from gitlabform.processors.util.branch_protection import BranchProtection
16
+
17
+
18
+ class BranchesProcessor(AbstractProcessor):
19
+ """
20
+ Processor for branch protection settings.
21
+
22
+ This processor is complex because GitLab's Protected Branches API uses different
23
+ data structures for Create (POST), Get (GET), and Update (PATCH) operations.
24
+
25
+ It implements 'Additive Design' (existing rules are preserved) and
26
+ 'Raw Parameter Passing' (arbitrary keys in config are sent to the API).
27
+ """
28
+
29
+ def __init__(self, gitlab: GitLab, strict: bool):
30
+ super().__init__("branches", gitlab)
31
+ self.strict = strict
32
+
33
+ # Protected Branch API: https://docs.gitlab.com/api/protected_branches/#update-a-protected-branch
34
+ # Behind the scenes gitlab will map "allowed_to_merge" and "merge_access_level" to "merge_access_levels"
35
+ # and the same for unprotect and push access, so we need some custom diff analyzers to validate if the config
36
+ # has actually changed
37
+ self.custom_diff_analyzers["merge_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
38
+ self.custom_diff_analyzers["push_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
39
+ self.custom_diff_analyzers["unprotect_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
40
+
41
+ def _can_proceed(self, project_or_group: str, configuration: dict):
42
+ for branch in sorted(configuration["branches"]):
43
+ branch_config = configuration["branches"][branch]
44
+ if branch_config.get("protected") is None:
45
+ critical(f"The Protected key is mandatory in branches configuration, fix {branch} YAML config")
46
+ sys.exit(EXIT_INVALID_INPUT)
47
+
48
+ return True
49
+
50
+ def _process_configuration(self, project_and_group: str, configuration: dict):
51
+ """
52
+ Called from process defined in abstract_processor.py after checking self._can_proceed
53
+ Iterates through all branches defined in the configuration and applies protection rules.
54
+ """
55
+
56
+ project: Project = self.gl.get_project_by_path_cached(project_and_group)
57
+
58
+ for branch in sorted(configuration["branches"]):
59
+ branch_configuration: dict = self.convert_user_and_group_names_to_ids(configuration["branches"][branch])
60
+
61
+ self.process_branch_protection(project, branch, branch_configuration)
62
+
63
+ def process_branch_protection(self, project: Project, branch_name: str, branch_config: dict):
64
+ """
65
+ High-level logic for processing branch protection.
66
+
67
+ 1. Validates branch existence (unless wildcard).
68
+ 2. Handles 'protected: false' (unprotecting).
69
+ 3. Handles 'protected: true':
70
+ - If not currently protected: Create protection.
71
+ - If protected: Update using PATCH (EE > 15.6) or Unprotect/Reprotect (CE/Old EE).
72
+ """
73
+ protected_branch: Optional[ProjectProtectedBranch] = None
74
+
75
+ # If protected branch name contains a supported wildcard do not try looking it up
76
+ if not self.branch_name_contains_supported_wildcard(branch_name):
77
+ # Check branch we are trying to protect actually exists first
78
+ try:
79
+ project.branches.get(branch_name)
80
+ except GitlabGetError:
81
+ message = f"Branch '{branch_name}' not found when processing it to be protected/unprotected!"
82
+ if self.strict:
83
+ critical(message)
84
+ sys.exit(EXIT_INVALID_INPUT)
85
+ else:
86
+ warning(message)
87
+
88
+ try:
89
+ protected_branch = project.protectedbranches.get(branch_name)
90
+ except GitlabGetError:
91
+ message = f"The branch '{branch_name}' is not protected!"
92
+ info(message)
93
+
94
+ if branch_config.get("protected"):
95
+ if not protected_branch:
96
+ info(f"Creating branch protection for {branch_name}")
97
+ self.protect_branch(project, branch_name, branch_config, False)
98
+ return
99
+
100
+ # https://docs.gitlab.com/api/protected_branches/#update-a-protected-branch was only introduced after 15.6
101
+ # for user's on older versions of Gitlab or Community Edition (https://gitlab.com/rluna-gitlab/gitlab-ce/-/work_items/37)
102
+ # We need to unprotect and then reprotect the branch to apply the
103
+ # defined configuration
104
+ if self.gitlab.is_version_less_than("15.6.0") or (self.gitlab.enterprise == False):
105
+ self.process_branch_config_gitlab_under_15_6_0_or_ce(
106
+ branch_config, branch_name, project, protected_branch
107
+ )
108
+ return
109
+
110
+ # For later Gitlab versions we dynamically generate the data to send to the update endpoint based on the
111
+ # configured state and the current Gitlab state so do not need to check _needs_update first.
112
+
113
+ # Gitlab returns the allowed_to_merge etc. data in a different format from GET endpoint than it takes in
114
+ # the POST (create) or PATCH (update) endpoints
115
+ # GET: https://docs.gitlab.com/api/protected_branches/#get-a-single-protected-branch-or-wildcard-protected-branch
116
+ # POST: https://docs.gitlab.com/api/protected_branches/#protect-repository-branches
117
+ # PATCH: https://docs.gitlab.com/api/protected_branches/#update-a-protected-branch
118
+ # Therefore we first transform the configured YAML into a state matching the gitlab GET endpoint so we can
119
+ # compare states easier to determine what PATCH request we need to send (if any)
120
+ transformed_branch_config = BranchProtection.map_config_to_protected_branch_get_data(branch_config)
121
+
122
+ protected_branch_api_patch_data: dict = {}
123
+
124
+ # RAW PARAMETER PASSING (Top-Level):
125
+ # Iterate through any top-level flags (e.g., allow_force_push, code_owner_approval_required)
126
+ # and include them in the PATCH request if they differ from the current GitLab state.
127
+ special_list_keys = [
128
+ "merge_access_levels",
129
+ "push_access_levels",
130
+ "unprotect_access_levels",
131
+ ]
132
+ for key, value in transformed_branch_config.items():
133
+ if key not in special_list_keys:
134
+ # Check if this attribute exists on the GitLab object and needs an update
135
+ existing_value = getattr(protected_branch, key, None)
136
+ if existing_value != value:
137
+ info(f"Creating data to update {key} as necessary")
138
+ protected_branch_api_patch_data[key] = value
139
+
140
+ info("Creating data to update merge_access_levels as necessary")
141
+ merge_access_items_patch_data = BranchProtection.build_patch_request_data(
142
+ transformed_access_levels=transformed_branch_config.get("merge_access_levels"),
143
+ existing_records=tuple(BranchProtection.get_list_attribute(protected_branch, "merge_access_levels")),
144
+ )
145
+ if len(merge_access_items_patch_data) > 0:
146
+ protected_branch_api_patch_data["allowed_to_merge"] = merge_access_items_patch_data
147
+
148
+ info("Creating data to update push_access_levels as necessary")
149
+ push_access_items_patch_data = BranchProtection.build_patch_request_data(
150
+ transformed_access_levels=transformed_branch_config.get("push_access_levels"),
151
+ existing_records=tuple(BranchProtection.get_list_attribute(protected_branch, "push_access_levels")),
152
+ )
153
+ if len(push_access_items_patch_data) > 0:
154
+ protected_branch_api_patch_data["allowed_to_push"] = push_access_items_patch_data
155
+
156
+ info("Creating data to update unprotect_access_levels as necessary")
157
+
158
+ unprotect_access_items_patch_data = BranchProtection.build_patch_request_data(
159
+ transformed_access_levels=transformed_branch_config.get("unprotect_access_levels"),
160
+ existing_records=tuple(
161
+ BranchProtection.get_list_attribute(protected_branch, "unprotect_access_levels")
162
+ ),
163
+ )
164
+
165
+ if len(unprotect_access_items_patch_data) > 0:
166
+ protected_branch_api_patch_data["allowed_to_unprotect"] = unprotect_access_items_patch_data
167
+
168
+ if protected_branch_api_patch_data != {}:
169
+ # We have some updates to make
170
+ info(f"Updating protected branch {branch_name} with {protected_branch_api_patch_data}")
171
+ self.protect_branch(project, branch_name, protected_branch_api_patch_data, True)
172
+
173
+ elif protected_branch and not branch_config.get("protected"):
174
+ info(f"Removing branch protection for {branch_name}")
175
+ self.unprotect_branch(protected_branch)
176
+
177
+ def process_branch_config_gitlab_under_15_6_0_or_ce(self, branch_config, branch_name, project, protected_branch):
178
+ """
179
+ Processes the branches configuration for gitlab version <=15.6.0 or Community Edition,
180
+ where in-place updates (PATCH) are not supported or effective.
181
+ If a change is detected, the branch is unprotected and then reprotected from scratch.
182
+ """
183
+
184
+ # Gitlab returns the allowed_to_merge etc data in a different format from GET endpoint than it takes in
185
+ # the POST (create) endpoint
186
+ # GET: https://docs.gitlab.com/api/protected_branches/#get-a-single-protected-branch-or-wildcard-protected-branch
187
+ # POST: https://docs.gitlab.com/api/protected_branches/#protect-repository-branches
188
+ # Therefore we first transform the configured YAML into a state matching the gitlab GET endpoint,
189
+ # before checking if it needs_update
190
+ if self._needs_update(
191
+ protected_branch.attributes, BranchProtection.map_config_to_protected_branch_get_data(branch_config)
192
+ ):
193
+ info(
194
+ f"Gitlab version is less than 15.6.0, so un-protecting and reprotecting branch {branch_name} to apply new config..."
195
+ )
196
+ self.unprotect_branch(protected_branch)
197
+
198
+ # Send the untransformed config to the POST endpoint, as GitlabForm YAML structure conforms to the POST inputs
199
+ self.protect_branch(project, branch_name, branch_config, False)
200
+
201
+ def protect_branch(self, project: Project, branch_name: str, branch_config: dict, update_only: bool = False):
202
+ """
203
+ Create or update branch protection using given config.
204
+ Raise exception if running in strict mode.
205
+
206
+ args:
207
+ project (Project): Gitlab project
208
+ branch_name (str): Name of branch on the project to protect or update protection on
209
+ branch_config (dict): Branch protection configuration to apply
210
+ update_only (bool):
211
+ If True, update branch protection of branch with branch_name,
212
+ If False create a new protected branch with branch_name
213
+ """
214
+ try:
215
+ if update_only:
216
+ project.protectedbranches.update(branch_name, branch_config)
217
+ else:
218
+ project.protectedbranches.create({"name": branch_name, **branch_config})
219
+ except GitlabOperationError as e:
220
+ message = f"Protecting branch '{branch_name}' failed! Error '{e.error_message}"
221
+
222
+ if self.strict:
223
+ critical(message)
224
+ sys.exit(EXIT_PROCESSING_ERROR)
225
+ else:
226
+ error(message)
227
+
228
+ def unprotect_branch(self, protected_branch: ProjectProtectedBranch):
229
+ """
230
+ Unprotect a branch.
231
+ Raise exception if running in strict mode.
232
+ """
233
+ try:
234
+ # The delete method doesn't delete the actual branch.
235
+ # It only unprotects the branch.
236
+ protected_branch.delete()
237
+ except GitlabDeleteError as e:
238
+ message = f"Branch '{protected_branch.name}' could not be unprotected! Error '{e.error_message}'"
239
+ if self.strict:
240
+ critical(message)
241
+ sys.exit(EXIT_PROCESSING_ERROR)
242
+ else:
243
+ warning(message)
244
+
245
+ def convert_user_and_group_names_to_ids(self, branch_config: dict):
246
+ """
247
+ Pre-processor to resolve names to IDs.
248
+ Translates 'user: username' or 'group: name' into 'user_id' or 'group_id' as
249
+ config by replacing them with ids.
250
+ """
251
+ info("Transforming User and Group names in Branch configuration to Ids")
252
+
253
+ for key in branch_config:
254
+ if isinstance(branch_config[key], list):
255
+ for item in branch_config[key]:
256
+ if isinstance(item, dict):
257
+ if "user" in item:
258
+ username = item.pop("user")
259
+ user_id = self.gl.get_user_id_cached(username)
260
+ if user_id is None:
261
+ raise GitlabGetError(
262
+ f"transform_branch_config - No users found when searching for username {username}",
263
+ 404,
264
+ )
265
+ item["user_id"] = user_id
266
+ elif "group" in item:
267
+ item["group_id"] = self.gl.get_group_id(item.pop("group"))
268
+
269
+ return branch_config
270
+
271
+ @staticmethod
272
+ def branch_name_contains_supported_wildcard(branch):
273
+ """
274
+ Gitlab supports "*" wildcards when protecting branches:
275
+ https://docs.gitlab.com/user/project/repository/branches/protected/#use-wildcard-rules
276
+ """
277
+ return "*" in branch