gitlabform 6.0.0__tar.gz → 6.1.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 (272) hide show
  1. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/ISSUE_TEMPLATE/bug_report.md +11 -0
  2. {gitlabform-6.0.0 → gitlabform-6.1.0}/PKG-INFO +2 -2
  3. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/gitlab/gitlab.rb +4 -1
  4. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/changelog.md +18 -0
  5. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/configuration/core.py +26 -0
  6. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/configuration/projects.py +25 -4
  7. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/core.py +3 -0
  8. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/lists/projects.py +43 -0
  9. gitlabform-6.1.0/gitlabform/processors/group/group_saml_links_processor.py +61 -0
  10. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/files_processor.py +13 -0
  11. {gitlabform-6.0.0 → gitlabform-6.1.0}/pyproject.toml +8 -8
  12. {gitlabform-6.0.0 → gitlabform-6.1.0}/tbump.toml +1 -1
  13. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_group_saml_links.py +32 -0
  14. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_remote_mirrors.py +85 -70
  15. gitlabform-6.1.0/tests/acceptance/standard/test_wildcard_patterns.py +68 -0
  16. gitlabform-6.1.0/tests/unit/configuration/test_wildcard_patterns.py +202 -0
  17. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/gitlab/test_core.py +71 -0
  18. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/test_files_processor.py +15 -0
  19. gitlabform-6.1.0/tests/unit/processors/test_group_saml_links_processor.py +93 -0
  20. {gitlabform-6.0.0 → gitlabform-6.1.0}/uv.lock +128 -143
  21. gitlabform-6.0.0/gitlabform/processors/group/group_saml_links_processor.py +0 -65
  22. {gitlabform-6.0.0 → gitlabform-6.1.0}/.coveragerc +0 -0
  23. {gitlabform-6.0.0 → gitlabform-6.1.0}/.dockerignore +0 -0
  24. {gitlabform-6.0.0 → gitlabform-6.1.0}/.git-blame-ignore-revs +0 -0
  25. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/CODEOWNERS +0 -0
  26. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  27. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/ISSUE_TEMPLATE/question-or-other-issue.md +0 -0
  28. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/actions/setup-uv-local/README.md +0 -0
  29. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/actions/setup-uv-local/action.yml +0 -0
  30. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/dependabot.yml +0 -0
  31. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/_main.yml +0 -0
  32. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/_releases.yml +0 -0
  33. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/build.yml +0 -0
  34. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/pr-ci-workflow.yml +0 -0
  35. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/prs-entrypoint-forks.yml +0 -0
  36. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/prs-entrypoint-main.yml +0 -0
  37. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/static-analysis.yml +0 -0
  38. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/tests-acceptance.yml +0 -0
  39. {gitlabform-6.0.0 → gitlabform-6.1.0}/.github/workflows/tests-unit.yml +0 -0
  40. {gitlabform-6.0.0 → gitlabform-6.1.0}/.gitignore +0 -0
  41. {gitlabform-6.0.0 → gitlabform-6.1.0}/.lgtm.yml +0 -0
  42. {gitlabform-6.0.0 → gitlabform-6.1.0}/.mypy.ini +0 -0
  43. {gitlabform-6.0.0 → gitlabform-6.1.0}/.overrides/gitlabform-logo-favicon.png +0 -0
  44. {gitlabform-6.0.0 → gitlabform-6.1.0}/.overrides/gitlabform-logo-favicon.svg +0 -0
  45. {gitlabform-6.0.0 → gitlabform-6.1.0}/.python-version +0 -0
  46. {gitlabform-6.0.0 → gitlabform-6.1.0}/CONTRIBUTING.md +0 -0
  47. {gitlabform-6.0.0 → gitlabform-6.1.0}/Dockerfile +0 -0
  48. {gitlabform-6.0.0 → gitlabform-6.1.0}/LICENSE +0 -0
  49. {gitlabform-6.0.0 → gitlabform-6.1.0}/README.md +0 -0
  50. {gitlabform-6.0.0 → gitlabform-6.1.0}/codecov.yml +0 -0
  51. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/__init__.py +0 -0
  52. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/common.py +0 -0
  53. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/docker.py +0 -0
  54. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/docs.py +0 -0
  55. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/env.py +0 -0
  56. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/gitlab/await-healthy.sh +0 -0
  57. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/gitlab/healthcheck-and-setup.sh +0 -0
  58. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/gitlab/run_gitlab_in_docker.sh +0 -0
  59. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/gitlab/tests.Dockerfile +0 -0
  60. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/infra.py +0 -0
  61. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/main.py +0 -0
  62. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/package.py +0 -0
  63. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/qa.py +0 -0
  64. {gitlabform-6.0.0 → gitlabform-6.1.0}/dev/release.py +0 -0
  65. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/automation.md +0 -0
  66. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/contrib/coding_guidelines.md +0 -0
  67. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/contrib/implementation_design.md +0 -0
  68. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/contrib/index.md +0 -0
  69. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/contrib/local_development.md +0 -0
  70. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/contrib/releases.md +0 -0
  71. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/contrib/workflows.md +0 -0
  72. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/features.md +0 -0
  73. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/Opencast-logo.png +0 -0
  74. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/egnyte-logo.svg +0 -0
  75. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/elastic-path-logo.svg +0 -0
  76. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/gitlabform-logo-square.png +0 -0
  77. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/gitlabform-logo.png +0 -0
  78. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/gitlabform-logo.svg +0 -0
  79. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/group-ldap-links-provider.png +0 -0
  80. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/liquidlight-logo.svg +0 -0
  81. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/roche-logo.png +0 -0
  82. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/images/stroeer-online-marketing-logo.svg +0 -0
  83. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/index.md +0 -0
  84. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/installation.md +0 -0
  85. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/archive_unarchive.md +0 -0
  86. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/avatar.md +0 -0
  87. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/badges.md +0 -0
  88. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/ci_cd_variables.md +0 -0
  89. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/deploy_keys.md +0 -0
  90. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/files.md +0 -0
  91. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/group_ldap_links.md +0 -0
  92. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/group_saml_links.md +0 -0
  93. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/index.md +0 -0
  94. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/integrations.md +0 -0
  95. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/job_token_scope.md +0 -0
  96. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/labels.md +0 -0
  97. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/members.md +0 -0
  98. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/merge_requests.md +0 -0
  99. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/pipeline_schedules.md +0 -0
  100. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/project_security_settings.md +0 -0
  101. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/project_transfer.md +0 -0
  102. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/protected_branches.md +0 -0
  103. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/protected_environments.md +0 -0
  104. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/push_mirrors.md +0 -0
  105. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/push_rules.md +0 -0
  106. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/resource_groups.md +0 -0
  107. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/settings.md +0 -0
  108. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/tags_protection.md +0 -0
  109. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/reference/webhooks.md +0 -0
  110. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/requirements.md +0 -0
  111. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/running.md +0 -0
  112. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/similar_apps.md +0 -0
  113. {gitlabform-6.0.0 → gitlabform-6.1.0}/docs/upgrade.md +0 -0
  114. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlab-config/config.yml +0 -0
  115. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlab-config/gitlab-config.md +0 -0
  116. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/__init__.py +0 -0
  117. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/configuration/__init__.py +0 -0
  118. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/configuration/common.py +0 -0
  119. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/configuration/groups.py +0 -0
  120. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/configuration/transform.py +0 -0
  121. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/constants.py +0 -0
  122. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/__init__.py +0 -0
  123. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/commits.py +0 -0
  124. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/group_badges.py +0 -0
  125. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/group_ldap_links.py +0 -0
  126. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/groups.py +0 -0
  127. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/merge_requests.py +0 -0
  128. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/pipelines.py +0 -0
  129. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/project_badges.py +0 -0
  130. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/project_deploy_keys.py +0 -0
  131. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  132. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/project_protected_environments.py +0 -0
  133. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/projects.py +0 -0
  134. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/python_gitlab.py +0 -0
  135. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/gitlab/variables.py +0 -0
  136. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/lists/__init__.py +0 -0
  137. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/lists/filter.py +0 -0
  138. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/lists/groups.py +0 -0
  139. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/output.py +0 -0
  140. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/__init__.py +0 -0
  141. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/abstract_processor.py +0 -0
  142. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/application/__init__.py +0 -0
  143. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/application/application_settings_processor.py +0 -0
  144. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/defining_keys.py +0 -0
  145. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/__init__.py +0 -0
  146. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_badges_processor.py +0 -0
  147. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_hooks_processor.py +0 -0
  148. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_labels_processor.py +0 -0
  149. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
  150. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_members_processor.py +0 -0
  151. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_protected_branches_processor.py +0 -0
  152. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
  153. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_settings_processor.py +0 -0
  154. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/group/group_variables_processor.py +0 -0
  155. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/multiple_entities_processor.py +0 -0
  156. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/__init__.py +0 -0
  157. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/badges_processor.py +0 -0
  158. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/branches_processor.py +0 -0
  159. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  160. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/hooks_processor.py +0 -0
  161. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/integrations_processor.py +0 -0
  162. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  163. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/members_processor.py +0 -0
  164. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  165. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  166. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/project_labels_processor.py +0 -0
  167. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/project_processor.py +0 -0
  168. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  169. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/project_security_settings.py +0 -0
  170. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/project_settings_processor.py +0 -0
  171. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/project_variables_processor.py +0 -0
  172. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/remote_mirrors_processor.py +0 -0
  173. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  174. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/schedules_processor.py +0 -0
  175. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/project/tags_processor.py +0 -0
  176. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/shared/__init__.py +0 -0
  177. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
  178. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/util/__init__.py +0 -0
  179. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/util/branch_protection.py +0 -0
  180. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/util/decorators.py +0 -0
  181. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/util/difference_logger.py +0 -0
  182. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/util/labels_processor.py +0 -0
  183. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/processors/util/variables_processor.py +0 -0
  184. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/run.py +0 -0
  185. {gitlabform-6.0.0 → gitlabform-6.1.0}/gitlabform/util.py +0 -0
  186. {gitlabform-6.0.0 → gitlabform-6.1.0}/mkdocs.yml +0 -0
  187. {gitlabform-6.0.0 → gitlabform-6.1.0}/prek.toml +0 -0
  188. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/__init__.py +0 -0
  189. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/__init__.py +0 -0
  190. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/conftest.py +0 -0
  191. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_branches.py +0 -0
  192. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_branches_users_case_insensitive.py +0 -0
  193. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_group_ldap_links.py +0 -0
  194. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_group_protected_branches.py +0 -0
  195. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_group_push_rules.py +0 -0
  196. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_group_settings.py +0 -0
  197. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_group_variables.py +0 -0
  198. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_merge_request_approval_rules.py +0 -0
  199. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_merge_request_approvals_settings.py +0 -0
  200. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_protected_environments.py +0 -0
  201. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_push_rules.py +0 -0
  202. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/premium/test_tags.py +0 -0
  203. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/__init__.py +0 -0
  204. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_application_settings.py +0 -0
  205. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_archive_project.py +0 -0
  206. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_badges.py +0 -0
  207. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_branches.py +0 -0
  208. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_deploy_keys.py +0 -0
  209. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_deploy_keys_all_projects.py +0 -0
  210. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_files.py +0 -0
  211. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_files_templates.py +0 -0
  212. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_avatar.py +0 -0
  213. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_badges.py +0 -0
  214. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_hooks.py +0 -0
  215. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_labels.py +0 -0
  216. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_members_case_insensitive.py +0 -0
  217. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_members_groups.py +0 -0
  218. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_members_users.py +0 -0
  219. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_settings.py +0 -0
  220. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_group_variables.py +0 -0
  221. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_hooks.py +0 -0
  222. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_integrations.py +0 -0
  223. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_job_token_scope.py +0 -0
  224. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_members.py +0 -0
  225. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_members_add_group.py +0 -0
  226. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_members_enforce.py +0 -0
  227. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_project_avatar.py +0 -0
  228. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_project_group_members_case_insensitive.py +0 -0
  229. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_project_labels.py +0 -0
  230. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_project_members_case_insensitve.py +0 -0
  231. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_project_settings.py +0 -0
  232. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_project_variables.py +0 -0
  233. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_resource_groups.py +0 -0
  234. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_running.py +0 -0
  235. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_scheduled_for_deletion_project.py +0 -0
  236. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_schedules.py +0 -0
  237. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_tags.py +0 -0
  238. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_token_from_config.py +0 -0
  239. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/standard/test_transfer_project.py +0 -0
  240. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/ultimate/test_group_members.py +0 -0
  241. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/ultimate/test_group_settings.py +0 -0
  242. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/ultimate/test_members.py +0 -0
  243. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/ultimate/test_project_security_settings.py +0 -0
  244. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/acceptance/ultimate/test_project_settings.py +0 -0
  245. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/__init__.py +0 -0
  246. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/__init__.py +0 -0
  247. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_case_sensitivity.py +0 -0
  248. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_inheritance_break_projects_and_groups.py +0 -0
  249. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_inheritance_break_subgroups.py +0 -0
  250. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_inheritance_break_validation.py +0 -0
  251. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_projects_and_groups.py +0 -0
  252. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_skip_groups_skip_projects.py +0 -0
  253. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_subgroups.py +0 -0
  254. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/test_yaml_version.py +0 -0
  255. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/transform/test_access_level_transformer.py +0 -0
  256. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/transform/test_group_transformer.py +0 -0
  257. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/transform/test_implicit_name_transformer.py +0 -0
  258. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/transform/test_user_and_group_transformers.py +0 -0
  259. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/configuration/transform/test_user_transformer.py +0 -0
  260. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/gitlab/test_python_gitlab.py +0 -0
  261. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/__init__.py +0 -0
  262. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/test_abstract_processor.py +0 -0
  263. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/test_abstract_processors.py +0 -0
  264. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/test_branches_processor.py +0 -0
  265. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/test_difference_logger.py +0 -0
  266. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/test_group_protected_branches_processor.py +0 -0
  267. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/processors/test_schedules_processor_extended_cron_pattern.py +0 -0
  268. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/test_access_levels.py +0 -0
  269. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/test_non_empty_configs_provider.py +0 -0
  270. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/test_parse_args.py +0 -0
  271. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/test_projects_provider.py +0 -0
  272. {gitlabform-6.0.0 → gitlabform-6.1.0}/tests/unit/test_utils.py +0 -0
