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