gitlabform 6.2.3__tar.gz → 6.3.0__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 (282) hide show
  1. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/pr-ci-workflow.yml +2 -2
  2. {gitlabform-6.2.3 → gitlabform-6.3.0}/PKG-INFO +3 -3
  3. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/changelog.md +33 -0
  4. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/contrib/implementation_design.md +1 -1
  5. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/protected_branches.md +32 -0
  6. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/__init__.py +2 -1
  7. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/__init__.py +5 -6
  8. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/core.py +1 -1
  9. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/groups.py +1 -1
  10. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/python_gitlab.py +1 -1
  11. gitlabform-6.3.0/gitlabform/processors/group/group_ldap_links_processor.py +106 -0
  12. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_settings_processor.py +3 -0
  13. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/__init__.py +1 -1
  14. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/branches_processor.py +31 -11
  15. gitlabform-6.3.0/gitlabform/processors/project/protected_environments_processor.py +68 -0
  16. {gitlabform-6.2.3 → gitlabform-6.3.0}/pyproject.toml +11 -11
  17. {gitlabform-6.2.3 → gitlabform-6.3.0}/tbump.toml +1 -1
  18. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/__init__.py +2 -0
  19. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_group_ldap_links.py +23 -0
  20. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_branches.py +59 -0
  21. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_settings.py +23 -0
  22. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/gitlab/test_core.py +14 -0
  23. gitlabform-6.3.0/tests/unit/gitlab/test_groups.py +15 -0
  24. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/gitlab/test_python_gitlab.py +8 -0
  25. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_branches_processor.py +32 -0
  26. gitlabform-6.3.0/tests/unit/processors/test_group_ldap_links_processor.py +165 -0
  27. gitlabform-6.3.0/tests/unit/processors/test_protected_environments_processor.py +130 -0
  28. {gitlabform-6.2.3 → gitlabform-6.3.0}/uv.lock +416 -327
  29. gitlabform-6.2.3/gitlabform/gitlab/group_ldap_links.py +0 -40
  30. gitlabform-6.2.3/gitlabform/gitlab/project_protected_environments.py +0 -37
  31. gitlabform-6.2.3/gitlabform/processors/group/group_ldap_links_processor.py +0 -16
  32. gitlabform-6.2.3/gitlabform/processors/shared/protected_environments_processor.py +0 -20
  33. gitlabform-6.2.3/tests/unit/processors/__init__.py +0 -0
  34. {gitlabform-6.2.3 → gitlabform-6.3.0}/.coveragerc +0 -0
  35. {gitlabform-6.2.3 → gitlabform-6.3.0}/.dockerignore +0 -0
  36. {gitlabform-6.2.3 → gitlabform-6.3.0}/.git-blame-ignore-revs +0 -0
  37. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/CODEOWNERS +0 -0
  38. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  39. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  40. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/ISSUE_TEMPLATE/question-or-other-issue.md +0 -0
  41. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/actions/setup-uv-local/README.md +0 -0
  42. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/actions/setup-uv-local/action.yml +0 -0
  43. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/dependabot.yml +0 -0
  44. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/_main.yml +0 -0
  45. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/_releases.yml +0 -0
  46. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/build.yml +0 -0
  47. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/codecov-upload.yml +0 -0
  48. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/prs-entrypoint-forks.yml +0 -0
  49. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/prs-entrypoint-main.yml +0 -0
  50. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/static-analysis.yml +0 -0
  51. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/tests-acceptance.yml +0 -0
  52. {gitlabform-6.2.3 → gitlabform-6.3.0}/.github/workflows/tests-unit.yml +0 -0
  53. {gitlabform-6.2.3 → gitlabform-6.3.0}/.gitignore +0 -0
  54. {gitlabform-6.2.3 → gitlabform-6.3.0}/.lgtm.yml +0 -0
  55. {gitlabform-6.2.3 → gitlabform-6.3.0}/.mypy.ini +0 -0
  56. {gitlabform-6.2.3 → gitlabform-6.3.0}/.overrides/gitlabform-logo-favicon.png +0 -0
  57. {gitlabform-6.2.3 → gitlabform-6.3.0}/.overrides/gitlabform-logo-favicon.svg +0 -0
  58. {gitlabform-6.2.3 → gitlabform-6.3.0}/.python-version +0 -0
  59. {gitlabform-6.2.3 → gitlabform-6.3.0}/CONTRIBUTING.md +0 -0
  60. {gitlabform-6.2.3 → gitlabform-6.3.0}/Dockerfile +0 -0
  61. {gitlabform-6.2.3 → gitlabform-6.3.0}/LICENSE +0 -0
  62. {gitlabform-6.2.3 → gitlabform-6.3.0}/README.md +0 -0
  63. {gitlabform-6.2.3 → gitlabform-6.3.0}/codecov.yml +0 -0
  64. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/__init__.py +0 -0
  65. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/common.py +0 -0
  66. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/docker.py +0 -0
  67. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/docs.py +0 -0
  68. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/env.py +0 -0
  69. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/gitlab/await-healthy.sh +0 -0
  70. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/gitlab/gitlab.rb +0 -0
  71. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/gitlab/healthcheck-and-setup.sh +0 -0
  72. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/gitlab/run_gitlab_in_docker.sh +0 -0
  73. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/gitlab/tests.Dockerfile +0 -0
  74. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/infra.py +0 -0
  75. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/main.py +0 -0
  76. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/package.py +0 -0
  77. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/qa.py +0 -0
  78. {gitlabform-6.2.3 → gitlabform-6.3.0}/dev/release.py +0 -0
  79. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/automation.md +0 -0
  80. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/contrib/coding_guidelines.md +0 -0
  81. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/contrib/index.md +0 -0
  82. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/contrib/local_development.md +0 -0
  83. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/contrib/releases.md +0 -0
  84. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/contrib/workflows.md +0 -0
  85. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/features.md +0 -0
  86. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/Opencast-logo.png +0 -0
  87. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/egnyte-logo.svg +0 -0
  88. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/elastic-path-logo.svg +0 -0
  89. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/gitlabform-logo-square.png +0 -0
  90. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/gitlabform-logo.png +0 -0
  91. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/gitlabform-logo.svg +0 -0
  92. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/group-ldap-links-provider.png +0 -0
  93. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/liquidlight-logo.svg +0 -0
  94. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/roche-logo.png +0 -0
  95. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/stroeer-online-marketing-logo.svg +0 -0
  96. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/images/ti8m-logo.png +0 -0
  97. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/index.md +0 -0
  98. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/installation.md +0 -0
  99. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/archive_unarchive.md +0 -0
  100. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/avatar.md +0 -0
  101. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/badges.md +0 -0
  102. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/ci_cd_variables.md +0 -0
  103. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/deploy_keys.md +0 -0
  104. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/files.md +0 -0
  105. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/group_ldap_links.md +0 -0
  106. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/group_saml_links.md +0 -0
  107. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/index.md +0 -0
  108. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/integrations.md +0 -0
  109. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/job_token_scope.md +0 -0
  110. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/labels.md +0 -0
  111. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/members.md +0 -0
  112. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/merge_requests.md +0 -0
  113. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/pipeline_schedules.md +0 -0
  114. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/project_security_settings.md +0 -0
  115. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/project_transfer.md +0 -0
  116. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/protected_environments.md +0 -0
  117. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/push_mirrors.md +0 -0
  118. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/push_rules.md +0 -0
  119. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/resource_groups.md +0 -0
  120. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/settings.md +0 -0
  121. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/tags_protection.md +0 -0
  122. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/reference/webhooks.md +0 -0
  123. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/requirements.md +0 -0
  124. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/running.md +0 -0
  125. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/similar_apps.md +0 -0
  126. {gitlabform-6.2.3 → gitlabform-6.3.0}/docs/upgrade.md +0 -0
  127. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlab-config/config.yml +0 -0
  128. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlab-config/gitlab-config.md +0 -0
  129. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/configuration/__init__.py +0 -0
  130. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/configuration/common.py +0 -0
  131. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/configuration/core.py +0 -0
  132. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/configuration/groups.py +0 -0
  133. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/configuration/projects.py +0 -0
  134. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/configuration/transform.py +0 -0
  135. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/constants.py +0 -0
  136. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/commits.py +0 -0
  137. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/merge_requests.py +0 -0
  138. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/pipelines.py +0 -0
  139. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  140. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/projects.py +0 -0
  141. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/gitlab/variables.py +0 -0
  142. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/lists/__init__.py +0 -0
  143. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/lists/filter.py +0 -0
  144. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/lists/groups.py +0 -0
  145. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/lists/projects.py +0 -0
  146. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/output.py +0 -0
  147. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/__init__.py +0 -0
  148. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/abstract_processor.py +0 -0
  149. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/application/__init__.py +0 -0
  150. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/application/application_settings_processor.py +0 -0
  151. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/defining_keys.py +0 -0
  152. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/__init__.py +0 -0
  153. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_badges_processor.py +0 -0
  154. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_hooks_processor.py +0 -0
  155. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_labels_processor.py +0 -0
  156. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_members_processor.py +0 -0
  157. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_protected_branches_processor.py +0 -0
  158. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
  159. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
  160. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/group/group_variables_processor.py +0 -0
  161. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/multiple_entities_processor.py +0 -0
  162. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/badges_processor.py +0 -0
  163. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  164. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/files_processor.py +0 -0
  165. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/hooks_processor.py +0 -0
  166. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/integrations_processor.py +0 -0
  167. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  168. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/members_processor.py +0 -0
  169. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  170. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  171. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/project_labels_processor.py +0 -0
  172. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/project_processor.py +0 -0
  173. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  174. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/project_security_settings.py +0 -0
  175. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/project_settings_processor.py +0 -0
  176. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/project_variables_processor.py +0 -0
  177. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/remote_mirrors_processor.py +0 -0
  178. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  179. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/schedules_processor.py +0 -0
  180. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/project/tags_processor.py +0 -0
  181. {gitlabform-6.2.3/gitlabform/processors/shared → gitlabform-6.3.0/gitlabform/processors/util}/__init__.py +0 -0
  182. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/util/badges_processor.py +0 -0
  183. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/util/branch_protection.py +0 -0
  184. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/util/decorators.py +0 -0
  185. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/util/difference_logger.py +0 -0
  186. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/util/labels_processor.py +0 -0
  187. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/processors/util/variables_processor.py +0 -0
  188. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/run.py +0 -0
  189. {gitlabform-6.2.3 → gitlabform-6.3.0}/gitlabform/util.py +0 -0
  190. {gitlabform-6.2.3 → gitlabform-6.3.0}/mkdocs.yml +0 -0
  191. {gitlabform-6.2.3 → gitlabform-6.3.0}/prek.toml +0 -0
  192. {gitlabform-6.2.3/gitlabform/processors/util → gitlabform-6.3.0/tests}/__init__.py +0 -0
  193. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/conftest.py +0 -0
  194. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_branches.py +0 -0
  195. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_branches_users_case_insensitive.py +0 -0
  196. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_group_protected_branches.py +0 -0
  197. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_group_push_rules.py +0 -0
  198. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_group_saml_links.py +0 -0
  199. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_group_settings.py +0 -0
  200. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_group_variables.py +0 -0
  201. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_merge_request_approval_rules.py +0 -0
  202. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_merge_request_approvals_settings.py +0 -0
  203. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_protected_environments.py +0 -0
  204. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_push_rules.py +0 -0
  205. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/premium/test_tags.py +0 -0
  206. {gitlabform-6.2.3/tests → gitlabform-6.3.0/tests/acceptance/standard}/__init__.py +0 -0
  207. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_application_settings.py +0 -0
  208. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_archive_project.py +0 -0
  209. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_badges.py +0 -0
  210. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_deploy_keys.py +0 -0
  211. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_deploy_keys_all_projects.py +0 -0
  212. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_files.py +0 -0
  213. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_files_templates.py +0 -0
  214. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_avatar.py +0 -0
  215. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_badges.py +0 -0
  216. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_hooks.py +0 -0
  217. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_labels.py +0 -0
  218. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_members_case_insensitive.py +0 -0
  219. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_members_groups.py +0 -0
  220. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_members_users.py +0 -0
  221. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_group_variables.py +0 -0
  222. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_hooks.py +0 -0
  223. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_integrations.py +0 -0
  224. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_job_token_scope.py +0 -0
  225. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_members.py +0 -0
  226. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_members_add_group.py +0 -0
  227. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_members_enforce.py +0 -0
  228. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_project_avatar.py +0 -0
  229. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_project_group_members_case_insensitive.py +0 -0
  230. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_project_labels.py +0 -0
  231. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_project_members_case_insensitve.py +0 -0
  232. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_project_settings.py +0 -0
  233. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_project_variables.py +0 -0
  234. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_remote_mirrors.py +0 -0
  235. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_resource_groups.py +0 -0
  236. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_running.py +0 -0
  237. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_scheduled_for_deletion_project.py +0 -0
  238. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_schedules.py +0 -0
  239. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_tags.py +0 -0
  240. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_token_from_config.py +0 -0
  241. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_transfer_project.py +0 -0
  242. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/standard/test_wildcard_patterns.py +0 -0
  243. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/ultimate/test_group_members.py +0 -0
  244. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/ultimate/test_group_settings.py +0 -0
  245. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/ultimate/test_members.py +0 -0
  246. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/ultimate/test_project_security_settings.py +0 -0
  247. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/acceptance/ultimate/test_project_settings.py +0 -0
  248. {gitlabform-6.2.3/tests/acceptance/standard → gitlabform-6.3.0/tests/unit}/__init__.py +0 -0
  249. {gitlabform-6.2.3/tests/unit → gitlabform-6.3.0/tests/unit/configuration}/__init__.py +0 -0
  250. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_case_sensitivity.py +0 -0
  251. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_inheritance_break_projects_and_groups.py +0 -0
  252. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_inheritance_break_subgroups.py +0 -0
  253. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_inheritance_break_validation.py +0 -0
  254. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_projects_and_groups.py +0 -0
  255. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_skip_groups_skip_projects.py +0 -0
  256. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_subgroups.py +0 -0
  257. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_wildcard_patterns.py +0 -0
  258. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/test_yaml_version.py +0 -0
  259. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/transform/test_access_level_transformer.py +0 -0
  260. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/transform/test_group_transformer.py +0 -0
  261. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/transform/test_implicit_name_transformer.py +0 -0
  262. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/transform/test_user_and_group_transformers.py +0 -0
  263. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/configuration/transform/test_user_transformer.py +0 -0
  264. {gitlabform-6.2.3/tests/unit/configuration → gitlabform-6.3.0/tests/unit/processors}/__init__.py +0 -0
  265. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_abstract_processor.py +0 -0
  266. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_abstract_processors.py +0 -0
  267. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_deploy_keys_processor.py +0 -0
  268. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_difference_logger.py +0 -0
  269. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_files_processor.py +0 -0
  270. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_group_members_processor.py +0 -0
  271. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_group_protected_branches_processor.py +0 -0
  272. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_group_saml_links_processor.py +0 -0
  273. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_merge_requests_approval_rules_processor.py +0 -0
  274. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_schedules_processor_extended_cron_pattern.py +0 -0
  275. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/processors/test_util_badges_processor.py +0 -0
  276. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/test_access_levels.py +0 -0
  277. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/test_non_empty_configs_provider.py +0 -0
  278. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/test_output.py +0 -0
  279. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/test_parse_args.py +0 -0
  280. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/test_projects_provider.py +0 -0
  281. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/test_show_summary.py +0 -0
  282. {gitlabform-6.2.3 → gitlabform-6.3.0}/tests/unit/test_utils.py +0 -0
