gitlabform 4.6.1__tar.gz → 4.7.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-4.6.1 → gitlabform-4.7.0}/PKG-INFO +10 -10
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/__init__.py +33 -11
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/core.py +30 -15
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/__init__.py +2 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/files_processor.py +8 -4
- gitlabform-4.7.0/gitlabform/processors/project/remote_mirrors_processor.py +280 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform.egg-info/PKG-INFO +10 -10
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform.egg-info/SOURCES.txt +1 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform.egg-info/requires.txt +9 -9
- {gitlabform-4.6.1 → gitlabform-4.7.0}/pyproject.toml +10 -10
- {gitlabform-4.6.1 → gitlabform-4.7.0}/LICENSE +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/README.md +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/configuration/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/configuration/common.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/configuration/core.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/configuration/groups.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/configuration/projects.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/configuration/transform.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/constants.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/commits.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/group_badges.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/group_ldap_links.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/group_variables.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/groups.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/merge_requests.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/pipelines.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/project_badges.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/project_deploy_keys.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/project_protected_environments.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/project_security_settings.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/projects.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/python_gitlab.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/gitlab/variables.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/lists/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/lists/filter.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/lists/groups.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/lists/projects.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/output.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/abstract_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/application/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/application/application_settings_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/defining_keys.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_badges_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_hooks_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_labels_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_members_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_settings_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_variables_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/multiple_entities_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/badges_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/branches_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/hooks_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/integrations_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/members_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_labels_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_security_settings.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_settings_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/resource_groups_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/schedules_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/tags_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/variables_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/shared/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/single_entity_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/util/__init__.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/util/decorators.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/util/difference_logger.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/util/labels_processor.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/run.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/util.py +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform.egg-info/dependency_links.txt +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform.egg-info/entry_points.txt +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform.egg-info/top_level.txt +0 -0
- {gitlabform-4.6.1 → gitlabform-4.7.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabform
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.7.0
|
|
4
4
|
Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
|
|
5
5
|
Author: Greg Dubicki and Contributors
|
|
6
6
|
Project-URL: Homepage, https://gitlabform.github.io/gitlabform/
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Version Control :: Git
|
|
|
20
20
|
Requires-Python: >=3.12.0
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: certifi==
|
|
23
|
+
Requires-Dist: certifi==2026.1.4
|
|
24
24
|
Requires-Dist: cli-ui==0.19.0
|
|
25
25
|
Requires-Dist: ez-yaml==1.2.0
|
|
26
26
|
Requires-Dist: Jinja2==3.1.6
|
|
@@ -28,24 +28,24 @@ Requires-Dist: luddite==1.0.4
|
|
|
28
28
|
Requires-Dist: MarkupSafe==3.0.3
|
|
29
29
|
Requires-Dist: mergedeep==1.3.4
|
|
30
30
|
Requires-Dist: packaging==25.0
|
|
31
|
-
Requires-Dist: python-gitlab==7.
|
|
32
|
-
Requires-Dist: python-gitlab[graphql]==7.
|
|
31
|
+
Requires-Dist: python-gitlab==7.1.0
|
|
32
|
+
Requires-Dist: python-gitlab[graphql]==7.1.0
|
|
33
33
|
Requires-Dist: requests==2.32.5
|
|
34
34
|
Requires-Dist: ruamel.yaml==0.17.21
|
|
35
|
-
Requires-Dist: types-requests==2.32.4.
|
|
36
|
-
Requires-Dist: types-setuptools==80.9.0.
|
|
35
|
+
Requires-Dist: types-requests==2.32.4.20260107
|
|
36
|
+
Requires-Dist: types-setuptools==80.9.0.20251223
|
|
37
37
|
Requires-Dist: yamlpath==3.8.2
|
|
38
38
|
Provides-Extra: test
|
|
39
|
-
Requires-Dist: coverage==7.
|
|
39
|
+
Requires-Dist: coverage==7.13.1; extra == "test"
|
|
40
40
|
Requires-Dist: cryptography==46.0.3; extra == "test"
|
|
41
41
|
Requires-Dist: deepdiff==8.6.1; extra == "test"
|
|
42
|
-
Requires-Dist: mypy==1.
|
|
42
|
+
Requires-Dist: mypy==1.19.1; extra == "test"
|
|
43
43
|
Requires-Dist: mypy-extensions==1.1.0; extra == "test"
|
|
44
44
|
Requires-Dist: pre-commit==2.21.0; extra == "test"
|
|
45
|
-
Requires-Dist: pytest==
|
|
45
|
+
Requires-Dist: pytest==9.0.2; extra == "test"
|
|
46
46
|
Requires-Dist: pytest-cov==7.0.0; extra == "test"
|
|
47
47
|
Requires-Dist: pytest-rerunfailures==16.1; extra == "test"
|
|
48
|
-
Requires-Dist: xkcdpass==1.
|
|
48
|
+
Requires-Dist: xkcdpass==1.30.0; extra == "test"
|
|
49
49
|
Provides-Extra: docs
|
|
50
50
|
Requires-Dist: mkdocs; extra == "docs"
|
|
51
51
|
Requires-Dist: mkdocs-material; extra == "docs"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import enum
|
|
2
|
+
import inspect
|
|
2
3
|
|
|
3
4
|
from typing import List
|
|
4
5
|
|
|
5
|
-
from gitlab import GraphQL
|
|
6
|
+
from gitlab import Gitlab as GitlabClient, GraphQL
|
|
6
7
|
|
|
7
8
|
from gitlabform.gitlab.commits import GitLabCommits
|
|
8
9
|
from gitlabform.gitlab.group_badges import GitLabGroupBadges
|
|
@@ -67,24 +68,45 @@ class GitLab(
|
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
class GitlabWrapper:
|
|
71
|
+
# Parameters accepted by python-gitlab's Gitlab.__init__
|
|
72
|
+
# Other config keys (like max_retries) are used elsewhere in gitlabform
|
|
73
|
+
# or passed to specific components like GraphQL
|
|
74
|
+
GITLAB_CLIENT_PARAMS = set(inspect.signature(GitlabClient.__init__).parameters.keys()) - {"self"}
|
|
75
|
+
|
|
76
|
+
# Parameters accepted by python-gitlab's GraphQL.__init__
|
|
77
|
+
GRAPHQL_PARAMS = set(inspect.signature(GraphQL.__init__).parameters.keys()) - {"self"}
|
|
78
|
+
|
|
70
79
|
def __init__(self, gitlabform: GitLab):
|
|
71
|
-
url = gitlabform.url
|
|
72
|
-
token = gitlabform.token
|
|
73
|
-
ssl_verify = gitlabform.ssl_verify
|
|
74
|
-
timeout = gitlabform.timeout
|
|
75
80
|
session = gitlabform.session
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
graphql_kwargs = {k: v for k, v in gitlabform.gitlab_config.items() if k in self.GRAPHQL_PARAMS}
|
|
83
|
+
graphql = GraphQL(**graphql_kwargs)
|
|
84
|
+
|
|
85
|
+
default_gitlab_kwargs = {
|
|
86
|
+
"retry_transient_errors": True,
|
|
87
|
+
}
|
|
88
|
+
renamed_gitlab_kwargs = {
|
|
89
|
+
# Bandit is used for security scanning and it complains about 'private_token' being
|
|
90
|
+
# a hardcoded secret. However, in this case we are just renaming a config key
|
|
91
|
+
# provided by the user to match the parameter name expected by python-gitlab.
|
|
92
|
+
# Hence, we can safely ignore this code security warning here.
|
|
93
|
+
"token": "private_token", # nosec B105
|
|
94
|
+
}
|
|
95
|
+
extra_gitlab_kwargs = {
|
|
96
|
+
**default_gitlab_kwargs,
|
|
97
|
+
**{
|
|
98
|
+
k: v
|
|
99
|
+
for k, v in gitlabform.gitlab_config.items()
|
|
100
|
+
if k not in renamed_gitlab_kwargs and k in self.GITLAB_CLIENT_PARAMS
|
|
101
|
+
},
|
|
102
|
+
**{renamed_gitlab_kwargs[k]: v for k, v in gitlabform.gitlab_config.items() if k in renamed_gitlab_kwargs},
|
|
103
|
+
}
|
|
78
104
|
|
|
79
105
|
self._gitlab: PythonGitlab = PythonGitlab(
|
|
80
|
-
url=url,
|
|
81
|
-
private_token=token,
|
|
82
|
-
ssl_verify=ssl_verify,
|
|
83
|
-
timeout=timeout,
|
|
84
106
|
api_version="4",
|
|
85
|
-
retry_transient_errors=True,
|
|
86
107
|
graphql=graphql,
|
|
87
108
|
session=session,
|
|
109
|
+
**extra_gitlab_kwargs,
|
|
88
110
|
)
|
|
89
111
|
|
|
90
112
|
def get_gitlab(self):
|
|
@@ -22,32 +22,47 @@ class GitLabCore:
|
|
|
22
22
|
def __init__(self, config_path=None, config_string=None):
|
|
23
23
|
self.configuration = Configuration(config_path, config_string)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
default_gitlab_config = {
|
|
26
|
+
"url": os.getenv("GITLAB_URL"),
|
|
27
|
+
"token": os.getenv("GITLAB_TOKEN"),
|
|
28
|
+
"ssl_verify": True,
|
|
29
|
+
"timeout": 10,
|
|
30
|
+
"max_retries": 3,
|
|
31
|
+
"backoff_factor": 0.25,
|
|
32
|
+
"retry_transient_errors": True,
|
|
33
|
+
}
|
|
34
|
+
gitlab_config_from_file = self.configuration.get("gitlab", {})
|
|
35
|
+
self.gitlab_config = {**default_gitlab_config, **gitlab_config_from_file}
|
|
29
36
|
|
|
30
37
|
self.session = requests.Session()
|
|
31
38
|
|
|
39
|
+
retries_status_forcelist = []
|
|
40
|
+
if self.gitlab_config["retry_transient_errors"]:
|
|
41
|
+
# 429 Too Many Requests is included to handle rate limiting
|
|
42
|
+
# Ideally we would like to handle Rate Limiting retrys based on 'Retry-After' header
|
|
43
|
+
# As done in python-gitlab: https://github.com/python-gitlab/python-gitlab/blob/main/docs/api-usage-advanced.rst#rate-limits
|
|
44
|
+
# 5xx status codes are included to retry after transient server errors
|
|
45
|
+
retries_status_forcelist = [429, 500, 502, 503, 504] + list(range(520, 531))
|
|
46
|
+
|
|
32
47
|
retries = Retry(
|
|
33
|
-
total=
|
|
34
|
-
backoff_factor=
|
|
35
|
-
status_forcelist=
|
|
48
|
+
total=self.gitlab_config["max_retries"],
|
|
49
|
+
backoff_factor=self.gitlab_config["backoff_factor"],
|
|
50
|
+
status_forcelist=retries_status_forcelist,
|
|
36
51
|
)
|
|
37
52
|
|
|
38
53
|
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
39
54
|
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
40
55
|
|
|
41
|
-
self.session.verify = self.ssl_verify
|
|
42
|
-
if not self.ssl_verify:
|
|
56
|
+
self.session.verify = self.gitlab_config["ssl_verify"]
|
|
57
|
+
if not self.gitlab_config["ssl_verify"]:
|
|
43
58
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
44
59
|
|
|
45
60
|
self.gitlabform_version = package_version("gitlabform")
|
|
46
61
|
self.requests_version = package_version("requests")
|
|
47
62
|
self.session.headers.update(
|
|
48
63
|
{
|
|
49
|
-
"private-token": self.token,
|
|
50
|
-
"authorization": f"Bearer {self.token}",
|
|
64
|
+
"private-token": self.gitlab_config["token"],
|
|
65
|
+
"authorization": f"Bearer {self.gitlab_config['token']}",
|
|
51
66
|
"user-agent": f"GitLabForm/{self.gitlabform_version} (python-requests/{self.requests_version})",
|
|
52
67
|
}
|
|
53
68
|
)
|
|
@@ -178,15 +193,15 @@ class GitLabCore:
|
|
|
178
193
|
if dict_data and json_data:
|
|
179
194
|
raise Exception("You need to pass the data either as dict (dict_data) or JSON (json_data), not both!")
|
|
180
195
|
|
|
181
|
-
url = f"{self.url}/api/v4/{self._format_with_url_encoding(path_as_format_string, args)}"
|
|
196
|
+
url = f"{self.gitlab_config['url']}/api/v4/{self._format_with_url_encoding(path_as_format_string, args)}"
|
|
182
197
|
if dict_data:
|
|
183
|
-
response = self.session.request(method, url, data=dict_data, timeout=self.timeout)
|
|
198
|
+
response = self.session.request(method, url, data=dict_data, timeout=self.gitlab_config["timeout"])
|
|
184
199
|
debug(f"===> data = {to_str(dict_data)}")
|
|
185
200
|
elif json_data:
|
|
186
|
-
response = self.session.request(method, url, json=json_data, timeout=self.timeout)
|
|
201
|
+
response = self.session.request(method, url, json=json_data, timeout=self.gitlab_config["timeout"])
|
|
187
202
|
debug(f"===> json = {to_str(json_data)}")
|
|
188
203
|
else:
|
|
189
|
-
response = self.session.request(method, url, timeout=self.timeout)
|
|
204
|
+
response = self.session.request(method, url, timeout=self.gitlab_config["timeout"])
|
|
190
205
|
|
|
191
206
|
if response.status_code in expected_codes:
|
|
192
207
|
# if we accept error responses then they will likely not contain a JSON body
|
|
@@ -43,6 +43,7 @@ from gitlabform.processors.shared.protected_environments_processor import (
|
|
|
43
43
|
from gitlabform.processors.project.project_security_settings import (
|
|
44
44
|
ProjectSecuritySettingsProcessor,
|
|
45
45
|
)
|
|
46
|
+
from gitlabform.processors.project.remote_mirrors_processor import RemoteMirrorsProcessor
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
class ProjectProcessors(AbstractProcessors):
|
|
@@ -75,4 +76,5 @@ class ProjectProcessors(AbstractProcessors):
|
|
|
75
76
|
ProtectedEnvironmentsProcessor(gitlab),
|
|
76
77
|
MergeRequestsApprovals(gitlab),
|
|
77
78
|
MergeRequestsApprovalRules(gitlab),
|
|
79
|
+
RemoteMirrorsProcessor(gitlab),
|
|
78
80
|
]
|
|
@@ -189,10 +189,14 @@ class FilesProcessor(AbstractProcessor):
|
|
|
189
189
|
)
|
|
190
190
|
|
|
191
191
|
except GitlabUpdateError as e:
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
if (
|
|
193
|
+
e.response_code == 400 or e.response_code == 403
|
|
194
|
+
) and "You are not allowed to push into this branch" in e.error_message:
|
|
195
|
+
# If the project is archived, modifying files is not allowed
|
|
196
|
+
if project.archived:
|
|
197
|
+
fatal(f"Project is archived, cannot modify files in it.: {e.error_message}")
|
|
198
|
+
|
|
199
|
+
# Otherwise, unprotect the branch but only if we know how to protect it again
|
|
196
200
|
if configuration.get("branches|" + branch.name + "|protected"):
|
|
197
201
|
debug(f"> Temporarily unprotecting the branch to '{operation}' a file in it...")
|
|
198
202
|
# Delete operation on protected branch removes the protection only
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from typing import Any, cast, Dict, Set, List, Optional
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from cli_ui import debug as verbose, info_1 as info, warning
|
|
5
|
+
|
|
6
|
+
from gitlab.exceptions import GitlabCreateError, GitlabUpdateError, GitlabDeleteError
|
|
7
|
+
from gitlab.v4.objects import Project, ProjectRemoteMirror
|
|
8
|
+
from gitlabform.gitlab import GitLab
|
|
9
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RemoteMirrorsProcessor(AbstractProcessor):
|
|
13
|
+
"""
|
|
14
|
+
A processor for the "remote_mirrors" configuration section.
|
|
15
|
+
|
|
16
|
+
Allows creating, updating, and deleting remote mirrors (push mirrors) for a project.
|
|
17
|
+
|
|
18
|
+
GitLabForm follows "raw parameter passing" pattern, which means that any parameter
|
|
19
|
+
supported by the GitLab API for remote mirrors can be used here.
|
|
20
|
+
The URL containing credentials (if any) is used as the config key for each mirror.
|
|
21
|
+
All other attributes are passed as-is to the GitLab API.
|
|
22
|
+
See: https://docs.gitlab.com/ee/api/remote_mirrors.html
|
|
23
|
+
|
|
24
|
+
Additionally, the following GitLabForm-specific keys are supported:
|
|
25
|
+
* enforce: (boolean) If true, mirrors not defined in the config will be deleted. This is a global option, not per-mirror.
|
|
26
|
+
* print_details: (boolean) If true, prints the full details of all mirrors found in GitLab for the project. This is a global option.
|
|
27
|
+
* force_push: (boolean) If true, triggers an immediate push sync.
|
|
28
|
+
* force_update: (boolean) If true, forces an update call even if the config looks unchanged (useful for updating credentials).
|
|
29
|
+
* print_public_key: (boolean) If true, retrieves and prints the SSH public key for the mirror (if applicable).
|
|
30
|
+
* delete: (boolean) If true, deletes the specified mirror.
|
|
31
|
+
|
|
32
|
+
Configuration example:
|
|
33
|
+
|
|
34
|
+
remote_mirrors:
|
|
35
|
+
enforce: true
|
|
36
|
+
"https://username:password@example.com/gitlab/project.git":
|
|
37
|
+
enabled: true
|
|
38
|
+
auth_method: password
|
|
39
|
+
only_protected_branches: true
|
|
40
|
+
force_push: true
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, gitlab: GitLab):
|
|
44
|
+
super().__init__("remote_mirrors", gitlab)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _normalize_url_for_comparison(url: str) -> str:
|
|
48
|
+
"""Normalize URL for comparison by removing credentials.
|
|
49
|
+
|
|
50
|
+
Given a mirror URL for password-based authentication,
|
|
51
|
+
this method returns the corresponding URL without credentials.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
http://username:password@host/path.git -> http://host/path.git
|
|
55
|
+
|
|
56
|
+
This can be used to compare mirror URLs without credentials to
|
|
57
|
+
find matching mirrors.
|
|
58
|
+
"""
|
|
59
|
+
from urllib.parse import urlparse
|
|
60
|
+
|
|
61
|
+
parsed = urlparse(url)
|
|
62
|
+
# Remove credentials (user:pass@) from netloc
|
|
63
|
+
clean_netloc = parsed.netloc.split("@")[-1]
|
|
64
|
+
return parsed._replace(netloc=clean_netloc).geturl()
|
|
65
|
+
|
|
66
|
+
def _process_configuration(self, project_and_group: str, configuration: Dict[str, Any]) -> None:
|
|
67
|
+
project: Project = self.gl.get_project_by_path_cached(project_and_group)
|
|
68
|
+
|
|
69
|
+
# 1. PREPARATION & OPTIMIZATION
|
|
70
|
+
mirrors_in_gitlab: List[ProjectRemoteMirror] = project.remote_mirrors.list(get_all=True)
|
|
71
|
+
gitlab_mirrors_map: Dict[str, ProjectRemoteMirror] = {
|
|
72
|
+
self._normalize_url_for_comparison(m.url): cast(ProjectRemoteMirror, m) for m in mirrors_in_gitlab
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
mirrors_in_config: Dict[str, Any] = configuration.get("remote_mirrors", {}).copy()
|
|
76
|
+
|
|
77
|
+
# --- GLOBAL OPTIONS ---
|
|
78
|
+
enforce_mirrors: bool = mirrors_in_config.pop("enforce", False)
|
|
79
|
+
print_details: bool = mirrors_in_config.pop("print_details", False)
|
|
80
|
+
|
|
81
|
+
urls_to_keep: Set[str] = set()
|
|
82
|
+
|
|
83
|
+
# 2. PROCESS CONFIGURATION (Create / Update / Delete)
|
|
84
|
+
for mirror_url in sorted(mirrors_in_config.keys()):
|
|
85
|
+
mirror_settings: Dict[str, Any] = mirrors_in_config[mirror_url]
|
|
86
|
+
norm_url: str = self._normalize_url_for_comparison(mirror_url)
|
|
87
|
+
mirror_in_gitlab = gitlab_mirrors_map.get(norm_url)
|
|
88
|
+
|
|
89
|
+
# --- CASE: EXPLICIT DELETE ---
|
|
90
|
+
if mirror_settings.get("delete"):
|
|
91
|
+
if mirror_in_gitlab:
|
|
92
|
+
self._delete_remote_mirror(mirror_in_gitlab)
|
|
93
|
+
gitlab_mirrors_map.pop(norm_url, None)
|
|
94
|
+
else:
|
|
95
|
+
verbose(f"Skip deleting remote mirror '{norm_url}', because it doesn't exist")
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# --- CASE: CREATE OR UPDATE ---
|
|
99
|
+
urls_to_keep.add(norm_url)
|
|
100
|
+
|
|
101
|
+
# Prepare payload: Extract local-only options
|
|
102
|
+
mirror_payload: Dict[str, Any] = {"url": mirror_url, **mirror_settings}
|
|
103
|
+
force_push: bool = mirror_payload.pop("force_push", False)
|
|
104
|
+
force_update: bool = mirror_payload.pop("force_update", False)
|
|
105
|
+
print_public_key: bool = mirror_payload.pop("print_public_key", False)
|
|
106
|
+
mirror_payload.pop("delete", None)
|
|
107
|
+
|
|
108
|
+
if mirror_in_gitlab:
|
|
109
|
+
self._update_existing_mirror(project, mirror_in_gitlab, mirror_payload, norm_url, force_update)
|
|
110
|
+
else:
|
|
111
|
+
mirror_in_gitlab = self._create_new_mirror(project, mirror_payload, mirror_url)
|
|
112
|
+
|
|
113
|
+
if print_public_key and mirror_in_gitlab:
|
|
114
|
+
self._handle_public_key_display(project, mirror_in_gitlab, norm_url)
|
|
115
|
+
|
|
116
|
+
if force_push and mirror_in_gitlab:
|
|
117
|
+
self._sync_remote_mirror(mirror_in_gitlab)
|
|
118
|
+
|
|
119
|
+
# 3. ENFORCEMENT PHASE
|
|
120
|
+
if enforce_mirrors:
|
|
121
|
+
self._enforce_mirrors(gitlab_mirrors_map, urls_to_keep)
|
|
122
|
+
|
|
123
|
+
# 4. REPORTING PHASE (Final State)
|
|
124
|
+
if print_details:
|
|
125
|
+
# We fetch a fresh list to show the final state after all updates/syncs
|
|
126
|
+
final_mirrors: List[ProjectRemoteMirror] = project.remote_mirrors.list(get_all=True)
|
|
127
|
+
if not final_mirrors:
|
|
128
|
+
info("🔍 No remote mirrors found for this project.")
|
|
129
|
+
else:
|
|
130
|
+
info(f"📋 Final Remote Mirror Report for '{project_and_group}':")
|
|
131
|
+
for mirror in final_mirrors:
|
|
132
|
+
info(" " + "─" * 30) # Visual separator using a light line
|
|
133
|
+
self._report_mirror_details(mirror)
|
|
134
|
+
info(" " + "─" * 30)
|
|
135
|
+
|
|
136
|
+
def _handle_public_key_display(self, project: Project, mirror_obj: ProjectRemoteMirror, norm_url: str) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Retrieves and prints the SSH public key for a mirror.
|
|
139
|
+
GitLab only provides this for mirrors configured with 'ssh_public_key' auth.
|
|
140
|
+
"""
|
|
141
|
+
# Only attempt retrieval if the auth method supports it
|
|
142
|
+
if getattr(mirror_obj, "auth_method", None) != "ssh_public_key":
|
|
143
|
+
verbose(f"Skipping public key display for '{norm_url}': auth_method is not 'ssh_public_key'")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
public_key: Optional[str] = None
|
|
147
|
+
try:
|
|
148
|
+
# TODO: python-gitlab does not yet support retrieving the public key via
|
|
149
|
+
# ProjectRemoteMirror object (e.g., mirror_obj.get_public_key()).
|
|
150
|
+
# Switch to native method once supported in the library.
|
|
151
|
+
|
|
152
|
+
# Mypy fix: cast the union return type (dict | Response) to dict[str, Any]
|
|
153
|
+
response = cast(
|
|
154
|
+
Dict[str, Any],
|
|
155
|
+
project.manager.gitlab.http_get(f"/projects/{project.id}/remote_mirrors/{mirror_obj.id}/public_key"),
|
|
156
|
+
)
|
|
157
|
+
public_key = response.get("public_key")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
warning(f"Failed to retrieve SSH public key for mirror {norm_url}: {e}")
|
|
160
|
+
|
|
161
|
+
if public_key:
|
|
162
|
+
info(f"🔑 SSH Public Key for mirror '{norm_url}':")
|
|
163
|
+
info(public_key)
|
|
164
|
+
info("👆 This public key must be added to the target repository to authorize the mirror.")
|
|
165
|
+
info(
|
|
166
|
+
"Please consult the GitLab documentation on 'Repository Mirroring' for specific setup instructions for your target platform."
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
verbose(f"No public key available to display for mirror '{norm_url}'")
|
|
170
|
+
|
|
171
|
+
def _needs_update(self, existing_mirror: Dict[str, Any], config_payload: Dict[str, Any]) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Overrides the base comparison to handle GitLab's URL credential masking.
|
|
174
|
+
Normalization is applied so that 'user:pass@host' matches '*****:*****@host'.
|
|
175
|
+
"""
|
|
176
|
+
comparison_payload: Dict[str, Any] = config_payload.copy()
|
|
177
|
+
if "url" in comparison_payload:
|
|
178
|
+
comparison_payload["url"] = self._normalize_url_for_comparison(comparison_payload["url"])
|
|
179
|
+
|
|
180
|
+
existing_mirror_dict = existing_mirror.copy()
|
|
181
|
+
|
|
182
|
+
existing_mirror_dict["url"] = self._normalize_url_for_comparison(existing_mirror_dict.get("url", ""))
|
|
183
|
+
|
|
184
|
+
return super()._needs_update(existing_mirror_dict, comparison_payload)
|
|
185
|
+
|
|
186
|
+
def _update_existing_mirror(
|
|
187
|
+
self,
|
|
188
|
+
project: Project,
|
|
189
|
+
mirror_obj: ProjectRemoteMirror,
|
|
190
|
+
payload: Dict[str, Any],
|
|
191
|
+
norm_url: str,
|
|
192
|
+
force_update: bool,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Compares and updates an existing mirror if changed or if force_update is set."""
|
|
195
|
+
|
|
196
|
+
should_update: bool = force_update or self._needs_update(mirror_obj.asdict(), payload)
|
|
197
|
+
|
|
198
|
+
if should_update:
|
|
199
|
+
if force_update:
|
|
200
|
+
verbose(f"Mirror '{norm_url}' update is being forced via 'force_update' flag.")
|
|
201
|
+
|
|
202
|
+
verbose(f"Updating remote mirror '{norm_url}' with latest config")
|
|
203
|
+
try:
|
|
204
|
+
project.remote_mirrors.update(id=mirror_obj.id, new_data=payload)
|
|
205
|
+
verbose(f"Updated remote mirror '{norm_url}'")
|
|
206
|
+
|
|
207
|
+
if force_update:
|
|
208
|
+
info(
|
|
209
|
+
f"!!! REMINDER: 'force_update' was used for mirror '{norm_url}'. "
|
|
210
|
+
"Please remove this flag from your configuration to avoid unnecessary API calls in future runs."
|
|
211
|
+
)
|
|
212
|
+
except GitlabUpdateError as e:
|
|
213
|
+
warning(f"Failed to update remote mirror {norm_url}: {e}")
|
|
214
|
+
else:
|
|
215
|
+
verbose(f"Remote mirror '{norm_url}' remains unchanged")
|
|
216
|
+
|
|
217
|
+
def _create_new_mirror(
|
|
218
|
+
self, project: Project, payload: Dict[str, Any], raw_url: str
|
|
219
|
+
) -> Optional[ProjectRemoteMirror]:
|
|
220
|
+
"""Creates a new remote mirror and handles API errors."""
|
|
221
|
+
norm_url = self._normalize_url_for_comparison(raw_url)
|
|
222
|
+
verbose(f"Creating remote mirror '{norm_url}'")
|
|
223
|
+
try:
|
|
224
|
+
return cast(ProjectRemoteMirror, project.remote_mirrors.create(payload))
|
|
225
|
+
except GitlabCreateError as e:
|
|
226
|
+
warning(f"Failed to create remote mirror {norm_url}: {e}")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def _enforce_mirrors(self, gitlab_mirrors_map: Dict[str, ProjectRemoteMirror], urls_to_keep: Set[str]) -> None:
|
|
230
|
+
"""Deletes mirrors present in GitLab that are not in the configuration."""
|
|
231
|
+
for norm_url, gm in gitlab_mirrors_map.items():
|
|
232
|
+
if norm_url not in urls_to_keep:
|
|
233
|
+
verbose(f"Enforce: Deleting remote mirror '{gm.url}' as it is not in the configuration")
|
|
234
|
+
self._delete_remote_mirror(gm)
|
|
235
|
+
|
|
236
|
+
def _delete_remote_mirror(self, mirror: ProjectRemoteMirror) -> None:
|
|
237
|
+
"""Delete the given `ProjectRemoteMirror` and handle errors."""
|
|
238
|
+
verbose(f"Deleting remote mirror '{mirror.url}'")
|
|
239
|
+
try:
|
|
240
|
+
mirror.delete()
|
|
241
|
+
except GitlabDeleteError as e:
|
|
242
|
+
warning(
|
|
243
|
+
f"Failed to delete remote mirror id={getattr(mirror, 'id', None)} url={getattr(mirror, 'url', None)}: {e}"
|
|
244
|
+
)
|
|
245
|
+
verbose(f"Failed to delete remote mirror '{mirror.url}'")
|
|
246
|
+
|
|
247
|
+
def _sync_remote_mirror(self, mirror: ProjectRemoteMirror) -> None:
|
|
248
|
+
"""Trigger sync for remote mirror when `force_push` is requested."""
|
|
249
|
+
mirror_id = getattr(mirror, "id", None)
|
|
250
|
+
mirror_url = getattr(mirror, "url", None)
|
|
251
|
+
verbose(f"Attempting sync for remote mirror id={mirror_id} url={mirror_url}")
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
result = mirror.sync()
|
|
255
|
+
verbose(f"Triggered sync for remote mirror '{mirror_url}' result={result}")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
warning(f"Failed to trigger sync for remote mirror id={mirror_id} url={mirror_url}: {e}")
|
|
258
|
+
|
|
259
|
+
def _report_mirror_details(self, mirror: ProjectRemoteMirror) -> None:
|
|
260
|
+
"""Prints every attribute of the mirror object, one per line."""
|
|
261
|
+
|
|
262
|
+
mirror_data = mirror.asdict()
|
|
263
|
+
|
|
264
|
+
# Mapping statuses to helpful visual cues
|
|
265
|
+
status_icons = {
|
|
266
|
+
"finished": "✅",
|
|
267
|
+
"started": "⏳",
|
|
268
|
+
"scheduled": "📅",
|
|
269
|
+
"failed": "❌",
|
|
270
|
+
"none": "⚪",
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for key, value in sorted(mirror_data.items()):
|
|
274
|
+
if key == "update_status":
|
|
275
|
+
icon = status_icons.get(value, "❓")
|
|
276
|
+
info(f" - {key}: {icon} {value}")
|
|
277
|
+
elif key == "last_error" and value:
|
|
278
|
+
info(f" - {key}: ⚠️ {value}")
|
|
279
|
+
else:
|
|
280
|
+
info(f" - {key}: {value}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabform
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.7.0
|
|
4
4
|
Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
|
|
5
5
|
Author: Greg Dubicki and Contributors
|
|
6
6
|
Project-URL: Homepage, https://gitlabform.github.io/gitlabform/
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Version Control :: Git
|
|
|
20
20
|
Requires-Python: >=3.12.0
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: certifi==
|
|
23
|
+
Requires-Dist: certifi==2026.1.4
|
|
24
24
|
Requires-Dist: cli-ui==0.19.0
|
|
25
25
|
Requires-Dist: ez-yaml==1.2.0
|
|
26
26
|
Requires-Dist: Jinja2==3.1.6
|
|
@@ -28,24 +28,24 @@ Requires-Dist: luddite==1.0.4
|
|
|
28
28
|
Requires-Dist: MarkupSafe==3.0.3
|
|
29
29
|
Requires-Dist: mergedeep==1.3.4
|
|
30
30
|
Requires-Dist: packaging==25.0
|
|
31
|
-
Requires-Dist: python-gitlab==7.
|
|
32
|
-
Requires-Dist: python-gitlab[graphql]==7.
|
|
31
|
+
Requires-Dist: python-gitlab==7.1.0
|
|
32
|
+
Requires-Dist: python-gitlab[graphql]==7.1.0
|
|
33
33
|
Requires-Dist: requests==2.32.5
|
|
34
34
|
Requires-Dist: ruamel.yaml==0.17.21
|
|
35
|
-
Requires-Dist: types-requests==2.32.4.
|
|
36
|
-
Requires-Dist: types-setuptools==80.9.0.
|
|
35
|
+
Requires-Dist: types-requests==2.32.4.20260107
|
|
36
|
+
Requires-Dist: types-setuptools==80.9.0.20251223
|
|
37
37
|
Requires-Dist: yamlpath==3.8.2
|
|
38
38
|
Provides-Extra: test
|
|
39
|
-
Requires-Dist: coverage==7.
|
|
39
|
+
Requires-Dist: coverage==7.13.1; extra == "test"
|
|
40
40
|
Requires-Dist: cryptography==46.0.3; extra == "test"
|
|
41
41
|
Requires-Dist: deepdiff==8.6.1; extra == "test"
|
|
42
|
-
Requires-Dist: mypy==1.
|
|
42
|
+
Requires-Dist: mypy==1.19.1; extra == "test"
|
|
43
43
|
Requires-Dist: mypy-extensions==1.1.0; extra == "test"
|
|
44
44
|
Requires-Dist: pre-commit==2.21.0; extra == "test"
|
|
45
|
-
Requires-Dist: pytest==
|
|
45
|
+
Requires-Dist: pytest==9.0.2; extra == "test"
|
|
46
46
|
Requires-Dist: pytest-cov==7.0.0; extra == "test"
|
|
47
47
|
Requires-Dist: pytest-rerunfailures==16.1; extra == "test"
|
|
48
|
-
Requires-Dist: xkcdpass==1.
|
|
48
|
+
Requires-Dist: xkcdpass==1.30.0; extra == "test"
|
|
49
49
|
Provides-Extra: docs
|
|
50
50
|
Requires-Dist: mkdocs; extra == "docs"
|
|
51
51
|
Requires-Dist: mkdocs-material; extra == "docs"
|
|
@@ -72,6 +72,7 @@ gitlabform/processors/project/project_processor.py
|
|
|
72
72
|
gitlabform/processors/project/project_push_rules_processor.py
|
|
73
73
|
gitlabform/processors/project/project_security_settings.py
|
|
74
74
|
gitlabform/processors/project/project_settings_processor.py
|
|
75
|
+
gitlabform/processors/project/remote_mirrors_processor.py
|
|
75
76
|
gitlabform/processors/project/resource_groups_processor.py
|
|
76
77
|
gitlabform/processors/project/schedules_processor.py
|
|
77
78
|
gitlabform/processors/project/tags_processor.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
certifi==
|
|
1
|
+
certifi==2026.1.4
|
|
2
2
|
cli-ui==0.19.0
|
|
3
3
|
ez-yaml==1.2.0
|
|
4
4
|
Jinja2==3.1.6
|
|
@@ -6,12 +6,12 @@ luddite==1.0.4
|
|
|
6
6
|
MarkupSafe==3.0.3
|
|
7
7
|
mergedeep==1.3.4
|
|
8
8
|
packaging==25.0
|
|
9
|
-
python-gitlab==7.
|
|
10
|
-
python-gitlab[graphql]==7.
|
|
9
|
+
python-gitlab==7.1.0
|
|
10
|
+
python-gitlab[graphql]==7.1.0
|
|
11
11
|
requests==2.32.5
|
|
12
12
|
ruamel.yaml==0.17.21
|
|
13
|
-
types-requests==2.32.4.
|
|
14
|
-
types-setuptools==80.9.0.
|
|
13
|
+
types-requests==2.32.4.20260107
|
|
14
|
+
types-setuptools==80.9.0.20251223
|
|
15
15
|
yamlpath==3.8.2
|
|
16
16
|
|
|
17
17
|
[docs]
|
|
@@ -19,13 +19,13 @@ mkdocs
|
|
|
19
19
|
mkdocs-material
|
|
20
20
|
|
|
21
21
|
[test]
|
|
22
|
-
coverage==7.
|
|
22
|
+
coverage==7.13.1
|
|
23
23
|
cryptography==46.0.3
|
|
24
24
|
deepdiff==8.6.1
|
|
25
|
-
mypy==1.
|
|
25
|
+
mypy==1.19.1
|
|
26
26
|
mypy-extensions==1.1.0
|
|
27
27
|
pre-commit==2.21.0
|
|
28
|
-
pytest==
|
|
28
|
+
pytest==9.0.2
|
|
29
29
|
pytest-cov==7.0.0
|
|
30
30
|
pytest-rerunfailures==16.1
|
|
31
|
-
xkcdpass==1.
|
|
31
|
+
xkcdpass==1.30.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitlabform"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.7.0"
|
|
8
8
|
authors = [{ name = "Greg Dubicki and Contributors" }]
|
|
9
9
|
description = "🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML"
|
|
10
10
|
keywords = ["cli", "yaml", "gitlab", "configuration-as-code"]
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
requires-python = ">=3.12.0"
|
|
25
25
|
dependencies = [
|
|
26
|
-
"certifi==
|
|
26
|
+
"certifi==2026.1.4",
|
|
27
27
|
"cli-ui==0.19.0",
|
|
28
28
|
"ez-yaml==1.2.0",
|
|
29
29
|
"Jinja2==3.1.6",
|
|
@@ -31,12 +31,12 @@ dependencies = [
|
|
|
31
31
|
"MarkupSafe==3.0.3",
|
|
32
32
|
"mergedeep==1.3.4",
|
|
33
33
|
"packaging==25.0",
|
|
34
|
-
"python-gitlab==7.
|
|
35
|
-
"python-gitlab[graphql]==7.
|
|
34
|
+
"python-gitlab==7.1.0",
|
|
35
|
+
"python-gitlab[graphql]==7.1.0",
|
|
36
36
|
"requests==2.32.5",
|
|
37
37
|
"ruamel.yaml==0.17.21",
|
|
38
|
-
"types-requests==2.32.4.
|
|
39
|
-
"types-setuptools==80.9.0.
|
|
38
|
+
"types-requests==2.32.4.20260107",
|
|
39
|
+
"types-setuptools==80.9.0.20251223",
|
|
40
40
|
"yamlpath==3.8.2",
|
|
41
41
|
]
|
|
42
42
|
|
|
@@ -52,16 +52,16 @@ content-type = "text/markdown"
|
|
|
52
52
|
|
|
53
53
|
[project.optional-dependencies]
|
|
54
54
|
test = [
|
|
55
|
-
"coverage==7.
|
|
55
|
+
"coverage==7.13.1",
|
|
56
56
|
"cryptography==46.0.3",
|
|
57
57
|
"deepdiff==8.6.1",
|
|
58
|
-
"mypy==1.
|
|
58
|
+
"mypy==1.19.1",
|
|
59
59
|
"mypy-extensions==1.1.0",
|
|
60
60
|
"pre-commit==2.21.0",
|
|
61
|
-
"pytest==
|
|
61
|
+
"pytest==9.0.2",
|
|
62
62
|
"pytest-cov==7.0.0",
|
|
63
63
|
"pytest-rerunfailures==16.1",
|
|
64
|
-
"xkcdpass==1.
|
|
64
|
+
"xkcdpass==1.30.0",
|
|
65
65
|
]
|
|
66
66
|
docs = ["mkdocs", "mkdocs-material"]
|
|
67
67
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_ldap_links_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_members_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_push_rules_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_saml_links_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_settings_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/group/group_variables_processor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/deploy_keys_processor.py
RENAMED
|
File without changes
|
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/integrations_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/job_token_scope_processor.py
RENAMED
|
File without changes
|
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/merge_requests_approval_rules.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/merge_requests_approvals.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_labels_processor.py
RENAMED
|
File without changes
|
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_push_rules_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_security_settings.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/project_settings_processor.py
RENAMED
|
File without changes
|
{gitlabform-4.6.1 → gitlabform-4.7.0}/gitlabform/processors/project/resource_groups_processor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|