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