@@ -77,7 +77,7 @@ jobs:
77
77
  # uses: actions/setup-example@v1
78
78
 
79
79
  - name: Initialize CodeQL
80
- uses: github/codeql-action/init@v4.38.0
80
+ uses: github/codeql-action/init@v4.38.2
81
81
  with:
82
82
  languages: ${{ matrix.language }}
83
83
  build-mode: ${{ matrix.build-mode }}
@@ -89,6 +89,6 @@ jobs:
89
89
  # queries: security-extended,security-and-quality
90
90
 
91
91
  - name: Perform CodeQL Analysis
92
- uses: github/codeql-action/analyze@v4.38.0
92
+ uses: github/codeql-action/analyze@v4.38.2
93
93
  with:
94
94
  category: "/language:${{matrix.language}}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitlabform
3
- Version: 6.2.3
3
+ Version: 6.3.0
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
@@ -23,10 +23,10 @@ Requires-Dist: certifi==2026.7.22
23
23
  Requires-Dist: cli-ui==0.19.0
24
24
  Requires-Dist: jinja2==3.1.6
25
25
  Requires-Dist: luddite==1.0.4
26
- Requires-Dist: markupsafe==3.0.3
26
+ Requires-Dist: markupsafe==3.0.4
27
27
  Requires-Dist: mergedeep==1.3.4
