saltext.github 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. saltext/github/__init__.py +31 -0
  2. saltext/github/modules/__init__.py +0 -0
  3. saltext/github/modules/github.py +1873 -0
  4. saltext/github/modules/github_org.py +667 -0
  5. saltext/github/modules/github_repo.py +1658 -0
  6. saltext/github/modules/github_user.py +75 -0
  7. saltext/github/resources/__init__.py +0 -0
  8. saltext/github/resources/github/__init__.py +251 -0
  9. saltext/github/resources/github/modules/__init__.py +0 -0
  10. saltext/github/resources/github/modules/actions.py +175 -0
  11. saltext/github/resources/github/modules/config.py +69 -0
  12. saltext/github/resources/github/modules/deploy_key.py +122 -0
  13. saltext/github/resources/github/modules/environment.py +241 -0
  14. saltext/github/resources/github/modules/grains.py +18 -0
  15. saltext/github/resources/github/modules/issue.py +157 -0
  16. saltext/github/resources/github/modules/label.py +161 -0
  17. saltext/github/resources/github/modules/milestone.py +103 -0
  18. saltext/github/resources/github/modules/pages.py +149 -0
  19. saltext/github/resources/github/modules/pull.py +90 -0
  20. saltext/github/resources/github/modules/repo.py +155 -0
  21. saltext/github/resources/github/modules/ruleset.py +144 -0
  22. saltext/github/resources/github/modules/saltutil.py +24 -0
  23. saltext/github/resources/github/modules/state.py +116 -0
  24. saltext/github/resources/github/modules/test.py +29 -0
  25. saltext/github/resources/github/states/__init__.py +0 -0
  26. saltext/github/resources/github/states/actions.py +117 -0
  27. saltext/github/resources/github/states/deploy_key.py +130 -0
  28. saltext/github/resources/github/states/environment.py +99 -0
  29. saltext/github/resources/github/states/label.py +71 -0
  30. saltext/github/resources/github/states/pages.py +92 -0
  31. saltext/github/resources/github/states/repo.py +108 -0
  32. saltext/github/resources/github/states/ruleset.py +81 -0
  33. saltext/github/states/__init__.py +0 -0
  34. saltext/github/states/github.py +966 -0
  35. saltext/github/states/github_org.py +369 -0
  36. saltext/github/states/github_repo.py +722 -0
  37. saltext/github/utils/__init__.py +72 -0
  38. saltext/github/utils/gh/__init__.py +14 -0
  39. saltext/github/utils/gh/actions.py +195 -0
  40. saltext/github/utils/gh/cache.py +126 -0
  41. saltext/github/utils/gh/client.py +534 -0
  42. saltext/github/utils/gh/deploy_keys.py +121 -0
  43. saltext/github/utils/gh/environments.py +271 -0
  44. saltext/github/utils/gh/exceptions.py +28 -0
  45. saltext/github/utils/gh/grains_files.py +366 -0
  46. saltext/github/utils/gh/issues.py +193 -0
  47. saltext/github/utils/gh/labels.py +142 -0
  48. saltext/github/utils/gh/members.py +157 -0
  49. saltext/github/utils/gh/milestones.py +122 -0
  50. saltext/github/utils/gh/pages.py +128 -0
  51. saltext/github/utils/gh/profile.py +458 -0
  52. saltext/github/utils/gh/pulls.py +99 -0
  53. saltext/github/utils/gh/repos.py +465 -0
  54. saltext/github/utils/gh/resource.py +35 -0
  55. saltext/github/utils/gh/rulesets.py +191 -0
  56. saltext/github/utils/gh/statecores/__init__.py +70 -0
  57. saltext/github/utils/gh/statecores/actions.py +180 -0
  58. saltext/github/utils/gh/statecores/deploy_keys.py +241 -0
  59. saltext/github/utils/gh/statecores/environments.py +212 -0
  60. saltext/github/utils/gh/statecores/labels.py +116 -0
  61. saltext/github/utils/gh/statecores/pages.py +177 -0
  62. saltext/github/utils/gh/statecores/repo.py +249 -0
  63. saltext/github/utils/gh/statecores/rulesets.py +115 -0
  64. saltext/github/utils/gh/teams.py +375 -0
  65. saltext/github/utils/gh/users.py +87 -0
  66. saltext/github/version.py +1 -0
  67. saltext_github-1.1.0.dist-info/METADATA +151 -0
  68. saltext_github-1.1.0.dist-info/RECORD +73 -0
  69. saltext_github-1.1.0.dist-info/WHEEL +5 -0
  70. saltext_github-1.1.0.dist-info/entry_points.txt +2 -0
  71. saltext_github-1.1.0.dist-info/licenses/LICENSE +201 -0
  72. saltext_github-1.1.0.dist-info/licenses/NOTICE +10 -0
  73. saltext_github-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,70 @@
