gitlabform 6.2.4__tar.gz → 6.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gitlabform-6.2.4 → gitlabform-6.3.1}/PKG-INFO +4 -4
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/changelog.md +33 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/contrib/implementation_design.md +2 -2
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/protected_branches.md +32 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/__init__.py +2 -1
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/__init__.py +3 -8
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/core.py +0 -11
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_settings_processor.py +3 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/__init__.py +1 -1
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/branches_processor.py +31 -11
- gitlabform-6.3.1/gitlabform/processors/project/merge_requests_approval_rules.py +158 -0
- gitlabform-6.3.1/gitlabform/processors/project/protected_environments_processor.py +68 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/util.py +3 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/pyproject.toml +13 -13
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tbump.toml +1 -1
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/__init__.py +2 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_branches.py +59 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_settings.py +23 -0
- gitlabform-6.3.1/tests/unit/configuration/transform/test_convert_to_simple_types.py +19 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_branches_processor.py +32 -0
- gitlabform-6.3.1/tests/unit/processors/test_merge_requests_approval_rules_processor.py +364 -0
- gitlabform-6.3.1/tests/unit/processors/test_protected_environments_processor.py +130 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/uv.lock +441 -351
- gitlabform-6.2.4/gitlabform/gitlab/project_merge_requests_approvals.py +0 -94
- gitlabform-6.2.4/gitlabform/gitlab/project_protected_environments.py +0 -37
- gitlabform-6.2.4/gitlabform/processors/project/merge_requests_approval_rules.py +0 -38
- gitlabform-6.2.4/gitlabform/processors/shared/protected_environments_processor.py +0 -20
- gitlabform-6.2.4/tests/unit/processors/__init__.py +0 -0
- gitlabform-6.2.4/tests/unit/processors/test_merge_requests_approval_rules_processor.py +0 -120
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.coveragerc +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.dockerignore +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.git-blame-ignore-revs +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/CODEOWNERS +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/ISSUE_TEMPLATE/question-or-other-issue.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/actions/setup-uv-local/README.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/actions/setup-uv-local/action.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/dependabot.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/_main.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/_releases.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/build.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/codecov-upload.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/pr-ci-workflow.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/prs-entrypoint-forks.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/prs-entrypoint-main.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/static-analysis.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/tests-acceptance.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.github/workflows/tests-unit.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.gitignore +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.lgtm.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.mypy.ini +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.overrides/gitlabform-logo-favicon.png +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.overrides/gitlabform-logo-favicon.svg +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/.python-version +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/CONTRIBUTING.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/Dockerfile +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/LICENSE +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/README.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/codecov.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/common.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/docker.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/docs.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/env.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/gitlab/await-healthy.sh +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/gitlab/gitlab.rb +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/gitlab/healthcheck-and-setup.sh +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/gitlab/run_gitlab_in_docker.sh +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/gitlab/tests.Dockerfile +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/infra.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/main.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/package.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/qa.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/dev/release.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/automation.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/contrib/coding_guidelines.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/contrib/index.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/contrib/local_development.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/contrib/releases.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/contrib/workflows.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/features.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/Opencast-logo.png +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/egnyte-logo.svg +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/elastic-path-logo.svg +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/gitlabform-logo-square.png +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/gitlabform-logo.png +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/gitlabform-logo.svg +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/group-ldap-links-provider.png +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/liquidlight-logo.svg +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/roche-logo.png +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/stroeer-online-marketing-logo.svg +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/images/ti8m-logo.png +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/index.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/installation.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/archive_unarchive.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/avatar.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/badges.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/ci_cd_variables.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/deploy_keys.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/files.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/group_ldap_links.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/group_saml_links.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/index.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/integrations.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/job_token_scope.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/labels.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/members.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/merge_requests.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/pipeline_schedules.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/project_security_settings.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/project_transfer.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/protected_environments.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/push_mirrors.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/push_rules.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/resource_groups.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/settings.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/tags_protection.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/reference/webhooks.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/requirements.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/running.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/similar_apps.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/docs/upgrade.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlab-config/config.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlab-config/gitlab-config.md +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/configuration/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/configuration/common.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/configuration/core.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/configuration/groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/configuration/projects.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/configuration/transform.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/constants.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/commits.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/merge_requests.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/pipelines.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/projects.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/python_gitlab.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/gitlab/variables.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/lists/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/lists/filter.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/lists/groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/lists/projects.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/output.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/abstract_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/application/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/application/application_settings_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/defining_keys.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_badges_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_hooks_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_labels_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_members_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_protected_branches_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_variables_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/multiple_entities_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/badges_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/files_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/hooks_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/integrations_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/members_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/project_labels_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/project_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/project_security_settings.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/project_settings_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/project_variables_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/remote_mirrors_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/resource_groups_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/schedules_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/project/tags_processor.py +0 -0
- {gitlabform-6.2.4/gitlabform/processors/shared → gitlabform-6.3.1/gitlabform/processors/util}/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/util/badges_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/util/branch_protection.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/util/decorators.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/util/difference_logger.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/util/labels_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/util/variables_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/run.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/mkdocs.yml +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/prek.toml +0 -0
- {gitlabform-6.2.4/gitlabform/processors/util → gitlabform-6.3.1/tests}/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/conftest.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_branches.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_branches_users_case_insensitive.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_group_ldap_links.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_group_protected_branches.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_group_push_rules.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_group_saml_links.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_group_settings.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_group_variables.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_merge_request_approval_rules.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_merge_request_approvals_settings.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_protected_environments.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_push_rules.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/premium/test_tags.py +0 -0
- {gitlabform-6.2.4/tests → gitlabform-6.3.1/tests/acceptance/standard}/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_application_settings.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_archive_project.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_badges.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_deploy_keys.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_deploy_keys_all_projects.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_files.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_files_templates.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_avatar.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_badges.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_hooks.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_labels.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_members_case_insensitive.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_members_groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_members_users.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_group_variables.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_hooks.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_integrations.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_job_token_scope.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_members.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_members_add_group.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_members_enforce.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_project_avatar.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_project_group_members_case_insensitive.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_project_labels.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_project_members_case_insensitve.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_project_settings.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_project_variables.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_remote_mirrors.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_resource_groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_running.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_scheduled_for_deletion_project.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_schedules.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_tags.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_token_from_config.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_transfer_project.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/standard/test_wildcard_patterns.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/ultimate/test_group_members.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/ultimate/test_group_settings.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/ultimate/test_members.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/ultimate/test_project_security_settings.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/acceptance/ultimate/test_project_settings.py +0 -0
- {gitlabform-6.2.4/tests/acceptance/standard → gitlabform-6.3.1/tests/unit}/__init__.py +0 -0
- {gitlabform-6.2.4/tests/unit → gitlabform-6.3.1/tests/unit/configuration}/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_case_sensitivity.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_inheritance_break_projects_and_groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_inheritance_break_subgroups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_inheritance_break_validation.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_projects_and_groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_skip_groups_skip_projects.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_subgroups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_wildcard_patterns.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/test_yaml_version.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/transform/test_access_level_transformer.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/transform/test_group_transformer.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/transform/test_implicit_name_transformer.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/transform/test_user_and_group_transformers.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/configuration/transform/test_user_transformer.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/gitlab/test_core.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/gitlab/test_groups.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/gitlab/test_python_gitlab.py +0 -0
- {gitlabform-6.2.4/tests/unit/configuration → gitlabform-6.3.1/tests/unit/processors}/__init__.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_abstract_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_abstract_processors.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_deploy_keys_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_difference_logger.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_files_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_group_ldap_links_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_group_members_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_group_protected_branches_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_group_saml_links_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_schedules_processor_extended_cron_pattern.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/processors/test_util_badges_processor.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/test_access_levels.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/test_non_empty_configs_provider.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/test_output.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/test_parse_args.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/test_projects_provider.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/test_show_summary.py +0 -0
- {gitlabform-6.2.4 → gitlabform-6.3.1}/tests/unit/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gitlabform
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.3.1
|
|
4
4
|
Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
|
|
5
5
|
Project-URL: Homepage, https://gitlabform.github.io/gitlabform/
|
|
6
6
|
Project-URL: Repository, https://github.com/gitlabform/gitlabform.git
|
|
@@ -19,14 +19,14 @@ 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.
|
|
22
|
+
Requires-Dist: certifi==2026.10.23
|
|
23
23
|
Requires-Dist: cli-ui==0.19.0
|
|
24
24
|
Requires-Dist: jinja2==3.1.6
|
|
25
25
|
Requires-Dist: luddite==1.0.4
|
|
26
|
-
Requires-Dist: markupsafe==3.0.
|
|
26
|
+
Requires-Dist: markupsafe==3.0.4
|
|
27
27
|
Requires-Dist: mergedeep==1.3.4
|
|
28
28
|
Requires-Dist: packaging==26.3
|
|
29
|
-
Requires-Dist: python-gitlab[graphql]==8.
|
|
29
|
+
Requires-Dist: python-gitlab[graphql]==8.6.0
|
|
30
30
|
Requires-Dist: requests==2.34.2
|
|
31
31
|
Requires-Dist: rich==15.0.0
|
|
32
32
|
Requires-Dist: ruamel-yaml==0.19.1
|
|
@@ -6,6 +6,39 @@ For details on how to migrate between major versions, please refer to the [upgra
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## 6.3.1
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* keep long configuration values from being wrapped [#1470](https://github.com/gitlabform/gitlabform/pull/1470). ([robertrotaru](https://github.com/robertrotaru))
|
|
14
|
+
|
|
15
|
+
### Refactors
|
|
16
|
+
|
|
17
|
+
* migrate merge requests approval rules processor to python-gitlab [#1459](https://github.com/gitlabform/gitlabform/pull/1459). ([rickbrouwer](https://github.com/rickbrouwer))
|
|
18
|
+
|
|
19
|
+
### Dependencies
|
|
20
|
+
|
|
21
|
+
* Update various dependencies to newer versions.
|
|
22
|
+
|
|
23
|
+
Thanks to all the contributors of this release!
|
|
24
|
+
|
|
25
|
+
## 6.3.0
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* show diff in dry-run mode for group settings [#1462](https://github.com/gitlabform/gitlabform/pull/1462). ([vetlekise](https://github.com/vetlekise))
|
|
30
|
+
* skip branch protection for non-existent branches [#1457](https://github.com/gitlabform/gitlabform/pull/1457). ([amimas](https://github.com/amimas))
|
|
31
|
+
|
|
32
|
+
### Refactors
|
|
33
|
+
|
|
34
|
+
* migrate protected environments processor to python-gitlab [#1438](https://github.com/gitlabform/gitlabform/pull/1438). ([rickbrouwer](https://github.com/rickbrouwer))
|
|
35
|
+
|
|
36
|
+
### Dependencies
|
|
37
|
+
|
|
38
|
+
* Update various dependencies to newer versions.
|
|
39
|
+
|
|
40
|
+
Thanks to all the contributors of this release!
|
|
41
|
+
|
|
9
42
|
## 6.2.4
|
|
10
43
|
|
|
11
44
|
### Performance
|
|
@@ -99,8 +99,8 @@ Sometimes there is some logic in these methods if:
|
|
|
99
99
|
|
|
100
100
|
* we only need a specific part of the response from GitLab API - see `GitLabProjects.get_all_projects()` as an example,
|
|
101
101
|
* some GitLab APIs need some workarounds for their bugs or documentation inconsistencies, like:
|
|
102
|
-
* some APIs declare in the docs that they accept both "group/project" string OR a project id while in fact only the latter works
|
|
103
|
-
* some APIs return invalid HTTP error codes, like 404 instead of 400 - see `
|
|
102
|
+
* some APIs declare in the docs that they accept both "group/project" string OR a project id while in fact only the latter works,
|
|
103
|
+
* some APIs return invalid HTTP error codes, like 404 instead of 400 - see `GroupLDAPLinksProcessor._create_link` as an example.
|
|
104
104
|
|
|
105
105
|
**Note**: Some of the code here is **NOT used by the GitLabForm app**, but utilized by internal Egnyte
|
|
106
106
|
applications that have not (yet) switched to the more standard `python-gitlab`.
|
|
@@ -105,6 +105,38 @@ projects_and_groups:
|
|
|
105
105
|
allow_force_push: true
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
### Project only option
|
|
109
|
+
|
|
110
|
+
This option is only supported for project-level branch protection under `branches`, and is a GitLabForm-specific feature.
|
|
111
|
+
|
|
112
|
+
GitLab allows creating branch protection even when a branch does not exist. This is useful so that a branch is automatically protected when it matches the configured name. If this is not desired, use a top-level key named `skip_missing_branch: true`. Setting this flag will skip branch protection for a branch when it does not exist.
|
|
113
|
+
|
|
114
|
+
This is intended for exact branch names only. It does not apply to wildcard patterns such as `release-*`.
|
|
115
|
+
|
|
116
|
+
- If `strict` mode is enabled, GitLabForm still fails on missing literal branches.
|
|
117
|
+
- If `strict` mode is disabled, GitLabForm skips protecting a missing literal branch and continues.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
projects_and_groups:
|
|
123
|
+
group_1/project_1:
|
|
124
|
+
branches:
|
|
125
|
+
skip_missing_branch: true
|
|
126
|
+
main:
|
|
127
|
+
protected: true
|
|
128
|
+
push_access_level: no access
|
|
129
|
+
merge_access_level: maintainer
|
|
130
|
+
branch-that-does-not-exist-yet: # This will be skipped
|
|
131
|
+
protected: true
|
|
132
|
+
push_access_level: no access
|
|
133
|
+
merge_access_level: maintainer
|
|
134
|
+
"release-*":
|
|
135
|
+
protected: true
|
|
136
|
+
push_access_level: no access
|
|
137
|
+
merge_access_level: maintainer
|
|
138
|
+
```
|
|
139
|
+
|
|
108
140
|
### Premium-only features
|
|
109
141
|
|
|
110
142
|
!!! info
|
|
@@ -56,6 +56,7 @@ class GitLabForm:
|
|
|
56
56
|
noop=False,
|
|
57
57
|
output_file=None,
|
|
58
58
|
recurse_subgroups=True,
|
|
59
|
+
strict=True,
|
|
59
60
|
):
|
|
60
61
|
if target and config_string:
|
|
61
62
|
# this mode is basically only for testing
|
|
@@ -64,7 +65,7 @@ class GitLabForm:
|
|
|
64
65
|
self.config_string = config_string
|
|
65
66
|
self.verbose = True
|
|
66
67
|
self.debug = True
|
|
67
|
-
self.strict =
|
|
68
|
+
self.strict = strict
|
|
68
69
|
self.start_from = 1
|
|
69
70
|
self.start_from_group = 1
|
|
70
71
|
self.noop = noop
|
|
@@ -10,12 +10,7 @@ from gitlabform.gitlab.commits import GitLabCommits
|
|
|
10
10
|
from gitlabform.gitlab.groups import GitLabGroups
|
|
11
11
|
from gitlabform.gitlab.merge_requests import GitLabMergeRequests
|
|
12
12
|
from gitlabform.gitlab.pipelines import GitLabPipelines
|
|
13
|
-
from gitlabform.gitlab.
|
|
14
|
-
GitLabProjectProtectedEnvironments,
|
|
15
|
-
)
|
|
16
|
-
from gitlabform.gitlab.project_merge_requests_approvals import (
|
|
17
|
-
GitLabProjectMergeRequestsApprovals,
|
|
18
|
-
)
|
|
13
|
+
from gitlabform.gitlab.projects import GitLabProjects
|
|
19
14
|
from gitlabform.gitlab.python_gitlab import PythonGitlab
|
|
20
15
|
|
|
21
16
|
|
|
@@ -46,13 +41,13 @@ class AccessLevel(enum.IntEnum):
|
|
|
46
41
|
return [level.name.lower().replace("_", " ") for level in AccessLevel]
|
|
47
42
|
|
|
48
43
|
|
|
44
|
+
# GitLabGroups and GitLabProjects stay as bases until gitlabform/lists/ no longer uses their methods
|
|
49
45
|
class GitLab(
|
|
50
46
|
GitLabCommits,
|
|
51
47
|
GitLabGroups,
|
|
52
48
|
GitLabMergeRequests,
|
|
53
49
|
GitLabPipelines,
|
|
54
|
-
|
|
55
|
-
GitLabProjectMergeRequestsApprovals,
|
|
50
|
+
GitLabProjects,
|
|
56
51
|
):
|
|
57
52
|
pass
|
|
58
53
|
|
|
@@ -157,17 +157,6 @@ class GitLabCore:
|
|
|
157
157
|
group = self._make_requests_to_api("groups/%s?with_projects=false", path, "GET")
|
|
158
158
|
return int(group["id"])
|
|
159
159
|
|
|
160
|
-
@functools.lru_cache()
|
|
161
|
-
def _get_protected_branch_id(self, project_and_group_name, branch) -> int:
|
|
162
|
-
branch = self._make_requests_to_api("projects/%s/protected_branches/%s", (project_and_group_name, branch))
|
|
163
|
-
return int(branch["id"])
|
|
164
|
-
|
|
165
|
-
@functools.lru_cache()
|
|
166
|
-
def _get_project_id(self, project_and_group):
|
|
167
|
-
# This is a NEW workaround for https://github.com/gitlabhq/gitlabhq/issues/8290
|
|
168
|
-
result = self.get_project(project_and_group)
|
|
169
|
-
return str(result["id"])
|
|
170
|
-
|
|
171
160
|
def _make_requests_to_api(
|
|
172
161
|
self,
|
|
173
162
|
path_as_format_string,
|
{gitlabform-6.2.4 → gitlabform-6.3.1}/gitlabform/processors/group/group_settings_processor.py
RENAMED
|
@@ -11,6 +11,9 @@ class GroupSettingsProcessor(AbstractProcessor):
|
|
|
11
11
|
def __init__(self, gitlab: GitLab):
|
|
12
12
|
super().__init__("group_settings", gitlab)
|
|
13
13
|
|
|
14
|
+
def _get_current_state(self, group: str) -> dict:
|
|
15
|
+
return self.gl.get_group_by_path_cached(group).asdict()
|
|
16
|
+
|
|
14
17
|
def _process_configuration(self, group: str, configuration: Dict):
|
|
15
18
|
configured_group_settings = configuration.get("group_settings", {})
|
|
16
19
|
|
|
@@ -36,7 +36,7 @@ from gitlabform.processors.project.resource_groups_processor import (
|
|
|
36
36
|
from gitlabform.processors.project.schedules_processor import SchedulesProcessor
|
|
37
37
|
from gitlabform.processors.project.tags_processor import TagsProcessor
|
|
38
38
|
from gitlabform.processors.project.project_variables_processor import ProjectVariablesProcessor
|
|
39
|
-
from gitlabform.processors.
|
|
39
|
+
from gitlabform.processors.project.protected_environments_processor import (
|
|
40
40
|
ProtectedEnvironmentsProcessor,
|
|
41
41
|
)
|
|
42
42
|
|
|
@@ -39,8 +39,11 @@ class BranchesProcessor(AbstractProcessor):
|
|
|
39
39
|
self.custom_diff_analyzers["unprotect_access_levels"] = BranchProtection.naive_access_level_diff_analyzer
|
|
40
40
|
|
|
41
41
|
def _can_proceed(self, project_or_group: str, configuration: dict):
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
branches_config = dict(configuration.get("branches", {}))
|
|
43
|
+
branches_config.pop("skip_missing_branch", None)
|
|
44
|
+
|
|
45
|
+
for branch in sorted(branches_config):
|
|
46
|
+
branch_config = branches_config[branch]
|
|
44
47
|
if branch_config.get("protected") is None:
|
|
45
48
|
critical(f"The Protected key is mandatory in branches configuration, fix {branch} YAML config")
|
|
46
49
|
sys.exit(EXIT_INVALID_INPUT)
|
|
@@ -54,36 +57,51 @@ class BranchesProcessor(AbstractProcessor):
|
|
|
54
57
|
"""
|
|
55
58
|
|
|
56
59
|
project: Project = self.gl.get_project_by_path_cached(project_and_group)
|
|
60
|
+
branches_config: dict = dict(configuration["branches"])
|
|
61
|
+
skip_missing_branch_enabled: bool = branches_config.pop("skip_missing_branch", False)
|
|
57
62
|
|
|
58
|
-
for branch in sorted(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.process_branch_protection(project, branch, branch_configuration)
|
|
63
|
+
for branch in sorted(branches_config):
|
|
64
|
+
self.process_branch_protection(project, branch, branches_config[branch], skip_missing_branch_enabled)
|
|
62
65
|
|
|
63
|
-
def process_branch_protection(
|
|
66
|
+
def process_branch_protection(
|
|
67
|
+
self, project: Project, branch_name: str, branch_configuration: dict, skip_if_missing: bool
|
|
68
|
+
):
|
|
64
69
|
"""
|
|
65
70
|
High-level logic for processing branch protection.
|
|
66
71
|
|
|
67
72
|
1. Validates branch existence (unless wildcard).
|
|
68
73
|
2. Handles 'protected: false' (unprotecting).
|
|
69
74
|
3. Handles 'protected: true':
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
- If not currently protected: Create protection.
|
|
76
|
+
- If protected: Update using PATCH (EE > 15.6) or Unprotect/Reprotect (CE/Old EE).
|
|
77
|
+
4. Skips protection if the branch does not exist and 'skip_if_missing' is enabled.
|
|
78
|
+
- This is only applicable for literal branch names, not wildcards.
|
|
72
79
|
"""
|
|
73
80
|
protected_branch: Optional[ProjectProtectedBranch] = None
|
|
74
81
|
|
|
75
82
|
# If protected branch name contains a supported wildcard do not try looking it up
|
|
76
83
|
if not self.branch_name_contains_supported_wildcard(branch_name):
|
|
77
|
-
# Check branch we are trying to protect actually exists first
|
|
84
|
+
# Check branch we are trying to protect actually exists first.
|
|
85
|
+
# When explicitly requested, skip missing literal branches instead of creating
|
|
86
|
+
# a protection rule for a branch that does not exist yet.
|
|
78
87
|
try:
|
|
79
88
|
project.branches.get(branch_name)
|
|
80
|
-
except GitlabGetError:
|
|
89
|
+
except GitlabGetError as e:
|
|
90
|
+
if e.response_code != 404:
|
|
91
|
+
raise
|
|
92
|
+
|
|
81
93
|
message = f"Branch '{branch_name}' not found when processing it to be protected/unprotected!"
|
|
82
94
|
if self.strict:
|
|
83
95
|
critical(message)
|
|
84
96
|
sys.exit(EXIT_INVALID_INPUT)
|
|
85
97
|
else:
|
|
86
98
|
warning(message)
|
|
99
|
+
if skip_if_missing:
|
|
100
|
+
info(
|
|
101
|
+
f"Skipping branch protection for '{branch_name}' because the branch does not exist and "
|
|
102
|
+
"skip_missing_branch is enabled."
|
|
103
|
+
)
|
|
104
|
+
return
|
|
87
105
|
|
|
88
106
|
try:
|
|
89
107
|
protected_branch = project.protectedbranches.get(branch_name)
|
|
@@ -91,6 +109,8 @@ class BranchesProcessor(AbstractProcessor):
|
|
|
91
109
|
message = f"The branch '{branch_name}' is not protected!"
|
|
92
110
|
info(message)
|
|
93
111
|
|
|
112
|
+
branch_config: dict = self.convert_user_and_group_names_to_ids(branch_configuration)
|
|
113
|
+
|
|
94
114
|
if branch_config.get("protected"):
|
|
95
115
|
if not protected_branch:
|
|
96
116
|
info(f"Creating branch protection for {branch_name}")
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from logging import critical, info
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
from gitlab.exceptions import GitlabGetError
|
|
6
|
+
from gitlab.v4.objects import Project, ProjectApprovalRule
|
|
7
|
+
|
|
8
|
+
from gitlabform.constants import EXIT_INVALID_INPUT
|
|
9
|
+
from gitlabform.gitlab import GitLab
|
|
10
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MergeRequestsApprovalRules(AbstractProcessor):
|
|
14
|
+
def __init__(self, gitlab: GitLab):
|
|
15
|
+
super().__init__("merge_requests_approval_rules", gitlab)
|
|
16
|
+
|
|
17
|
+
def _process_configuration(self, project_and_group: str, configuration: Dict):
|
|
18
|
+
# copy, so that popping "enforce" does not mutate the shared configuration,
|
|
19
|
+
# which is reused when this method is retried
|
|
20
|
+
configured_rules: Dict = dict(configuration.get("merge_requests_approval_rules", {}))
|
|
21
|
+
enforce = configured_rules.pop("enforce", False)
|
|
22
|
+
|
|
23
|
+
# Rules are matched against GitLab by name, so entries sharing a name would
|
|
24
|
+
# reconcile ambiguously; reject them before any changes are made.
|
|
25
|
+
self._fail_on_duplicate_names(project_and_group, configured_rules)
|
|
26
|
+
|
|
27
|
+
project: Project = self.gl.get_project_by_path_cached(project_and_group)
|
|
28
|
+
existing_rules: List[ProjectApprovalRule] = project.approvalrules.list(get_all=True)
|
|
29
|
+
|
|
30
|
+
# Resolve branch names to ids before anything is deleted, so that a bad branch
|
|
31
|
+
# name in the config fails the section before it can remove existing rules,
|
|
32
|
+
# and so that each unique branch is fetched only once.
|
|
33
|
+
protected_branch_ids = self._protected_branch_ids(project, configured_rules)
|
|
34
|
+
|
|
35
|
+
if enforce:
|
|
36
|
+
# Delete stale rules before adding new ones, so that unique-constraint slots
|
|
37
|
+
# (e.g. only one any_approver rule per project) are freed for the rules below.
|
|
38
|
+
configured_names = {c.get("name") for c in configured_rules.values() if c.get("name")}
|
|
39
|
+
for existing in existing_rules:
|
|
40
|
+
if existing.name not in configured_names:
|
|
41
|
+
info(
|
|
42
|
+
f"Deleting rule '{existing.name}' of {self.configuration_name} in {project_and_group}"
|
|
43
|
+
f" as it's not in config and enforce is set to true."
|
|
44
|
+
)
|
|
45
|
+
existing.delete()
|
|
46
|
+
existing_rules = [rule for rule in existing_rules if rule.name in configured_names]
|
|
47
|
+
|
|
48
|
+
for entity_name, rule_config in configured_rules.items():
|
|
49
|
+
name = rule_config.get("name")
|
|
50
|
+
matching = next((rule for rule in existing_rules if rule.name == name), None)
|
|
51
|
+
|
|
52
|
+
if rule_config.get("delete", False):
|
|
53
|
+
if matching:
|
|
54
|
+
info(f"Deleting {entity_name} of {self.configuration_name} in {project_and_group}")
|
|
55
|
+
matching.delete()
|
|
56
|
+
else:
|
|
57
|
+
info(
|
|
58
|
+
f"Not deleting {entity_name} of {self.configuration_name} in {project_and_group},"
|
|
59
|
+
f" because it doesn't exist"
|
|
60
|
+
)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if matching:
|
|
64
|
+
if self._needs_update(matching.asdict(), rule_config):
|
|
65
|
+
info(f" * Editing {entity_name} of {self.configuration_name} in {project_and_group}")
|
|
66
|
+
project.approvalrules.update(
|
|
67
|
+
matching.id, self._payload_for_update(rule_config, protected_branch_ids)
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
info(
|
|
71
|
+
f" * {entity_name} of {self.configuration_name} in {project_and_group}"
|
|
72
|
+
f" doesn't need an update."
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
info(f" * Adding {entity_name} of {self.configuration_name} in {project_and_group}")
|
|
76
|
+
project.approvalrules.create(self._payload_for_create(rule_config, protected_branch_ids))
|
|
77
|
+
|
|
78
|
+
def _needs_update(self, entity_in_gitlab: Dict, entity_in_configuration: Dict) -> bool:
|
|
79
|
+
# GitLab returns users/groups as lists of objects and protected_branches as list
|
|
80
|
+
# of objects with a "name" field, while the config (post-transform) has user_ids /
|
|
81
|
+
# group_ids as int lists and protected_branches as list of names. Without
|
|
82
|
+
# normalization the base _needs_update always triggers an update, even when nothing
|
|
83
|
+
# changed. We normalize both sides unconditionally so keys line up regardless of
|
|
84
|
+
# whether GitLab omitted an empty list or the user omitted the field in config.
|
|
85
|
+
gitlab_norm = dict(entity_in_gitlab)
|
|
86
|
+
gitlab_norm["user_ids"] = sorted(u["id"] for u in gitlab_norm.pop("users", []))
|
|
87
|
+
gitlab_norm["group_ids"] = sorted(g["id"] for g in gitlab_norm.pop("groups", []))
|
|
88
|
+
gitlab_protected_branches = gitlab_norm.pop("protected_branches", [])
|
|
89
|
+
|
|
90
|
+
# The update payload still sends omitted user_ids/group_ids/protected_branches as
|
|
91
|
+
# empty lists (old behavior, kept until #1468), so the comparison here has to
|
|
92
|
+
# mirror what _payload_for_update will send, or a rule would never converge.
|
|
93
|
+
# "delete" is a gitlabform control key, not a rule attribute.
|
|
94
|
+
config_norm = dict(entity_in_configuration)
|
|
95
|
+
config_norm.pop("delete", None)
|
|
96
|
+
config_norm["user_ids"] = sorted(config_norm.get("user_ids", []))
|
|
97
|
+
config_norm["group_ids"] = sorted(config_norm.get("group_ids", []))
|
|
98
|
+
|
|
99
|
+
# The config can reference branches by name (protected_branches) or directly
|
|
100
|
+
# by id (protected_branch_ids); compare whichever representation it uses.
|
|
101
|
+
if "protected_branch_ids" in config_norm:
|
|
102
|
+
gitlab_norm["protected_branch_ids"] = sorted(b["id"] for b in gitlab_protected_branches)
|
|
103
|
+
config_norm["protected_branch_ids"] = sorted(config_norm["protected_branch_ids"])
|
|
104
|
+
if "protected_branch_ids" not in config_norm or "protected_branches" in config_norm:
|
|
105
|
+
gitlab_norm["protected_branches"] = sorted(b["name"] for b in gitlab_protected_branches)
|
|
106
|
+
config_norm["protected_branches"] = sorted(config_norm.get("protected_branches", []))
|
|
107
|
+
|
|
108
|
+
return super()._needs_update(gitlab_norm, config_norm)
|
|
109
|
+
|
|
110
|
+
def _fail_on_duplicate_names(self, project_and_group: str, configured_rules: Dict) -> None:
|
|
111
|
+
names = [rule_config.get("name") for rule_config in configured_rules.values() if rule_config.get("name")]
|
|
112
|
+
duplicates = {name for name in names if names.count(name) > 1}
|
|
113
|
+
if duplicates:
|
|
114
|
+
critical(
|
|
115
|
+
f"Entities in {self.configuration_name} for {project_and_group}"
|
|
116
|
+
f" share the same name(s): {sorted(duplicates)}"
|
|
117
|
+
)
|
|
118
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _protected_branch_ids(project: Project, configured_rules: Dict) -> Dict[str, int]:
|
|
122
|
+
branch_names = {
|
|
123
|
+
branch_name
|
|
124
|
+
for rule_config in configured_rules.values()
|
|
125
|
+
if not rule_config.get("delete", False)
|
|
126
|
+
for branch_name in rule_config.get("protected_branches", [])
|
|
127
|
+
}
|
|
128
|
+
if not branch_names:
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
existing = {branch.name: branch.id for branch in project.protectedbranches.list(get_all=True)}
|
|
132
|
+
missing = branch_names - existing.keys()
|
|
133
|
+
if missing:
|
|
134
|
+
raise GitlabGetError(
|
|
135
|
+
f"Protected branches {sorted(missing)} not found in this project",
|
|
136
|
+
response_code=404,
|
|
137
|
+
)
|
|
138
|
+
return {branch_name: existing[branch_name] for branch_name in branch_names}
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _payload_for_create(rule_config: Dict, protected_branch_ids: Dict[str, int]) -> Dict:
|
|
142
|
+
# "delete" is a gitlabform control key and should not be sent to the API
|
|
143
|
+
payload = {k: v for k, v in rule_config.items() if k != "delete"}
|
|
144
|
+
if "protected_branches" in payload:
|
|
145
|
+
# the API expects protected_branch_ids, while the config has branch names
|
|
146
|
+
branch_names = payload.pop("protected_branches")
|
|
147
|
+
payload["protected_branch_ids"] = [protected_branch_ids[branch_name] for branch_name in branch_names]
|
|
148
|
+
return payload
|
|
149
|
+
|
|
150
|
+
def _payload_for_update(self, rule_config: Dict, protected_branch_ids: Dict[str, int]) -> Dict:
|
|
151
|
+
payload = self._payload_for_create(rule_config, protected_branch_ids)
|
|
152
|
+
# GitLab interprets omitted user_ids/group_ids/protected_branch_ids as "no change",
|
|
153
|
+
# but the old code sent them as empty lists, clearing them. That behavior is kept
|
|
154
|
+
# for now; changing it is a breaking change, tracked in #1468.
|
|
155
|
+
payload.setdefault("user_ids", [])
|
|
156
|
+
payload.setdefault("group_ids", [])
|
|
157
|
+
payload.setdefault("protected_branch_ids", [])
|
|
158
|
+
return payload
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from logging import info
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
from gitlab.v4.objects import Project, ProjectProtectedEnvironment
|
|
5
|
+
|
|
6
|
+
from gitlabform.gitlab import GitLab
|
|
7
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProtectedEnvironmentsProcessor(AbstractProcessor):
|
|
11
|
+
def __init__(self, gitlab: GitLab):
|
|
12
|
+
super().__init__("protected_environments", gitlab)
|
|
13
|
+
# deploy_access_levels is a list of dicts; compare positionally, ignoring keys
|
|
14
|
+
# that GitLab adds (id, description) but the config doesn't set.
|
|
15
|
+
self.custom_diff_analyzers["deploy_access_levels"] = self.recursive_diff_analyzer
|
|
16
|
+
|
|
17
|
+
def _process_configuration(self, project_and_group: str, configuration: Dict):
|
|
18
|
+
# copy, so that popping "enforce" does not mutate the shared configuration,
|
|
19
|
+
# which is reused when this method is retried
|
|
20
|
+
configured_envs: Dict = dict(configuration.get("protected_environments", {}))
|
|
21
|
+
enforce = configured_envs.pop("enforce", False)
|
|
22
|
+
|
|
23
|
+
project: Project = self.gl.get_project_by_path_cached(project_and_group)
|
|
24
|
+
existing_envs: List[ProjectProtectedEnvironment] = project.protected_environments.list(get_all=True)
|
|
25
|
+
|
|
26
|
+
processed_names: set = set()
|
|
27
|
+
|
|
28
|
+
for entity_name, env_config in configured_envs.items():
|
|
29
|
+
name = env_config["name"]
|
|
30
|
+
matching = next((env for env in existing_envs if env.name == name), None)
|
|
31
|
+
|
|
32
|
+
if env_config.get("delete", False):
|
|
33
|
+
if matching:
|
|
34
|
+
info(f"Deleting {entity_name} of {self.configuration_name} in {project_and_group}")
|
|
35
|
+
matching.delete()
|
|
36
|
+
else:
|
|
37
|
+
info(
|
|
38
|
+
f"Not deleting {entity_name} of {self.configuration_name} in {project_and_group},"
|
|
39
|
+
f" because it doesn't exist"
|
|
40
|
+
)
|
|
41
|
+
processed_names.add(name)
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
if matching:
|
|
45
|
+
if self._needs_update(matching.asdict(), env_config):
|
|
46
|
+
# The API has an update endpoint (https://docs.gitlab.com/api/protected_environments/),
|
|
47
|
+
# but python-gitlab's ProjectProtectedEnvironmentManager doesn't support it, so we recreate.
|
|
48
|
+
info(f" * Recreating {entity_name} of {self.configuration_name} in {project_and_group}")
|
|
49
|
+
matching.delete()
|
|
50
|
+
project.protected_environments.create(env_config)
|
|
51
|
+
else:
|
|
52
|
+
info(
|
|
53
|
+
f" * {entity_name} of {self.configuration_name} in {project_and_group}"
|
|
54
|
+
f" doesn't need an update."
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
info(f" * Adding {entity_name} of {self.configuration_name} in {project_and_group}")
|
|
58
|
+
project.protected_environments.create(env_config)
|
|
59
|
+
processed_names.add(name)
|
|
60
|
+
|
|
61
|
+
if enforce:
|
|
62
|
+
for existing in existing_envs:
|
|
63
|
+
if existing.name not in processed_names:
|
|
64
|
+
info(
|
|
65
|
+
f"Deleting protected environment '{existing.name}' of {self.configuration_name}"
|
|
66
|
+
f" in {project_and_group} as it's not in config and enforce is set to true."
|
|
67
|
+
)
|
|
68
|
+
existing.delete()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import sys
|
|
2
3
|
from datetime import date
|
|
3
4
|
from io import StringIO
|
|
4
5
|
|
|
@@ -46,6 +47,8 @@ def configure_ruamel_yaml_loader(typ=None, pure=False, output=None, plug_ins=Non
|
|
|
46
47
|
yaml_loader.indent(mapping=3, sequence=2, offset=0)
|
|
47
48
|
yaml_loader.allow_duplicate_keys = True
|
|
48
49
|
yaml_loader.explicit_start = False
|
|
50
|
+
# never wrap long scalars: a wrapped double-quoted string that is reloaded can gain spaces after backslash escapes
|
|
51
|
+
yaml_loader.width = sys.maxsize
|
|
49
52
|
# show null
|
|
50
53
|
yaml_loader.representer.add_representer(
|
|
51
54
|
type(None),
|
|
@@ -10,7 +10,7 @@ packages = ["gitlabform"]
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "gitlabform"
|
|
13
|
-
version = "6.
|
|
13
|
+
version = "6.3.1"
|
|
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,14 +31,14 @@ classifiers = [
|
|
|
31
31
|
|
|
32
32
|
# Main runtime dependencies. Pinned strictly to ensure production stability.
|
|
33
33
|
dependencies = [
|
|
34
|
-
"certifi==2026.
|
|
34
|
+
"certifi==2026.10.23",
|
|
35
35
|
"cli-ui==0.19.0",
|
|
36
36
|
"jinja2==3.1.6",
|
|
37
37
|
"luddite==1.0.4",
|
|
38
|
-
"markupsafe==3.0.
|
|
38
|
+
"markupsafe==3.0.4",
|
|
39
39
|
"mergedeep==1.3.4",
|
|
40
40
|
"packaging==26.3",
|
|
41
|
-
"python-gitlab[graphql]==8.
|
|
41
|
+
"python-gitlab[graphql]==8.6.0",
|
|
42
42
|
"requests==2.34.2",
|
|
43
43
|
"rich==15.0.0",
|
|
44
44
|
"ruamel.yaml==0.19.1",
|
|
@@ -53,34 +53,34 @@ dev = [
|
|
|
53
53
|
{ include-group = "lint" },
|
|
54
54
|
{ include-group = "docs" },
|
|
55
55
|
{ include-group = "release" },
|
|
56
|
-
"commitizen==4.
|
|
57
|
-
"prek==0.5.
|
|
56
|
+
"commitizen==4.19.0",
|
|
57
|
+
"prek==0.5.3",
|
|
58
58
|
]
|
|
59
59
|
# Minimum set of dependencies required to execute the test suite.
|
|
60
60
|
test = [
|
|
61
|
-
"coverage==7.16.
|
|
61
|
+
"coverage==7.16.1",
|
|
62
62
|
"cryptography==50.0.1",
|
|
63
63
|
"pytest==9.1.1",
|
|
64
64
|
"pytest-cov==7.1.0",
|
|
65
|
-
"pytest-rerunfailures==16.
|
|
65
|
+
"pytest-rerunfailures==16.7",
|
|
66
66
|
"xkcdpass==1.30.0",
|
|
67
67
|
]
|
|
68
68
|
# Static analysis and formatting tools.
|
|
69
69
|
lint = [
|
|
70
70
|
"bandit==1.9.4",
|
|
71
|
-
"black==26.
|
|
71
|
+
"black==26.10.1",
|
|
72
72
|
"codespell==2.4.3",
|
|
73
73
|
"mypy==2.3.1",
|
|
74
74
|
"mypy-extensions==1.1.0",
|
|
75
|
-
"ruff==0.16.
|
|
76
|
-
"types-requests==2.33.0.
|
|
75
|
+
"ruff==0.16.9",
|
|
76
|
+
"types-requests==2.33.0.20260906",
|
|
77
77
|
]
|
|
78
78
|
# Documentation generation stack.
|
|
79
79
|
docs = [
|
|
80
80
|
"markdown==3.10.3",
|
|
81
|
-
"zensical==0.0.
|
|
81
|
+
"zensical==0.0.65",
|
|
82
82
|
"pygments==2.21.0",
|
|
83
|
-
"pymdown-extensions==
|
|
83
|
+
"pymdown-extensions==12.1",
|
|
84
84
|
]
|
|
85
85
|
# Tools required for building and verifying releases.
|
|
86
86
|
release = [
|
|
@@ -338,6 +338,7 @@ def run_gitlabform(
|
|
|
338
338
|
noop: bool = False,
|
|
339
339
|
output_file: Optional[str] = None,
|
|
340
340
|
recurse_subgroups: bool = True,
|
|
341
|
+
strict: bool = True,
|
|
341
342
|
) -> None:
|
|
342
343
|
"""
|
|
343
344
|
Runs GitLabForm with the given configuration and target.
|
|
@@ -362,5 +363,6 @@ def run_gitlabform(
|
|
|
362
363
|
noop=noop,
|
|
363
364
|
output_file=output_file,
|
|
364
365
|
recurse_subgroups=recurse_subgroups,
|
|
366
|
+
strict=strict,
|
|
365
367
|
)
|
|
366
368
|
gf.run()
|