28
28
  Requires-Dist: packaging==26.3
29
- Requires-Dist: python-gitlab[graphql]==8.5.0
29
+ Requires-Dist: python-gitlab[graphql]==8.6.0
30
30
  Requires-Dist: requests==2.34.2
31
31
  Requires-Dist: rich==15.0.0
32
32
  Requires-Dist: ruamel-yaml==0.19.1
@@ -6,6 +6,39 @@ For details on how to migrate between major versions, please refer to the [upgra
6
6
 
7
7
  ---
8
8
 
9
+ ## 6.3.0
10
+
11
+ ### Features
12
+
13
+ * show diff in dry-run mode for group settings [#1462](https://github.com/gitlabform/gitlabform/pull/1462). ([vetlekise](https://github.com/vetlekise))
14
+ * skip branch protection for non-existent branches [#1457](https://github.com/gitlabform/gitlabform/pull/1457). ([amimas](https://github.com/amimas))
15
+
16
+ ### Refactors
17
+
18
+ * migrate protected environments processor to python-gitlab [#1438](https://github.com/gitlabform/gitlabform/pull/1438). ([rickbrouwer](https://github.com/rickbrouwer))
19
+
20
+ ### Dependencies
21
+
22
+ * Update various dependencies to newer versions.
23
+
24
+ Thanks to all the contributors of this release!
25
+
26
+ ## 6.2.4
27
+
28
+ ### Performance
29
+
30
+ * skip fetching projects when querying groups [#1436](https://github.com/gitlabform/gitlabform/pull/1436). ([ryanpham-gitlab](https://github.com/ryanpham-gitlab))
31
+
32
+ ### Refactors
33
+
34
+ * migrate group ldap links processor to python-gitlab [#1428](https://github.com/gitlabform/gitlabform/pull/1428). ([rickbrouwer](https://github.com/rickbrouwer))
35
+
36
+ ### Dependencies
37
+
38
+ * Update various dependencies to newer versions.
39
+
40
+ Thanks to all the contributors of this release!
41
+
9
42
  ## 6.2.3
10
43
 
11
44
  ### Bug Fixes
@@ -100,7 +100,7 @@ Sometimes there is some logic in these methods if:
100
100
  * we only need a specific part of the response from GitLab API - see `GitLabProjects.get_all_projects()` as an example,
101
101
  * some GitLab APIs need some workarounds for their bugs or documentation inconsistencies, like:
102
102
  * some APIs declare in the docs that they accept both "group/project" string OR a project id while in fact only the latter works - see `GitLabProjects.post_approvals()` as an example,
103
- * some APIs return invalid HTTP error codes, like 404 instead of 400 - see `GitLabGroupLDAPLinks.add_ldap_group_link` as an example.
103
+ * some APIs return invalid HTTP error codes, like 404 instead of 400 - see `GroupLDAPLinksProcessor._create_link` as an example.
104
104
 
105
105
  **Note**: Some of the code here is **NOT used by the GitLabForm app**, but utilized by internal Egnyte
106
106
  applications that have not (yet) switched to the more standard `python-gitlab`.
@@ -105,6 +105,38 @@ projects_and_groups:
105
105
  allow_force_push: true
106
106
  ```
107
107
 
108
+ ### Project only option
109
+
110
+ This option is only supported for project-level branch protection under `branches`, and is a GitLabForm-specific feature.
111
+
112
+ GitLab allows creating branch protection even when a branch does not exist. This is useful so that a branch is automatically protected when it matches the configured name. If this is not desired, use a top-level key named `skip_missing_branch: true`. Setting this flag will skip branch protection for a branch when it does not exist.
113
+
114
+ This is intended for exact branch names only. It does not apply to wildcard patterns such as `release-*`.
115
+
116
+ - If `strict` mode is enabled, GitLabForm still fails on missing literal branches.
117
+ - If `strict` mode is disabled, GitLabForm skips protecting a missing literal branch and continues.
118
+
119
+ Example:
120
+
121
+ ```yaml
122
+ projects_and_groups:
123
+ group_1/project_1:
124
+ branches:
125
+ skip_missing_branch: true
126
+ main:
127
+ protected: true
128
+ push_access_level: no access
129
+ merge_access_level: maintainer
130
+ branch-that-does-not-exist-yet: # This will be skipped
131
+ protected: true
132
+ push_access_level: no access
133
+ merge_access_level: maintainer
134
+ "release-*":
135
+ protected: true
136
+ push_access_level: no access
137
+ merge_access_level: maintainer
138
+ ```
139
+
108
140
  ### Premium-only features
109
141
 
110
142
  !!! info
@@ -56,6 +56,7 @@ class GitLabForm:
56
56
  noop=False,
57
57
  output_file=None,
58
58
  recurse_subgroups=True,
59
+ strict=True,
59
60
  ):
60
61
  if target and config_string:
61
62
  # this mode is basically only for testing
@@ -64,7 +65,7 @@ class GitLabForm:
64
65
  self.config_string = config_string
65
66
  self.verbose = True
66
67
  self.debug = True
67
- self.strict = True
68
+ self.strict = strict
68
69
  self.start_from = 1
69
70
  self.start_from_group = 1
70
71
  self.noop = noop
@@ -7,12 +7,10 @@ from typing import List
7
7
  from gitlab import Gitlab as GitlabClient, GraphQL
8
8
 
9
9
  from gitlabform.gitlab.commits import GitLabCommits
10
- from gitlabform.gitlab.group_ldap_links import GitLabGroupLDAPLinks
10
+ from gitlabform.gitlab.groups import GitLabGroups
11
11
  from gitlabform.gitlab.merge_requests import GitLabMergeRequests
12
12
  from gitlabform.gitlab.pipelines import GitLabPipelines
13
- from gitlabform.gitlab.project_protected_environments import (
14
- GitLabProjectProtectedEnvironments,
15
- )
13
+ from gitlabform.gitlab.projects import GitLabProjects
16
14
  from gitlabform.gitlab.project_merge_requests_approvals import (
17
15
  GitLabProjectMergeRequestsApprovals,
18
16
  )
@@ -46,12 +44,13 @@ class AccessLevel(enum.IntEnum):
46
44
  return [level.name.lower().replace("_", " ") for level in AccessLevel]
47
45
 
48
46
 
47
+ # GitLabGroups and GitLabProjects stay as bases until gitlabform/lists/ no longer uses their methods
49
48
  class GitLab(
50
49
  GitLabCommits,
50
+ GitLabGroups,
51
51
  GitLabMergeRequests,
52
- GitLabGroupLDAPLinks,
53
52
  GitLabPipelines,
54
- GitLabProjectProtectedEnvironments,
53
+ GitLabProjects,
55
54
  GitLabProjectMergeRequestsApprovals,
56
55
  ):
57
56
  pass
@@ -154,7 +154,7 @@ class GitLabCore:
154
154
 
155
155
  @functools.lru_cache()
156
156
  def _get_group_id(self, path) -> int:
157
- group = self._make_requests_to_api("groups/%s", path, "GET")
157
+ group = self._make_requests_to_api("groups/%s?with_projects=false", path, "GET")
158
158
  return int(group["id"])
159
159
 
160
160
  @functools.lru_cache()
@@ -29,7 +29,7 @@ class GitLabGroups(GitLabCore):
29
29
  raise NotFoundException(f"Group/subgroup with path '{some_string}' not found.")
30
30
 
31
31
  def get_group(self, name):
32
- return self._make_requests_to_api("groups/%s", name)
32
+ return self._make_requests_to_api("groups/%s?with_projects=false", name)
33
33
 
34
34
  def get_group_descendants(self, group_id_or_path):
35
35
  return self._make_requests_to_api("groups/%s/descendant_groups", group_id_or_path)
@@ -84,7 +84,7 @@ class PythonGitlab(Gitlab):
84
84
 
85
85
  @functools.lru_cache()
86
86
  def get_group_by_path_cached(self, groupname: str) -> Group:
87
- group: Group = self.groups.get(groupname)
87
+ group: Group = self.groups.get(groupname, with_projects=False)
88
88
  if group:
89
89
  return group
90
90
 
@@ -0,0 +1,106 @@
1
+ from logging import info
2
+ from typing import Dict, List, Optional, Tuple
3
+
4
+ from gitlab.exceptions import GitlabCreateError, GitlabListError
5
+ from gitlab.v4.objects import Group, GroupLDAPGroupLink
6
+
7
+ from gitlabform.gitlab import GitLab
8
+ from gitlabform.processors.abstract_processor import AbstractProcessor
9
+
10
+ # A link is identified by provider + exactly one of (cn, filter).
11
+ DefiningKey = Tuple[Optional[str], Optional[str], Optional[str]]
12
+
13
+
14
+ class GroupLDAPLinksProcessor(AbstractProcessor):
15
+ def __init__(self, gitlab: GitLab):
16
+ super().__init__("group_ldap_links", gitlab)
17
+
18
+ def _process_configuration(self, group_path_and_name: str, configuration: Dict):
19
+ configured_links: Dict = configuration.get("group_ldap_links", {})
20
+ enforce = configured_links.pop("enforce", False)
21
+
22
+ group: Group = self.gl.get_group_by_path_cached(group_path_and_name)
23
+ existing_links: List[GroupLDAPGroupLink] = self._list_existing_links(group)
24
+
25
+ processed_keys: set = set()
26
+
27
+ for entity_name, link_config in configured_links.items():
28
+ key = self._defining_key_of_config(link_config)
29
+ matching = next(
30
+ (link for link in existing_links if self._defining_key_of_link(link) == key),
31
+ None,
32
+ )
33
+
34
+ if link_config.get("delete", False):
35
+ if matching:
36
+ info(f"Deleting {entity_name} of {self.configuration_name} in {group_path_and_name}")
37
+ matching.delete()
38
+ else:
39
+ info(
40
+ f"Not deleting {entity_name} of {self.configuration_name} in {group_path_and_name},"
41
+ f" because it doesn't exist"
42
+ )
43
+ processed_keys.add(key)
44
+ continue
45
+
46
+ if matching:
47
+ if self._needs_update(matching.asdict(), link_config):
48
+ # The API has no endpoint to update an LDAP link
49
+ # (https://docs.gitlab.com/api/group_ldap_links/), so we recreate it.
50
+ info(f" * Recreating {entity_name} of {self.configuration_name} in {group_path_and_name}")
51
+ matching.delete()
52
+ self._create_link(group, link_config)
53
+ else:
54
+ info(
55
+ f" * {entity_name} of {self.configuration_name} in {group_path_and_name}"
56
+ f" doesn't need an update."
57
+ )
58
+ else:
59
+ info(f" * Adding {entity_name} of {self.configuration_name} in {group_path_and_name}")
60
+ self._create_link(group, link_config)
61
+ processed_keys.add(key)
62
+
63
+ if enforce:
64
+ for existing in existing_links:
65
+ if self._defining_key_of_link(existing) not in processed_keys:
66
+ info(
67
+ f"Deleting LDAP link (provider={existing.provider},"
68
+ f" cn={getattr(existing, 'cn', None)},"
69
+ f" filter={getattr(existing, 'filter', None)})"
70
+ f" of {self.configuration_name} in {group_path_and_name}"
71
+ f" as it's not in config and enforce is set to true."
72
+ )
73
+ existing.delete()
74
+
75
+ @staticmethod
76
+ def _list_existing_links(group: Group) -> List[GroupLDAPGroupLink]:
77
+ # GitLab returns 404 (not an empty list) when a group has no LDAP links configured;
78
+ # python-gitlab surfaces that as GitlabListError.
79
+ try:
80
+ return group.ldap_group_links.list(get_all=True)
81
+ except GitlabListError as e:
82
+ if e.response_code == 404:
83
+ return []
84
+ raise
85
+
86
+ @staticmethod
87
+ def _create_link(group: Group, link_config: Dict) -> None:
88
+ try:
89
+ group.ldap_group_links.create(link_config)
90
+ except GitlabCreateError as e:
91
+ # GitLab returns 404 instead of 400 when the create parameters are invalid
92
+ # (e.g. a non-existent provider), so a 404 here does not mean a missing group.
93
+ if e.response_code == 404:
94
+ raise GitlabCreateError(
95
+ f"Invalid parameters for an LDAP group link: {link_config}",
96
+ response_code=404,
97
+ ) from e
98
+ raise
99
+
100
+ @staticmethod
101
+ def _defining_key_of_config(link_config: Dict) -> DefiningKey:
102
+ return (link_config.get("provider"), link_config.get("cn"), link_config.get("filter"))
103
+
104
+ @staticmethod
105
+ def _defining_key_of_link(link: GroupLDAPGroupLink) -> DefiningKey:
106
+ return (link.provider, getattr(link, "cn", None), getattr(link, "filter", None))
@@ -11,6 +11,9 @@ class GroupSettingsProcessor(AbstractProcessor):
11
11
  def __init__(self, gitlab: GitLab):
12
12
  super().__init__("group_settings", gitlab)
13
13
 
14
+ def _get_current_state(self, group: str) -> dict:
15
+ return self.gl.get_group_by_path_cached(group).asdict()
16
+
14
17
  def _process_configuration(self, group: str, configuration: Dict):
15
18
  configured_group_settings = configuration.get("group_settings", {})
16
19
 
@@ -36,7 +36,7 @@ from gitlabform.processors.project.resource_groups_processor import (
36
36
  from gitlabform.processors.project.schedules_processor import SchedulesProcessor
37
37
  from gitlabform.processors.project.tags_processor import TagsProcessor
38
38
  from gitlabform.processors.project.project_variables_processor import ProjectVariablesProcessor
39
- from gitlabform.processors.shared.protected_environments_processor import (
39
+ from gitlabform.processors.project.protected_environments_processor import (
40
40
  ProtectedEnvironmentsProcessor,
41
41
  )
42
42
 
@@ -39,8 +39,11 @@ class BranchesProcessor(AbstractProcessor):
39
39
  self.custom_diff_analyzers["unprotect_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
40
40
 
41
41
  def _can_proceed(self, project_or_group: str, configuration: dict):
42
- for branch in sorted(configuration["branches"]):
43
- branch_config = configuration["branches"][branch]
42
+ branches_config = dict(configuration.get("branches", {}))
43
+ branches_config.pop("skip_missing_branch", None)
44
+
45
+ for branch in sorted(branches_config):
46
+ branch_config = branches_config[branch]
44
47
  if branch_config.get("protected") is None:
45
48
  critical(f"The Protected key is mandatory in branches configuration, fix {branch} YAML config")
46
49
  sys.exit(EXIT_INVALID_INPUT)
@@ -54,36 +57,51 @@ class BranchesProcessor(AbstractProcessor):
54
57
  """
55
58
 
56
59
  project: Project = self.gl.get_project_by_path_cached(project_and_group)
60
+ branches_config: dict = dict(configuration["branches"])
61
+ skip_missing_branch_enabled: bool = branches_config.pop("skip_missing_branch", False)
57
62
 
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)
63
+ for branch in sorted(branches_config):
64
+ self.process_branch_protection(project, branch, branches_config[branch], skip_missing_branch_enabled)
62
65
 
63
- def process_branch_protection(self, project: Project, branch_name: str, branch_config: dict):
66
+ def process_branch_protection(
67
+ self, project: Project, branch_name: str, branch_configuration: dict, skip_if_missing: bool
68
+ ):
64
69
  """
65
70
  High-level logic for processing branch protection.
66
71
 
67
72
  1. Validates branch existence (unless wildcard).
68
73
  2. Handles 'protected: false' (unprotecting).
69
74
  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).
75
+ - If not currently protected: Create protection.
76
+ - If protected: Update using PATCH (EE > 15.6) or Unprotect/Reprotect (CE/Old EE).
77
+ 4. Skips protection if the branch does not exist and 'skip_if_missing' is enabled.
78
+ - This is only applicable for literal branch names, not wildcards.
72
79
  """
73
80
  protected_branch: Optional[ProjectProtectedBranch] = None
74
81
 
75
82
  # If protected branch name contains a supported wildcard do not try looking it up
76
83
  if not self.branch_name_contains_supported_wildcard(branch_name):
77
- # Check branch we are trying to protect actually exists first
84
+ # Check branch we are trying to protect actually exists first.
85
+ # When explicitly requested, skip missing literal branches instead of creating
86
+ # a protection rule for a branch that does not exist yet.
78
87
  try:
79
88
  project.branches.get(branch_name)
80
- except GitlabGetError:
89
+ except GitlabGetError as e:
90
+ if e.response_code != 404:
91
+ raise
92
+
81
93
  message = f"Branch '{branch_name}' not found when processing it to be protected/unprotected!"
82
94
  if self.strict:
83
95
  critical(message)
84
96
  sys.exit(EXIT_INVALID_INPUT)
85
97
  else:
86
98
  warning(message)
99
+ if skip_if_missing:
100
+ info(
101
+ f"Skipping branch protection for '{branch_name}' because the branch does not exist and "
102
+ "skip_missing_branch is enabled."
103
+ )
104
+ return
87
105
 
88
106
  try:
89
107
  protected_branch = project.protectedbranches.get(branch_name)
@@ -91,6 +109,8 @@ class BranchesProcessor(AbstractProcessor):
91
109
  message = f"The branch '{branch_name}' is not protected!"
92
110
  info(message)
93
111
 
112
+ branch_config: dict = self.convert_user_and_group_names_to_ids(branch_configuration)
113
+
94
114
  if branch_config.get("protected"):
95
115
  if not protected_branch:
96
116
  info(f"Creating branch protection for {branch_name}")
@@ -0,0 +1,68 @@
1
+ from logging import info
2
+ from typing import Dict, List
3
+
4
+ from gitlab.v4.objects import Project, ProjectProtectedEnvironment
5
+
6
+ from gitlabform.gitlab import GitLab
7
+ from gitlabform.processors.abstract_processor import AbstractProcessor
8
+
9
+
10
+ class ProtectedEnvironmentsProcessor(AbstractProcessor):
11
+ def __init__(self, gitlab: GitLab):
12
+ super().__init__("protected_environments", gitlab)
13
+ # deploy_access_levels is a list of dicts; compare positionally, ignoring keys
14
+ # that GitLab adds (id, description) but the config doesn't set.
15
+ self.custom_diff_analyzers["deploy_access_levels"] = self.recursive_diff_analyzer
16
+
17
+ def _process_configuration(self, project_and_group: str, configuration: Dict):
18
+ # copy, so that popping "enforce" does not mutate the shared configuration,
19
+ # which is reused when this method is retried
20
+ configured_envs: Dict = dict(configuration.get("protected_environments", {}))
21
+ enforce = configured_envs.pop("enforce", False)
22
+
23
+ project: Project = self.gl.get_project_by_path_cached(project_and_group)
24
+ existing_envs: List[ProjectProtectedEnvironment] = project.protected_environments.list(get_all=True)
25
+
26
+ processed_names: set = set()
27
+
28
+ for entity_name, env_config in configured_envs.items():
29
+ name = env_config["name"]
30
+ matching = next((env for env in existing_envs if env.name == name), None)
31
+
32
+ if env_config.get("delete", False):
33
+ if matching:
34
+ info(f"Deleting {entity_name} of {self.configuration_name} in {project_and_group}")
35
+ matching.delete()
36
+ else:
37
+ info(
38
+ f"Not deleting {entity_name} of {self.configuration_name} in {project_and_group},"
39
+ f" because it doesn't exist"
40
+ )
41
+ processed_names.add(name)
42
+ continue
43
+
44
+ if matching:
45
+ if self._needs_update(matching.asdict(), env_config):
46
+ # The API has an update endpoint (https://docs.gitlab.com/api/protected_environments/),
47
+ # but python-gitlab's ProjectProtectedEnvironmentManager doesn't support it, so we recreate.
48
+ info(f" * Recreating {entity_name} of {self.configuration_name} in {project_and_group}")
49
+ matching.delete()
50
+ project.protected_environments.create(env_config)
51
+ else:
52
+ info(
53
+ f" * {entity_name} of {self.configuration_name} in {project_and_group}"
54
+ f" doesn't need an update."
55
+ )
56
+ else:
57
+ info(f" * Adding {entity_name} of {self.configuration_name} in {project_and_group}")
58
+ project.protected_environments.create(env_config)
59
+ processed_names.add(name)
60
+
61
+ if enforce:
62
+ for existing in existing_envs:
63
+ if existing.name not in processed_names:
64
+ info(
65
+ f"Deleting protected environment '{existing.name}' of {self.configuration_name}"
66
+ f" in {project_and_group} as it's not in config and enforce is set to true."
67
+ )
68
+ existing.delete()
@@ -10,7 +10,7 @@ packages = ["gitlabform"]
10
10
 
11
11
  [project]
12
12
  name = "gitlabform"
13
- version = "6.2.3"
13
+ version = "6.3.0"
14
14
  requires-python = ">=3.12.0"
15
15
  authors = [{ name = "Greg Dubicki and Contributors" }]
16
16
  description = "🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML"
@@ -35,10 +35,10 @@ dependencies = [
35
35
  "cli-ui==0.19.0",
36
36
  "jinja2==3.1.6",
37
37
  "luddite==1.0.4",
38
- "markupsafe==3.0.3",
38
+ "markupsafe==3.0.4",
39
39
  "mergedeep==1.3.4",
40
40
  "packaging==26.3",
41
- "python-gitlab[graphql]==8.5.0",
41
+ "python-gitlab[graphql]==8.6.0",
42
42
  "requests==2.34.2",
43
43
  "rich==15.0.0",
44
44
  "ruamel.yaml==0.19.1",
@@ -53,16 +53,16 @@ dev = [
53
53
  { include-group = "lint" },
54
54
  { include-group = "docs" },
55
55
  { include-group = "release" },
56
- "commitizen==4.18.0",
57
- "prek==0.5.2",
56
+ "commitizen==4.18.1",
57
+ "prek==0.5.3",
58
58
  ]
59
59
  # Minimum set of dependencies required to execute the test suite.
60
60
  test = [
61
- "coverage==7.16.0",
61
+ "coverage==7.16.1",
62
62
  "cryptography==50.0.1",
63
63
  "pytest==9.1.1",
64
64
  "pytest-cov==7.1.0",
65
- "pytest-rerunfailures==16.6.1",
65
+ "pytest-rerunfailures==16.7",
66
66
  "xkcdpass==1.30.0",
67
67
  ]
68
68
  # Static analysis and formatting tools.
@@ -72,15 +72,15 @@ lint = [
72
72
  "codespell==2.4.3",
73
73
  "mypy==2.3.1",
74
74
  "mypy-extensions==1.1.0",
75
- "ruff==0.16.5",
76
- "types-requests==2.33.0.20260712",
75
+ "ruff==0.16.8",
76
+ "types-requests==2.33.0.20260906",
77
77
  ]
78
78
  # Documentation generation stack.
79
79
  docs = [
80
80
  "markdown==3.10.3",
81
- "zensical==0.0.58",
81
+ "zensical==0.0.62",
82
82
  "pygments==2.21.0",
83
- "pymdown-extensions==11.0.2",
83
+ "pymdown-extensions==12.0.1",
84
84
  ]
85
85
  # Tools required for building and verifying releases.
86
86
  release = [
@@ -2,7 +2,7 @@
2
2
  github_url = "https://github.com/gitlabform/gitlabform/"
3
3
 
4
4
  [version]
5
- current = "6.2.3"
5
+ current = "6.3.0"
6
6
 
7
7
  # Example of a semver regexp.
8
8
  # Make sure this matches current_version before
@@ -338,6 +338,7 @@ def run_gitlabform(
338
338
  noop: bool = False,
339
339
  output_file: Optional[str] = None,
340
340
  recurse_subgroups: bool = True,
341
+ strict: bool = True,
341
342
  ) -> None:
342
343
  """
343
344
  Runs GitLabForm with the given configuration and target.
@@ -362,5 +363,6 @@ def run_gitlabform(
362
363
  noop=noop,
363
364
  output_file=output_file,
364
365
  recurse_subgroups=recurse_subgroups,
366
+ strict=strict,
365
367
  )
366
368
  gf.run()
@@ -120,3 +120,26 @@ class TestGroupLDAPLinks:
120
120
  assert ldap_links[0].provider == "LDAP Main"
121
121
  assert ldap_links[0].cn == "devops"
122
122
  assert ldap_links[0].group_access == AccessLevel.get_value("developer")
123
+
124
+ def test__delete_ldap_links(self, gl, group):
125
+ """Test deleting an LDAP link of a group with 'delete: true'."""
126
+
127
+ # Verify group has 1 ldap link - last state of the group in previous test
128
+ assert len(group.ldap_group_links.list()) == 1
129
+
130
+ delete_ldap_link = f"""
131
+ projects_and_groups:
132
+ {group.full_path}/*:
133
+ group_ldap_links:
134
+ devops_users:
135
+ provider: LDAP Main
136
+ cn: devops
137
+ delete: true
138
+ """
139
+ run_gitlabform(delete_ldap_link, group)
140
+
141
+ # Verify that the last LDAP link was removed.
142
+ # GitLab API returns 404 if there are no LDAP links configured.
143
+ refreshed_group = gl.groups.get(group.id)
144
+ with pytest.raises(GitlabListError):
145
+ refreshed_group.ldap_group_links.list(get_all=True)