1
+ """
2
+ Shared cores implementing the repository-scoped states: the full
3
+ inspect -> test-gate -> apply flow, including change reporting.
4
+
5
+ They are consumed by the classic :py:mod:`github_repo
6
+ <saltext.github.states.github_repo>` state module as well as the
7
+ ``github`` resource type's state modules, which are both thin wrappers
8
+ handling identity and connection resolution.
9
+
10
+ The cores return complete state return dictionaries (``name``,
11
+ ``changes``, ``result``, ``comment``) and raise
12
+ :py:class:`CommandExecutionError <salt.exceptions.CommandExecutionError>`
13
+ (usually the :py:mod:`gh <saltext.github.utils.gh>` subclasses) on
14
+ failures, which the wrappers translate into a failed state return.
15
+ """
16
+
17
+ import functools
18
+
19
+ from salt.exceptions import CommandExecutionError
20
+
21
+
22
+ class StateError(CommandExecutionError):
23
+ """
24
+ Raised when applying state changes failed. Compound states can
25
+ attach the changes that were applied before the failure, which
26
+ :func:`failed`/:func:`catch_errors` include in the state return.
27
+ """
28
+
29
+ def __init__(self, message, *, changes=None):
30
+ super().__init__(message)
31
+ self.changes = changes or {}
32
+
33
+
34
+ def failed(name, err):
35
+ """
36
+ Render a raised error into a failed state return, including
37
+ partially applied changes carried by the error (:class:`StateError`).
38
+ """
39
+ return {
40
+ "name": name,
41
+ "changes": getattr(err, "changes", None) or {},
42
+ "result": False,
43
+ "comment": str(err),
44
+ }
45
+
46
+
47
+ def catch_errors(func):
48
+ """
49
+ Wrap a state function to render raised
50
+ :class:`CommandExecutionError <salt.exceptions.CommandExecutionError>`
51
+ into a failed state return instead of a traceback.
52
+ """
53
+
54
+ @functools.wraps(func)
55
+ def wrapper(name, *args, **kwargs):
56
+ try:
57
+ return func(name, *args, **kwargs)
58
+ except CommandExecutionError as err:
59
+ return failed(name, err)
60
+
61
+ return wrapper
62
+
63
+
64
+ def differs(current, desired):
65
+ """
66
+ Report whether a managed parameter needs updating. The identity check
67
+ makes boolean comparisons exact: since ``True == 1`` in Python, an
68
+ equality check alone would not register e.g. ``1`` -> ``True``.
69
+ """
70
+ return current is not desired and current != desired
@@ -0,0 +1,180 @@
1
+ """
2
+ State cores managing repository Actions secrets and workflow permissions.
3
+ """
4
+
5
+ import logging
6
+
7
+ from salt.exceptions import CommandExecutionError
8
+
9
+ from saltext.github.utils import filter_unset
10
+ from saltext.github.utils.gh import actions as gh_actions
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ def secret_present(conn, name, repo_name, *, value=None, force=False, owner=None, test=False):
16
+ """
17
+ Ensure an Actions secret is present in a repository.
18
+
19
+ Secret values are write-only in the GitHub API, so this core cannot
20
+ detect value drift. By default, an existing secret is left untouched.
21
+ Set ``force`` to push the value on every run.
22
+
23
+ conn
24
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
25
+
26
+ name
27
+ Name of the secret. GitHub treats secret names
28
+ case-insensitively.
29
+
30
+ repo_name
31
+ Name of the repository.
32
+
33
+ value
34
+ Plaintext secret value. Required when the secret does not exist
35
+ yet or ``force`` is set.
36
+
37
+ force
38
+ Push the value even when the secret already exists.
39
+ Defaults to false.
40
+
41
+ owner
42
+ Owner of the repository. Defaults to the connection's owner.
43
+
44
+ test
45
+ Only project the changes without applying them (``test`` mode).
46
+ """
47
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
48
+
49
+ secrets = [sec["name"] for sec in gh_actions.list_secrets(conn, repo_name, owner=owner)]
50
+ current = next((sec for sec in secrets if sec.lower() == name.lower()), None)
51
+
52
+ if current is not None and not force:
53
+ ret["comment"] = (
54
+ f"Secret {name} is already present. Note that secret values are "
55
+ "write-only and cannot be verified, set force to push the value."
56
+ )
57
+ return ret
58
+
59
+ if value is None:
60
+ raise CommandExecutionError(f"A value is required to set secret {name}")
61
+
62
+ if current is None:
63
+ ret["changes"]["created"] = name
64
+ verb = "created"
65
+ else:
66
+ ret["changes"]["updated"] = name
67
+ verb = "updated"
68
+
69
+ if test:
70
+ ret["result"] = None
71
+ ret["comment"] = f"Secret {name} would have been {verb}"
72
+ return ret
73
+
74
+ gh_actions.set_secret(conn, repo_name, name, value, owner=owner)
75
+ ret["comment"] = f"Secret {name} has been {verb}"
76
+ return ret
77
+
78
+
79
+ def secret_absent(conn, name, repo_name, *, owner=None, test=False):
80
+ """
81
+ Ensure an Actions secret is absent from a repository.
82
+
83
+ conn
84
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
85
+
86
+ name
87
+ Name of the secret. GitHub treats secret names
88
+ case-insensitively.
89
+
90
+ repo_name
91
+ Name of the repository.
92
+
93
+ owner
94
+ Owner of the repository. Defaults to the connection's owner.
95
+
96
+ test
97
+ Only project the changes without applying them (``test`` mode).
98
+ """
99
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
100
+
101
+ secrets = [sec["name"] for sec in gh_actions.list_secrets(conn, repo_name, owner=owner)]
102
+ current = next((sec for sec in secrets if sec.lower() == name.lower()), None)
103
+ if current is None:
104
+ ret["comment"] = f"Secret {name} is already absent"
105
+ return ret
106
+
107
+ ret["changes"]["deleted"] = current
108
+ if test:
109
+ ret["result"] = None
110
+ ret["comment"] = f"Secret {name} would have been removed"
111
+ return ret
112
+
113
+ gh_actions.delete_secret(conn, repo_name, current, owner=owner)
114
+ ret["comment"] = f"Secret {name} has been removed"
115
+ return ret
116
+
117
+
118
+ def workflow_permissions_managed(
119
+ conn,
120
+ name,
121
+ repo_name,
122
+ *,
123
+ default_workflow_permissions=None,
124
+ can_approve_pull_request_reviews=None,
125
+ owner=None,
126
+ test=False,
127
+ ):
128
+ """
129
+ Manage the default workflow permissions granted to the ``GITHUB_TOKEN``
130
+ in a repository.
131
+
132
+ conn
133
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
134
+
135
+ name
136
+ State name (reported back unchanged).
137
+
138
+ repo_name
139
+ Name of the repository.
140
+
141
+ default_workflow_permissions
142
+ Default permissions for the ``GITHUB_TOKEN``, either ``read``
143
+ or ``write``.
144
+
145
+ can_approve_pull_request_reviews
146
+ Allow GitHub Actions to approve pull requests.
147
+
148
+ owner
149
+ Owner of the repository. Defaults to the connection's owner.
150
+
151
+ test
152
+ Only project the changes without applying them (``test`` mode).
153
+ """
154
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
155
+
156
+ desired = filter_unset(
157
+ {
158
+ "default_workflow_permissions": default_workflow_permissions,
159
+ "can_approve_pull_request_reviews": can_approve_pull_request_reviews,
160
+ }
161
+ )
162
+ if not desired:
163
+ raise CommandExecutionError("At least one setting to manage must be specified")
164
+
165
+ current = gh_actions.get_workflow_permissions(conn, repo_name, owner=owner)
166
+ diff = {key: val for key, val in desired.items() if current.get(key) != val}
167
+ if not diff:
168
+ ret["comment"] = "Workflow permissions are already set as specified"
169
+ return ret
170
+
171
+ ret["changes"]["old"] = {key: current.get(key) for key in diff}
172
+ ret["changes"]["new"] = diff
173
+ if test:
174
+ ret["result"] = None
175
+ ret["comment"] = "Workflow permissions would have been updated"
176
+ return ret
177
+
178
+ gh_actions.set_workflow_permissions(conn, repo_name, owner=owner, **diff)
179
+ ret["comment"] = "Workflow permissions have been updated"
180
+ return ret
@@ -0,0 +1,241 @@
1
+ """
2
+ State core managing repository deploy keys.
3
+ """
4
+
5
+ import logging
6
+
7
+ from salt.exceptions import CommandExecutionError
8
+
9
+ from saltext.github.utils.gh import actions as gh_actions
10
+ from saltext.github.utils.gh import deploy_keys as gh_deploy_keys
11
+ from saltext.github.utils.gh.statecores import StateError
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def present(conn, name, key, repo_name, *, read_only=False, owner=None, test=False):
17
+ """
18
+ Ensure a deploy key with this title and public key is present
19
+ in a repository.
20
+
21
+ Since deploy keys are immutable in the GitHub API, a key whose
22
+ ``key`` or ``read_only`` setting differs is replaced (removed
23
+ and recreated).
24
+
25
+ conn
26
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
27
+
28
+ name
29
+ Display title of the deploy key.
30
+
31
+ key
32
+ SSH public key.
33
+
34
+ repo_name
35
+ Name of the repository.
36
+
37
+ read_only
38
+ Deny write access to the repository for this key. Defaults to false.
39
+
40
+ owner
41
+ Owner of the repository. Defaults to the connection's owner.
42
+
43
+ test
44
+ Only project the changes without applying them (``test`` mode).
45
+ """
46
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
47
+
48
+ keys = gh_deploy_keys.list_all(conn, repo_name, owner=owner)
49
+ current = next((cur for cur in keys if cur["title"] == name), None)
50
+
51
+ if current is not None:
52
+ if gh_deploy_keys.key_body(current["key"]) == gh_deploy_keys.key_body(key) and bool(
53
+ current["read_only"]
54
+ ) is bool(read_only):
55
+ ret["comment"] = f"Deploy key {name} is present as specified"
56
+ return ret
57
+ ret["changes"]["old"] = {"key": current["key"], "read_only": current["read_only"]}
58
+ ret["changes"]["new"] = {"key": gh_deploy_keys.key_body(key), "read_only": bool(read_only)}
59
+ else:
60
+ ret["changes"]["created"] = name
61
+
62
+ verb = "added" if current is None else "replaced"
63
+ if test:
64
+ ret["result"] = None
65
+ ret["comment"] = f"Deploy key {name} would have been {verb}"
66
+ return ret
67
+
68
+ if current is not None:
69
+ gh_deploy_keys.delete(conn, repo_name, current["id"], owner=owner)
70
+ gh_deploy_keys.create(conn, repo_name, name, key, read_only=bool(read_only), owner=owner)
71
+ ret["comment"] = f"Deploy key {name} has been {verb}"
72
+ return ret
73
+
74
+
75
+ def actions_present(
76
+ conn,
77
+ name,
78
+ secret_name,
79
+ repo_name,
80
+ *,
81
+ keygen,
82
+ read_only=False,
83
+ rotate=False,
84
+ owner=None,
85
+ test=False,
86
+ ):
87
+ """
88
+ Ensure a generated SSH keypair is distributed to a repository:
89
+ the public half as a deploy key, the private half as an Actions
90
+ secret. The keypair is generated in memory and never persisted;
91
+ only the public key's fingerprint is reported.
92
+
93
+ Since secret values are write-only in the GitHub API, an existing
94
+ pair cannot be verified to match and is left untouched unless
95
+ ``rotate`` is set. A pair with a missing half is regenerated.
96
+
97
+ conn
98
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
99
+
100
+ name
101
+ Display title of the deploy key.
102
+
103
+ secret_name
104
+ Name of the Actions secret holding the private key. GitHub
105
+ treats secret names case-insensitively.
106
+
107
+ repo_name
108
+ Name of the repository.
109
+
110
+ keygen
111
+ Callable generating the keypair, returning a mapping with
112
+ ``private_key`` (OpenSSH PEM format) and ``public_key``
113
+ (OpenSSH single-line format), like
114
+ :py:func:`ssh_pki.create_private_key
115
+ <salt.modules.ssh_pki.create_private_key>` does. Only invoked
116
+ when a new pair needs to be distributed.
117
+
118
+ read_only
119
+ Deny write access to the repository for this key. Defaults to false.
120
+
121
+ rotate
122
+ Generate and distribute a new keypair even when both halves
123
+ are present. Defaults to false.
124
+
125
+ owner
126
+ Owner of the repository. Defaults to the connection's owner.
127
+
128
+ test
129
+ Only project the changes without applying them (``test`` mode).
130
+ """
131
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
132
+
133
+ keys = gh_deploy_keys.list_all(conn, repo_name, owner=owner)
134
+ current = next((cur for cur in keys if cur["title"] == name), None)
135
+ secrets = [sec["name"] for sec in gh_actions.list_secrets(conn, repo_name, owner=owner)]
136
+ current_secret = next((sec for sec in secrets if sec.lower() == secret_name.lower()), None)
137
+
138
+ if current is not None and current_secret is not None and not rotate:
139
+ if bool(current["read_only"]) is bool(read_only):
140
+ ret["comment"] = (
141
+ f"Deploy key {name} and secret {secret_name} are present. Note that "
142
+ "the private key cannot be verified to match the deploy key, "
143
+ "set rotate to regenerate the pair."
144
+ )
145
+ return ret
146
+ # Deploy keys are immutable, but the read_only flag can be
147
+ # flipped by re-adding the known public key. The secret is
148
+ # left untouched since the pair still matches.
149
+ ret["changes"]["old"] = {"read_only": current["read_only"]}
150
+ ret["changes"]["new"] = {"read_only": bool(read_only)}
151
+ if test:
152
+ ret["result"] = None
153
+ ret["comment"] = f"Deploy key {name} would have been replaced"
154
+ return ret
155
+ applied = {}
156
+ try:
157
+ gh_deploy_keys.delete(conn, repo_name, current["id"], owner=owner)
158
+ applied["deleted"] = name
159
+ gh_deploy_keys.create(
160
+ conn, repo_name, name, current["key"], read_only=bool(read_only), owner=owner
161
+ )
162
+ except CommandExecutionError as err:
163
+ raise StateError(str(err), changes=applied) from err
164
+ ret["comment"] = f"Deploy key {name} has been replaced"
165
+ return ret
166
+
167
+ key_verb = "created" if current is None else "replaced"
168
+ secret_verb = "created" if current_secret is None else "updated"
169
+ ret["changes"]["deploy_key"] = {key_verb: name}
170
+ ret["changes"]["secret"] = {secret_verb: secret_name}
171
+
172
+ if test:
173
+ ret["result"] = None
174
+ ret["comment"] = (
175
+ f"A keypair would have been generated and distributed as "
176
+ f"deploy key {name} and secret {secret_name}"
177
+ )
178
+ return ret
179
+
180
+ pair = keygen()
181
+ private, public = pair["private_key"], f"{pair['public_key'].strip()} {name}"
182
+ ret["changes"]["deploy_key"]["fingerprint"] = gh_deploy_keys.fingerprint(public)
183
+
184
+ # Remove a stale deploy key before pushing the new pair: if a later
185
+ # step fails, the missing deploy key makes the next run regenerate
186
+ # the pair. Other orderings can leave a mismatched key and secret
187
+ # both present, which is undetectable afterwards.
188
+ applied = {}
189
+ try:
190
+ if current is not None:
191
+ gh_deploy_keys.delete(conn, repo_name, current["id"], owner=owner)
192
+ applied["deploy_key"] = {"deleted": name}
193
+ gh_actions.set_secret(conn, repo_name, secret_name, private, owner=owner)
194
+ applied["secret"] = ret["changes"]["secret"]
195
+ gh_deploy_keys.create(conn, repo_name, name, public, read_only=bool(read_only), owner=owner)
196
+ except CommandExecutionError as err:
197
+ raise StateError(str(err), changes=applied) from err
198
+
199
+ ret["comment"] = (
200
+ f"A keypair has been generated and distributed as "
201
+ f"deploy key {name} and secret {secret_name}"
202
+ )
203
+ return ret
204
+
205
+
206
+ def absent(conn, name, repo_name, *, owner=None, test=False):
207
+ """
208
+ Ensure no deploy key with this title is present in a repository.
209
+
210
+ conn
211
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
212
+
213
+ name
214
+ Display title of the deploy key.
215
+
216
+ repo_name
217
+ Name of the repository.
218
+
219
+ owner
220
+ Owner of the repository. Defaults to the connection's owner.
221
+
222
+ test
223
+ Only project the changes without applying them (``test`` mode).
224
+ """
225
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
226
+
227
+ keys = gh_deploy_keys.list_all(conn, repo_name, owner=owner)
228
+ current = next((cur for cur in keys if cur["title"] == name), None)
229
+ if current is None:
230
+ ret["comment"] = f"Deploy key {name} is already absent"
231
+ return ret
232
+
233
+ ret["changes"]["deleted"] = name
234
+ if test:
235
+ ret["result"] = None
236
+ ret["comment"] = f"Deploy key {name} would have been removed"
237
+ return ret
238
+
239
+ gh_deploy_keys.delete(conn, repo_name, current["id"], owner=owner)
240
+ ret["comment"] = f"Deploy key {name} has been removed"
241
+ return ret
@@ -0,0 +1,212 @@
1
+ """
2
+ State core managing repository (deployment) environments.
3
+ """
4
+
5
+ import logging
6
+
7
+ from salt.exceptions import CommandExecutionError
8
+
9
+ from saltext.github.utils import filter_unset
10
+ from saltext.github.utils.gh import environments as gh_environments
11
+ from saltext.github.utils.gh.exceptions import GitHubNotFoundError
12
+ from saltext.github.utils.gh.statecores import StateError
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ def _environment_settings_diff(ret, current_settings, desired):
18
+ """
19
+ Record an existing environment's settings that need updating in
20
+ ``ret`` and return whether there are any.
21
+ """
22
+ diff = {}
23
+ for key, val in desired.items():
24
+ current_val = current_settings.get(key)
25
+ if key == "reviewers":
26
+ matches = sorted((reviewer["type"], reviewer["id"]) for reviewer in val) == sorted(
27
+ (reviewer["type"], reviewer["id"]) for reviewer in current_val
28
+ )
29
+ else:
30
+ matches = current_val == val
31
+ if not matches:
32
+ diff[key] = val
33
+ if diff:
34
+ ret["changes"]["old"] = {key: current_settings.get(key) for key in diff}
35
+ ret["changes"]["new"] = diff
36
+ return bool(diff)
37
+
38
+
39
+ def present(
40
+ conn,
41
+ name,
42
+ repo_name,
43
+ *,
44
+ wait_timer=None,
45
+ prevent_self_review=None,
46
+ reviewers=None,
47
+ deployment_branches=None,
48
+ owner=None,
49
+ test=False,
50
+ ):
51
+ """
52
+ Ensure an environment is present in a repository and configured
53
+ as specified. Unspecified settings are left unmanaged.
54
+
55
+ conn
56
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
57
+
58
+ name
59
+ Name of the environment.
60
+
61
+ repo_name
62
+ Name of the repository.
63
+
64
+ wait_timer
65
+ Amount of time to delay a job after it is initially triggered,
66
+ in minutes.
67
+
68
+ prevent_self_review
69
+ Prevent a user who created a job from approving it.
70
+
71
+ reviewers
72
+ People or teams that may review jobs that reference the
73
+ environment, as a list of mappings with ``type`` (``User`` or
74
+ ``Team``) and ``id``.
75
+
76
+ deployment_branches
77
+ Which branches can deploy to this environment. Either ``all``,
78
+ ``protected`` (branches with branch protection rules) or a list
79
+ of branch name patterns.
80
+
81
+ owner
82
+ Owner of the repository. Defaults to the connection's owner.
83
+
84
+ test
85
+ Only project the changes without applying them (``test`` mode).
86
+ """
87
+ ret = {
88
+ "name": name,
89
+ "changes": {},
90
+ "result": True,
91
+ "comment": f"Environment {name} is configured as specified",
92
+ }
93
+
94
+ try:
95
+ current = gh_environments.get(conn, repo_name, name, owner=owner)
96
+ except GitHubNotFoundError:
97
+ current = None
98
+
99
+ desired = filter_unset(
100
+ {
101
+ "wait_timer": wait_timer,
102
+ "prevent_self_review": prevent_self_review,
103
+ "reviewers": reviewers,
104
+ }
105
+ )
106
+ if deployment_branches is not None:
107
+ desired["deployment_branch_policy"] = gh_environments.branch_policy_from_patterns(
108
+ deployment_branches
109
+ )
110
+
111
+ settings_diff = False
112
+ branches_added, branches_removed, current_policies = [], [], []
113
+ if current is None:
114
+ ret["changes"]["created"] = name
115
+ set_kwargs = desired
116
+ if isinstance(deployment_branches, list):
117
+ branches_added = sorted(set(deployment_branches))
118
+ else:
119
+ current_settings = gh_environments.normalize(current)
120
+ settings_diff = _environment_settings_diff(ret, current_settings, desired)
121
+ # The API call replaces the whole configuration, so settings
122
+ # that are not managed here need to be carried over.
123
+ set_kwargs = {**current_settings, **desired}
124
+ if isinstance(deployment_branches, list):
125
+ try:
126
+ current_policies = gh_environments.list_branch_policies(
127
+ conn, repo_name, name, owner=owner
128
+ )
129
+ except GitHubNotFoundError:
130
+ # Not found when the current policy type is not "custom"
131
+ current_policies = []
132
+ current_patterns = {policy["name"] for policy in current_policies}
133
+ branches_added = sorted(set(deployment_branches) - current_patterns)
134
+ branches_removed = sorted(current_patterns - set(deployment_branches))
135
+
136
+ if branches_added or branches_removed:
137
+ ret["changes"]["deployment_branches"] = {
138
+ "added": branches_added,
139
+ "removed": branches_removed,
140
+ }
141
+
142
+ if not ret["changes"]:
143
+ return ret
144
+
145
+ verb = "created" if current is None else "updated"
146
+ if test:
147
+ ret["result"] = None
148
+ ret["comment"] = f"Environment {name} would have been {verb}"
149
+ return ret
150
+
151
+ applied = {}
152
+ try:
153
+ if current is None or settings_diff:
154
+ gh_environments.set_(conn, repo_name, name, owner=owner, **set_kwargs)
155
+ if current is None:
156
+ applied["created"] = name
157
+ else:
158
+ applied["old"] = ret["changes"]["old"]
159
+ applied["new"] = ret["changes"]["new"]
160
+ for pattern in branches_added:
161
+ gh_environments.create_branch_policy(conn, repo_name, name, pattern, owner=owner)
162
+ applied.setdefault("deployment_branches", {}).setdefault("added", []).append(pattern)
163
+ for policy in current_policies:
164
+ if policy["name"] in branches_removed:
165
+ gh_environments.delete_branch_policy(
166
+ conn, repo_name, name, policy["id"], owner=owner
167
+ )
168
+ applied.setdefault("deployment_branches", {}).setdefault("removed", []).append(
169
+ policy["name"]
170
+ )
171
+ except CommandExecutionError as err:
172
+ raise StateError(str(err), changes=applied) from err
173
+ ret["comment"] = f"Environment {name} has been {verb}"
174
+ return ret
175
+
176
+
177
+ def absent(conn, name, repo_name, *, owner=None, test=False):
178
+ """
179
+ Ensure an environment is absent from a repository.
180
+
181
+ conn
182
+ :class:`~saltext.github.utils.gh.client.GitHubConnection` to use.
183
+
184
+ name
185
+ Name of the environment.
186
+
187
+ repo_name
188
+ Name of the repository.
189
+
190
+ owner
191
+ Owner of the repository. Defaults to the connection's owner.
192
+
193
+ test
194
+ Only project the changes without applying them (``test`` mode).
195
+ """
196
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
197
+
198
+ try:
199
+ gh_environments.get(conn, repo_name, name, owner=owner)
200
+ except GitHubNotFoundError:
201
+ ret["comment"] = f"Environment {name} is already absent"
202
+ return ret
203
+
204
+ ret["changes"]["deleted"] = name
205
+ if test:
206
+ ret["result"] = None
207
+ ret["comment"] = f"Environment {name} would have been removed"
208
+ return ret
209
+
210
+ gh_environments.delete(conn, repo_name, name, owner=owner)
211
+ ret["comment"] = f"Environment {name} has been removed"
212
+ return ret