@@ -18,3 +18,14 @@ Output of `gitlabform -V`
18
18
  **GitLab version**
19
19
 
20
20
  F.e. `13.12.4-ee`
21
+
22
+ **GitLabForm configuration**
23
+
24
+ The relevant part of your gitlabform config (optionally anonymized).
25
+ If applicable, share both the before and after config.
26
+
27
+ **Would you be open to contributing a fix?**
28
+
29
+ - [ ] Yes
30
+ - [ ] No
31
+ - [ ] Maybe
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlabform
3
- Version: 6.0.0
3
+ Version: 6.1.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
@@ -19,7 +19,7 @@ Classifier: Operating System :: POSIX :: Linux
19
19
  Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Topic :: Software Development :: Version Control :: Git
21
21
  Requires-Python: >=3.12.0
22
- Requires-Dist: certifi==2026.5.20
22
+ Requires-Dist: certifi==2026.6.17
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
@@ -1,4 +1,7 @@
1
1
  gitlab_rails['initial_root_password']='mK9JnG7jwYdFcBNoQ3W3'
2
+ # Bind nginx on IPv4 and IPv6 loopback. Without IPv6, Sidekiq's mirror sync
3
+ # fails intermittently with ECONNREFUSED when libcurl resolves localhost to ::1.
4
+ nginx['listen_addresses'] = ['0.0.0.0', '[::]']
2
5
  registry['enable']=false
3
6
  prometheus_monitoring['enable']=false
4
7
  gitlab_rails['initial_license_file']='/etc/gitlab/Gitlab.gitlab-license'
@@ -38,4 +41,4 @@ gitlab_rails['omniauth_providers'] = [
38
41
  },
39
42
  label: 'SAML Login'
40
43
  }
41
- ]
44
+ ]
@@ -6,6 +6,24 @@ For details on how to migrate between major versions, please refer to the [upgra
6
6
 
7
7
  ---
8
8
 
9
+ ## 6.1.0
10
+
11
+ ### Features
12
+
13
+ * Add wildcard pattern support for project configuration [#1311](https://github.com/gitlabform/gitlabform/pull/1311). ([nikaro](https://github.com/nikaro))
14
+
15
+ ### Bug Fixes
16
+
17
+ * group_saml_links: stop delete-and-recreate churn on unchanged links [#1331](https://github.com/gitlabform/gitlabform/pull/1331). ([rickbrouwer](https://github.com/rickbrouwer))
18
+ * validate key in files config to avoid cryptic KeyError [#1317](https://github.com/gitlabform/gitlabform/pull/1317), resolves [#1312](https://github.com/gitlabform/gitlabform/issues/1312). ([rickbrouwer](https://github.com/rickbrouwer))
19
+ * Normalize GitLab URL while parsing configuration [#1338](https://github.com/gitlabform/gitlabform/pull/1338). ([schurzi](https://github.com/schurzi))
20
+
21
+ ### Dependencies
22
+
23
+ * Update various dependencies to newer versions.
24
+
25
+ Thanks to all the contributors of this release!
26
+
9
27
  ## 6.0.0
10
28
 
11
29
  ### Breaking Changes
@@ -1,3 +1,4 @@
1
+ import fnmatch
1
2
  import sys
2
3
  from typing import Any
3
4
 
@@ -211,6 +212,26 @@ class ConfigurationCore(ABC):
211
212
 
212
213
  return dict(merged_dict)
213
214
 
215
+ @staticmethod
216
+ def _get_key_type(a_key: str) -> str:
217
+ """
218
+ Classify a config key used under 'projects_and_groups'.
219
+ """
220
+ if a_key == "*":
221
+ return "common"
222
+ if a_key.endswith("/*"):
223
+ return "group"
224
+ if "*" in a_key:
225
+ return "project_pattern"
226
+ return "project"
227
+
228
+ @staticmethod
229
+ def _match_pattern(pattern: str, item: str) -> bool:
230
+ """
231
+ Case-insensitive fnmatch between pattern and item.
232
+ """
233
+ return fnmatch.fnmatchcase(item.lower(), pattern.lower())
234
+
214
235
  @staticmethod
215
236
  def _get_case_insensitively(a_dict: dict, a_key: str):
216
237
  for dict_key in a_dict.keys():
@@ -231,6 +252,7 @@ class ConfigurationCore(ABC):
231
252
  if list_element == item:
232
253
  return True
233
254
 
255
+ # Support for traditional group/* skip pattern
234
256
  if (
235
257
  list_element.endswith("/*")
236
258
  and item.startswith(list_element[:-2])
@@ -238,6 +260,10 @@ class ConfigurationCore(ABC):
238
260
  ):
239
261
  return True
240
262
 
263
+ # Support for partial wildcard patterns like group/foo-*
264
+ if "*" in list_element and fnmatch.fnmatchcase(item, list_element):
265
+ return True
266
+
241
267
  return False
242
268
 
243
269
  def _find_almost_duplicates(self):
@@ -15,14 +15,14 @@ class ConfigurationProjects(ConfigurationGroups, ABC):
15
15
  depends on the groups (and common) configuration.
16
16
  """
17
17
 
18
- def get_projects(self) -> list:
18
+ def get_projects(self, key_type: str = "project") -> list:
19
19
  """
20
20
  :return: sorted list of projects names, that are EXPLICITLY defined in the config
21
21
  """
22
22
  projects = []
23
23
  projects_and_groups = self.get("projects_and_groups")
24
24
  for element in projects_and_groups.keys():
25
- if element != "*" and not element.endswith("/*"):
25
+ if self._get_key_type(element) == key_type:
26
26
  projects.append(element)
27
27
  return sorted(projects)
28
28
 
@@ -68,6 +68,27 @@ class ConfigurationProjects(ConfigurationGroups, ABC):
68
68
  """
69
69
  :param group_and_project: 'group/project'
70
70
  :return: configuration for this project or empty dict if not defined,
71
- ignoring the case
71
+ ignoring the case. Also checks for matching project patterns.
72
72
  """
73
- return self._get_case_insensitively(self.get("projects_and_groups"), group_and_project)
73
+ projects_and_groups = self.get("projects_and_groups")
74
+
75
+ # 1. Exact match
76
+ exact = self._get_case_insensitively(projects_and_groups, group_and_project)
77
+ if exact:
78
+ return exact
79
+
80
+ # 2. Best matching pattern
81
+ matches = []
82
+ for key in projects_and_groups.keys():
83
+ if self._get_key_type(key) == "project_pattern":
84
+ if self._match_pattern(key, group_and_project):
85
+ # store a 3-tuple for later sorting
86
+ prefix = key.split("*", 1)[0]
87
+ matches.append((len(prefix), -key.count("*"), key))
88
+ if matches:
89
+ # longest literal prefix wins; tie -> fewer wildcards; still tie -> first appearance
90
+ matches.sort(reverse=True)
91
+ best_key = matches[0][2]
92
+ return self._get_case_insensitively(projects_and_groups, best_key)
93
+
94
+ return {}
@@ -37,6 +37,9 @@ class GitLabCore:
37
37
  gitlab_config_from_file = self.configuration.get("gitlab", {})
38
38
  self.gitlab_config = {**default_gitlab_config, **gitlab_config_from_file}
39
39
 
40
+ if self.gitlab_config["url"]:
41
+ self.gitlab_config["url"] = self.gitlab_config["url"].rstrip("/")
42
+
40
43
  self.session = requests.Session()
41
44
 
42
45
  retries_status_forcelist = []
@@ -96,6 +96,13 @@ class ProjectsProvider(GroupsProvider):
96
96
  # defined in the configuration, but we don't need to re-check for
97
97
  # being archived or scheduled for deletion projects that we already got from groups
98
98
 
99
+ for resolved_project in self._resolve_project_patterns(groups.get_effective()):
100
+ if (
101
+ resolved_project not in projects.requested
102
+ and resolved_project not in projects_from_configuration_not_from_groups
103
+ ):
104
+ projects_from_configuration_not_from_groups.append(resolved_project)
105
+
99
106
  (
100
107
  archived_projects_from_configuration_not_from_groups,
101
108
  scheduled_for_deletion_projects_from_configuration_not_from_groups,
@@ -198,6 +205,42 @@ class ProjectsProvider(GroupsProvider):
198
205
 
199
206
  return skipped
200
207
 
208
+ def _resolve_project_patterns(self, effective_groups: list) -> list:
209
+ """
210
+ Resolve project patterns from config to concrete project paths.
211
+ Only resolves patterns whose parent group is not already in the effective groups list
212
+ to avoid redundant API calls.
213
+ """
214
+ pattern_projects = []
215
+ for pattern in self.configuration.get_projects("project_pattern"):
216
+ pattern_group = pattern.rsplit("/", 1)[0]
217
+ if pattern_group in effective_groups:
218
+ # projects from this group already fetched through normal means
219
+ continue
220
+
221
+ try:
222
+ maybe_group = self.gitlab.get_group_case_insensitive(pattern_group)
223
+ group_path = maybe_group["full_path"]
224
+ except NotFoundException:
225
+ debug(
226
+ "Group '%s' for pattern '%s' not found in GitLab, skipping",
227
+ pattern_group,
228
+ pattern,
229
+ )
230
+ continue
231
+
232
+ try:
233
+ all_project_objects = self.gitlab.get_projects(group_path, include_archived=True, only_names=False)
234
+ except NotFoundException:
235
+ all_project_objects = []
236
+
237
+ for project_object in all_project_objects:
238
+ path = project_object["path_with_namespace"]
239
+ if self.configuration._match_pattern(pattern, path) and path not in pattern_projects:
240
+ pattern_projects.append(path)
241
+
242
+ return pattern_projects
243
+
201
244
  def _get_project_transfer_source_from_config(self, project: str) -> Optional[str]:
202
245
  try:
203
246
  debug(
@@ -0,0 +1,61 @@
1
+ from logging import debug
2
+ from typing import List
3
+
4
+ from gitlabform.gitlab import GitLab
5
+ from gitlab.v4.objects import Group, GroupSAMLGroupLink
6
+ from gitlabform.processors.abstract_processor import AbstractProcessor
7
+
8
+
9
+ class GroupSAMLLinksProcessor(AbstractProcessor):
10
+
11
+ def __init__(self, gitlab: GitLab):
12
+ super().__init__("group_saml_links", gitlab)
13
+
14
+ def _process_configuration(self, group_path_and_name: str, configuration: dict) -> None:
15
+ """Process the SAML links configuration for a group."""
16
+
17
+ configured_section = configuration.get("group_saml_links", {}) or {}
18
+ enforce_links = configuration.get("group_saml_links|enforce", False)
19
+
20
+ configured_links = {k: v for k, v in configured_section.items() if k != "enforce"}
21
+
22
+ group: Group = self.gl.get_group_by_path_cached(group_path_and_name)
23
+ existing_links: List[GroupSAMLGroupLink] = group.saml_group_links.list(get_all=True)
24
+ existing_by_name = {link.name: link for link in existing_links}
25
+
26
+ for _, link_configuration in configured_links.items():
27
+ saml_group_name = link_configuration.get("saml_group_name")
28
+ existing_link = existing_by_name.get(saml_group_name)
29
+
30
+ if existing_link is None:
31
+ group.saml_group_links.create(link_configuration)
32
+ continue
33
+
34
+ # GitLab API's request and response attributes differs when SAML link is created vs retrieved.
35
+ # On creation or retrieving a link, attribute is called `saml_group_name` but and returned response is 'name' instead.
36
+ # To compare existing link (retrieved from GitLab) with configured link, link configuration need to replace `saml_group_name` with `name`
37
+ expected = {**link_configuration, "name": saml_group_name}
38
+ expected.pop("saml_group_name", None)
39
+
40
+ if self._needs_update(existing_link.asdict(), expected):
41
+ # GitLab API has no update endpoint for SAML links; delete and recreate instead.
42
+ debug(f"Updating SAML link: {saml_group_name} with {link_configuration}")
43
+ existing_link.delete()
44
+ group.saml_group_links.create(link_configuration)
45
+
46
+ if enforce_links:
47
+ self._delete_extra_links(group, existing_links, configured_links)
48
+
49
+ def _delete_extra_links(
50
+ self,
51
+ group: Group,
52
+ existing: List[GroupSAMLGroupLink],
53
+ configured: dict,
54
+ ) -> None:
55
+ """Delete any SAML links that are not in the configuration."""
56
+ known_names = {link["saml_group_name"] for link in configured.values()}
57
+
58
+ for link in existing:
59
+ if link.name not in known_names:
60
+ debug(f"Deleting extra SAML link: {link.name}")
61
+ group.saml_group_links.delete(link.name)
@@ -24,6 +24,19 @@ class FilesProcessor(AbstractProcessor):
24
24
  self.strict = strict
25
25
  self.branch_processor = BranchesProcessor(gitlab, strict)
26
26
 
27
+ def _can_proceed(self, project_or_group: str, configuration: dict):
28
+ for file in sorted(configuration["files"]):
29
+ file_config = configuration["files"][file]
30
+ if "branches" not in file_config:
31
+ critical(
32
+ f"File '{file}' is missing the required 'branches' key."
33
+ f" Specify which branches to apply the file to using one of:"
34
+ f" 'all', 'protected', or a list of branch names (e.g. ['main'])."
35
+ )
36
+ sys.exit(EXIT_INVALID_INPUT)
37
+
38
+ return True
39
+
27
40
  def _process_configuration(self, project_and_group: str, configuration: dict):
28
41
  for file in sorted(configuration["files"]):
29
42
  project: Project = self.gl.get_project_by_path_cached(project_and_group)
@@ -10,7 +10,7 @@ packages = ["gitlabform"]
10
10
 
11
11
  [project]
12
12
  name = "gitlabform"
13
- version = "6.0.0"
13
+ version = "6.1.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"
@@ -31,7 +31,7 @@ classifiers = [
31
31
 
32
32
  # Main runtime dependencies. Pinned strictly to ensure production stability.
33
33
  dependencies = [
34
- "certifi==2026.5.20",
34
+ "certifi==2026.6.17",
35
35
  "cli-ui==0.19.0",
36
36
  "jinja2==3.1.6",
37
37
  "luddite==1.0.4",
@@ -53,14 +53,14 @@ dev = [
53
53
  { include-group = "lint" },
54
54
  { include-group = "docs" },
55
55
  { include-group = "release" },
56
- "commitizen==4.16.3",
57
- "prek==0.4.4",
56
+ "commitizen==4.16.4",
57
+ "prek==0.4.5",
58
58
  ]
59
59
  # Minimum set of dependencies required to execute the test suite.
60
60
  test = [
61
- "coverage==7.14.1",
61
+ "coverage==7.14.3",
62
62
  "cryptography==49.0.0",
63
- "pytest==9.1.0",
63
+ "pytest==9.1.1",
64
64
  "pytest-cov==7.1.0",
65
65
  "pytest-rerunfailures==16.3",
66
66
  "xkcdpass==1.30.0",
@@ -72,7 +72,7 @@ lint = [
72
72
  "codespell==2.4.2",
73
73
  "mypy==2.1.0",
74
74
  "mypy-extensions==1.1.0",
75
- "ruff==0.15.17",
75
+ "ruff==0.15.20",
76
76
  "types-requests==2.33.0.20260518",
77
77
  ]
78
78
  # Documentation generation stack.
@@ -82,7 +82,7 @@ docs = [
82
82
  "mkdocs-material==9.7.6",
83
83
  "mkdocs-material-extensions==1.3.1",
84
84
  "pygments==2.20.0",
85
- "pymdown-extensions==10.21.3",
85
+ "pymdown-extensions==11.0",
86
86
  ]
87
87
  # Tools required for building and verifying releases.
88
88
  release = [
@@ -2,7 +2,7 @@
2
2
  github_url = "https://github.com/gitlabform/gitlabform/"
3
3
 
4
4
  [version]
5
- current = "6.0.0"
5
+ current = "6.1.0"
6
6
 
7
7
  # Example of a semver regexp.
8
8
  # Make sure this matches current_version before
@@ -1,3 +1,5 @@
1
+ import logging
2
+
1
3
  import pytest
2
4
 
3
5
  from tests.acceptance import run_gitlabform
@@ -46,6 +48,36 @@ class TestGroupSamlLinks:
46
48
  assert saml_links[0].name == "devops_users"
47
49
  assert saml_links[0].access_level == AccessLevel.get_value("developer")
48
50
 
51
+ def test__no_churn_when_config_unchanged(self, gl, group_for_function, caplog):
52
+ assert len(group_for_function.saml_group_links.list()) == 0, "group_saml_links is not empty"
53
+
54
+ config = f"""
55
+ projects_and_groups:
56
+ {group_for_function.full_path}/*:
57
+ group_saml_links:
58
+ devops_users:
59
+ saml_group_name: devops_users
60
+ access_level: maintainer
61
+ """
62
+
63
+ # First run creates the link.
64
+ run_gitlabform(config, group_for_function)
65
+ assert len(gl.groups.get(group_for_function.id).saml_group_links.list(get_all=True)) == 1
66
+
67
+ # Second run with the same config must not trigger a delete + recreate.
68
+ caplog.clear()
69
+ with caplog.at_level(logging.DEBUG):
70
+ run_gitlabform(config, group_for_function)
71
+
72
+ assert (
73
+ "Updating SAML link" not in caplog.text
74
+ ), f"Expected no SAML link updates on second run, got: {caplog.text}"
75
+
76
+ saml_links = gl.groups.get(group_for_function.id).saml_group_links.list(get_all=True)
77
+ assert len(saml_links) == 1
78
+ assert saml_links[0].name == "devops_users"
79
+ assert saml_links[0].access_level == AccessLevel.get_value("maintainer")
80
+
49
81
  def test__enforce_saml_links(self, gl, group_for_function):
50
82
 
51
83
  assert len(group_for_function.saml_group_links.list()) == 0, "group_saml_links is not empty"
@@ -3,9 +3,9 @@ import os
3
3
  import pytest
4
4
  import time
5
5
  from typing import cast, Tuple, Generator
6
- from gitlab.v4.objects import Project, ProjectRemoteMirror, ProjectBranch, Group
6
+ from gitlab.v4.objects import Project, ProjectRemoteMirror, Group
7
7
 
8
- from tests.acceptance import run_gitlabform, get_random_name, create_project
8
+ from tests.acceptance import run_gitlabform, get_random_name, create_project, gl
9
9
 
10
10
 
11
11
  @pytest.fixture(scope="class")
@@ -349,86 +349,101 @@ class TestRemoteMirrorsProcessor:
349
349
 
350
350
  def test_remote_mirror_http_password_auth_sync(
351
351
  self,
352
- project: Project,
353
- mirror_urls: Tuple[str, str, str],
354
- mirror_target_projects: Tuple[Project, Project, Project],
352
+ project_for_function: Project,
353
+ other_group: Group,
354
+ root_username: str,
355
+ root_access_token: str,
355
356
  ) -> None:
356
357
  """
357
358
  Test remote mirror functionality for http and password based authentication.
358
359
  This also tests configuration of `force_push` config key.
359
360
 
360
- This is validated by creating a new branch and a test file in the source project.
361
- Then run gitlabform with `force_push` enabled for the target mirror.
362
- Then check the target mirror repo if the new branch and the test file exists.
361
+ Uses a function-scoped source and a dedicated empty target (no initial commit, so no
362
+ default-branch auto-protection race) to keep this test self-contained and free of the
363
+ cross-test state that previously made it flaky.
363
364
  """
364
- first_mirror_url_http_password_auth, _, _ = mirror_urls
365
- first_mirror_repo, _, _ = mirror_target_projects
366
-
367
- # In GitLab, default setting for branch protection is to deny force push.
368
- # For first mirror, we need to allow force push to the main branch, so that
369
- # we can validate the mirror functionality.
370
- first_mirror_repo_main_branch = cast(ProjectBranch, first_mirror_repo.protectedbranches.get("main"))
371
- first_mirror_repo_main_branch.allow_force_push = True
372
- first_mirror_repo_main_branch.save()
373
-
374
- # Create a test file in a new branch in the source project to validate mirror functionality
375
- # We use a unique branch name to ensure we are testing a fresh sync
376
- sync_branch_name = get_random_name("sync_test_branch")
377
- project.branches.create({"branch": sync_branch_name, "ref": "main"})
365
+ # Empty target: no commits, no main branch yet, no protected-branch race. The mirror's
366
+ # first push creates main fresh from the source.
367
+ target = cast(
368
+ Project,
369
+ gl.projects.create(
370
+ {
371
+ "name": get_random_name("mirror_sync_target"),
372
+ "namespace_id": other_group.id,
373
+ "default_branch": "main",
374
+ "initialize_with_readme": False,
375
+ }
376
+ ),
377
+ )
378
+ try:
379
+ gitlab_url = os.getenv("GITLAB_URL", "http://localhost").rstrip("/")
380
+ mirror_url = (
381
+ gitlab_url.replace("http://", f"http://{root_username}:{root_access_token}@")
382
+ + f"/{target.path_with_namespace}.git"
383
+ )
378
384
 
379
- test_file_content = "This is a test file for remote mirror validation"
380
- test_file_path = f"mirror_test_{sync_branch_name}.txt"
385
+ sync_branch_name = get_random_name("sync_test_branch")
386
+ project_for_function.branches.create({"branch": sync_branch_name, "ref": "main"})
381
387
 
382
- project.files.create(
383
- {
384
- "branch": sync_branch_name,
385
- "file_path": test_file_path,
386
- "content": test_file_content,
387
- "commit_message": "Add test file for mirror validation",
388
- }
389
- )
388
+ test_file_content = "This is a test file for remote mirror validation"
389
+ test_file_path = f"mirror_test_{sync_branch_name}.txt"
390
390
 
391
- # Validate that the test file exists in the source project
392
- source_file = project.files.get(ref=sync_branch_name, file_path=test_file_path)
393
- assert source_file is not None
394
- assert source_file.decode().decode("utf-8") == test_file_content
391
+ project_for_function.files.create(
392
+ {
393
+ "branch": sync_branch_name,
394
+ "file_path": test_file_path,
395
+ "content": test_file_content,
396
+ "commit_message": "Add test file for mirror validation",
397
+ }
398
+ )
395
399
 
396
- # Run GitLabForm with force_push enabled.
397
- # This should trigger the self._sync_remote_mirror(project, mirror_in_gitlab) logic.
398
- gitlabform_config = f"""
399
- projects_and_groups:
400
- {project.path_with_namespace}:
401
- remote_mirrors:
402
- {first_mirror_url_http_password_auth}:
403
- enabled: true
404
- auth_method: password
405
- force_push: true
406
- """
400
+ gitlabform_config = f"""
401
+ projects_and_groups:
402
+ {project_for_function.path_with_namespace}:
403
+ remote_mirrors:
404
+ {mirror_url}:
405
+ enabled: true
406
+ auth_method: password
407
+ force_push: true
408
+ """
407
409
 
408
- run_gitlabform(gitlabform_config, project.path_with_namespace)
410
+ run_gitlabform(gitlabform_config, project_for_function.path_with_namespace)
409
411
 
410
- # Validate mirror functionality: wait for sync and verify test file appears in target.
411
- # Even with force_push: true, GitLab's Sidekiq worker may take a few seconds to
412
- # complete the physical git push to the remote.
413
- found = False
414
- max_retries = 10
415
- retry_interval = 5
416
-
417
- for i in range(max_retries):
418
- try:
419
- # Attempt to retrieve the file from the target repository
420
- target_file = first_mirror_repo.files.get(ref=sync_branch_name, file_path=test_file_path)
421
- if target_file.decode().decode("utf-8") == test_file_content:
422
- found = True
423
- break
424
- except Exception:
425
- # If 404/Exception, wait and try again
426
- time.sleep(retry_interval)
427
-
428
- assert found, (
429
- f"Mirror sync failed: File '{test_file_path}' not found in target project "
430
- f"'{first_mirror_repo.path_with_namespace}' after {max_retries * retry_interval} seconds."
431
- )
412
+ max_wait_seconds = 60
413
+ poll_interval = 3
414
+ deadline = time.time() + max_wait_seconds
415
+ last_status: str | None = None
416
+ last_error: str | None = None
417
+ synced = False
418
+
419
+ while time.time() < deadline:
420
+ mirror = self._get_mirror_from_url(project_for_function, mirror_url)
421
+ assert mirror is not None, "Mirror disappeared from source project during sync"
422
+ last_status = getattr(mirror, "update_status", None)
423
+ last_error = getattr(mirror, "last_error", None)
424
+
425
+ if last_status == "failed":
426
+ pytest.fail(f"Mirror sync reported failed by GitLab. last_error={last_error!r}")
427
+
428
+ if last_status == "finished":
429
+ try:
430
+ target_file = target.files.get(ref=sync_branch_name, file_path=test_file_path)
431
+ if target_file.decode().decode("utf-8") == test_file_content:
432
+ synced = True
433
+ break
434
+ except Exception:
435
+ pass
436
+
437
+ time.sleep(poll_interval)
438
+
439
+ assert synced, (
440
+ f"Mirror sync did not complete within {max_wait_seconds}s: "
441
+ f"file '{test_file_path}' not found in target project "
442
+ f"'{target.path_with_namespace}' "
443
+ f"(last update_status={last_status!r}, last_error={last_error!r})."
444
+ )
445
+ finally:
446
+ target.delete()
432
447
 
433
448
  def test_remote_mirrors_sync_error(
434
449
  self,