winipedia-utils 0.3.41__py3-none-any.whl → 0.4.12__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.
Potentially problematic release.
This version of winipedia-utils might be problematic. Click here for more details.
- winipedia_utils/git/github/repo/__init__.py +1 -0
- winipedia_utils/git/github/repo/protect.py +104 -0
- winipedia_utils/git/github/repo/repo.py +205 -0
- winipedia_utils/git/github/workflows/base/__init__.py +1 -0
- winipedia_utils/git/{workflows → github/workflows}/base/base.py +95 -51
- winipedia_utils/git/github/workflows/health_check.py +55 -0
- winipedia_utils/git/{workflows → github/workflows}/publish.py +11 -8
- winipedia_utils/git/github/workflows/release.py +45 -0
- winipedia_utils/git/gitignore/config.py +49 -29
- winipedia_utils/git/gitignore/gitignore.py +1 -1
- winipedia_utils/git/pre_commit/config.py +18 -13
- winipedia_utils/git/pre_commit/hooks.py +22 -4
- winipedia_utils/git/pre_commit/run_hooks.py +2 -1
- winipedia_utils/iterating/iterate.py +3 -4
- winipedia_utils/modules/module.py +2 -0
- winipedia_utils/modules/package.py +2 -1
- winipedia_utils/projects/poetry/config.py +74 -36
- winipedia_utils/projects/project.py +2 -2
- winipedia_utils/setup.py +2 -0
- winipedia_utils/testing/config.py +83 -29
- winipedia_utils/testing/tests/base/fixtures/fixture.py +36 -0
- winipedia_utils/testing/tests/base/fixtures/scopes/module.py +6 -5
- winipedia_utils/testing/tests/base/fixtures/scopes/session.py +7 -8
- winipedia_utils/testing/tests/base/utils/utils.py +43 -2
- winipedia_utils/text/config.py +84 -37
- {winipedia_utils-0.3.41.dist-info → winipedia_utils-0.4.12.dist-info}/METADATA +23 -8
- {winipedia_utils-0.3.41.dist-info → winipedia_utils-0.4.12.dist-info}/RECORD +31 -27
- winipedia_utils/git/workflows/health_check.py +0 -51
- winipedia_utils/git/workflows/release.py +0 -33
- /winipedia_utils/git/{workflows/base → github}/__init__.py +0 -0
- /winipedia_utils/git/{workflows → github/workflows}/__init__.py +0 -0
- {winipedia_utils-0.3.41.dist-info → winipedia_utils-0.4.12.dist-info}/WHEEL +0 -0
- {winipedia_utils-0.3.41.dist-info → winipedia_utils-0.4.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Script to protect the repo and branches of a repository."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from winipedia_utils.git.github.repo.repo import (
|
|
6
|
+
DEFAULT_BRANCH,
|
|
7
|
+
DEFAULT_RULESET_NAME,
|
|
8
|
+
create_or_update_ruleset,
|
|
9
|
+
get_repo,
|
|
10
|
+
get_rules_payload,
|
|
11
|
+
)
|
|
12
|
+
from winipedia_utils.git.github.workflows.health_check import HealthCheckWorkflow
|
|
13
|
+
from winipedia_utils.modules.package import get_src_package
|
|
14
|
+
from winipedia_utils.projects.poetry.config import PyprojectConfigFile
|
|
15
|
+
from winipedia_utils.testing.tests.base.utils.utils import get_github_repo_token
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def protect_repository() -> None:
|
|
19
|
+
"""Protect the repository."""
|
|
20
|
+
set_secure_repo_settings()
|
|
21
|
+
create_or_update_default_branch_ruleset()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_secure_repo_settings() -> None:
|
|
25
|
+
"""Set standard settings for the repository."""
|
|
26
|
+
src_pkg_name = get_src_package().__name__
|
|
27
|
+
owner = PyprojectConfigFile.get_main_author_name()
|
|
28
|
+
token = get_github_repo_token()
|
|
29
|
+
repo = get_repo(token, owner, src_pkg_name)
|
|
30
|
+
|
|
31
|
+
toml_description = PyprojectConfigFile.load()["project"]["description"]
|
|
32
|
+
|
|
33
|
+
repo.edit(
|
|
34
|
+
name=src_pkg_name,
|
|
35
|
+
description=toml_description,
|
|
36
|
+
default_branch=DEFAULT_BRANCH,
|
|
37
|
+
delete_branch_on_merge=True,
|
|
38
|
+
allow_update_branch=True,
|
|
39
|
+
allow_merge_commit=False,
|
|
40
|
+
allow_rebase_merge=True,
|
|
41
|
+
allow_squash_merge=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_or_update_default_branch_ruleset() -> None:
|
|
46
|
+
"""Add a branch protection rule to the repository."""
|
|
47
|
+
create_or_update_ruleset(
|
|
48
|
+
**get_default_ruleset_params(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_default_ruleset_params() -> dict[str, Any]:
|
|
53
|
+
"""Get the default ruleset parameters."""
|
|
54
|
+
src_pkg_name = get_src_package().__name__
|
|
55
|
+
token = get_github_repo_token()
|
|
56
|
+
|
|
57
|
+
rules = get_rules_payload(
|
|
58
|
+
deletion={},
|
|
59
|
+
non_fast_forward={},
|
|
60
|
+
creation={},
|
|
61
|
+
update={},
|
|
62
|
+
pull_request={
|
|
63
|
+
"required_approving_review_count": 1,
|
|
64
|
+
"dismiss_stale_reviews_on_push": True,
|
|
65
|
+
"require_code_owner_review": True,
|
|
66
|
+
"require_last_push_approval": True,
|
|
67
|
+
"required_review_thread_resolution": True,
|
|
68
|
+
"automatic_copilot_code_review_enabled": False,
|
|
69
|
+
"allowed_merge_methods": ["merge", "squash", "rebase"],
|
|
70
|
+
},
|
|
71
|
+
required_linear_history={},
|
|
72
|
+
required_signatures={},
|
|
73
|
+
required_status_checks={
|
|
74
|
+
"strict_required_status_checks_policy": True,
|
|
75
|
+
"do_not_enforce_on_create": False,
|
|
76
|
+
"required_status_checks": [
|
|
77
|
+
{
|
|
78
|
+
"context": HealthCheckWorkflow.get_workflow_name(),
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"owner": PyprojectConfigFile.get_main_author_name(),
|
|
86
|
+
"token": token,
|
|
87
|
+
"repo_name": src_pkg_name,
|
|
88
|
+
"ruleset_name": DEFAULT_RULESET_NAME,
|
|
89
|
+
"enforcement": "active",
|
|
90
|
+
"bypass_actors": [
|
|
91
|
+
{
|
|
92
|
+
"actor_id": 5,
|
|
93
|
+
"actor_type": "RepositoryRole",
|
|
94
|
+
"bypass_mode": "always",
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"target": "branch",
|
|
98
|
+
"conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}},
|
|
99
|
+
"rules": rules,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
protect_repository()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Contains utilities for working with GitHub repositories."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from github import Github
|
|
6
|
+
from github.Auth import Token
|
|
7
|
+
from github.Repository import Repository
|
|
8
|
+
|
|
9
|
+
from winipedia_utils.logging.logger import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
DEFAULT_BRANCH = "main"
|
|
14
|
+
|
|
15
|
+
DEFAULT_RULESET_NAME = f"{DEFAULT_BRANCH} protection"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_rules_payload( # noqa: PLR0913
|
|
19
|
+
*,
|
|
20
|
+
creation: dict[str, Any] | None = None,
|
|
21
|
+
update: dict[str, Any] | None = None,
|
|
22
|
+
deletion: dict[str, Any] | None = None,
|
|
23
|
+
required_linear_history: dict[str, Any] | None = None,
|
|
24
|
+
merge_queue: dict[str, Any] | None = None,
|
|
25
|
+
required_deployments: dict[str, Any] | None = None,
|
|
26
|
+
required_signatures: dict[str, Any] | None = None,
|
|
27
|
+
pull_request: dict[str, Any] | None = None,
|
|
28
|
+
required_status_checks: dict[str, Any] | None = None,
|
|
29
|
+
non_fast_forward: dict[str, Any] | None = None,
|
|
30
|
+
commit_message_pattern: dict[str, Any] | None = None,
|
|
31
|
+
commit_author_email_pattern: dict[str, Any] | None = None,
|
|
32
|
+
committer_email_pattern: dict[str, Any] | None = None,
|
|
33
|
+
branch_name_pattern: dict[str, Any] | None = None,
|
|
34
|
+
tag_name_pattern: dict[str, Any] | None = None,
|
|
35
|
+
file_path_restriction: dict[str, Any] | None = None,
|
|
36
|
+
max_file_path_length: dict[str, Any] | None = None,
|
|
37
|
+
file_extension_restriction: dict[str, Any] | None = None,
|
|
38
|
+
max_file_size: dict[str, Any] | None = None,
|
|
39
|
+
workflows: dict[str, Any] | None = None,
|
|
40
|
+
code_scanning: dict[str, Any] | None = None,
|
|
41
|
+
copilot_code_review: dict[str, Any] | None = None,
|
|
42
|
+
) -> list[dict[str, Any]]:
|
|
43
|
+
"""Build a rules array for a GitHub ruleset.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
creation: Only allow users with bypass permission to create matching
|
|
47
|
+
refs.
|
|
48
|
+
update: Only allow users with bypass permission to update matching
|
|
49
|
+
refs.
|
|
50
|
+
deletion: Only allow users with bypass permissions to delete matching
|
|
51
|
+
refs.
|
|
52
|
+
required_linear_history: Prevent merge commits from being pushed to
|
|
53
|
+
matching refs.
|
|
54
|
+
merge_queue: Merges must be performed via a merge queue.
|
|
55
|
+
required_deployments: Choose which environments must be successfully
|
|
56
|
+
deployed to before refs can be pushed.
|
|
57
|
+
required_signatures: Commits pushed to matching refs must have verified
|
|
58
|
+
signatures.
|
|
59
|
+
pull_request: Require all commits be made to a non-target branch and
|
|
60
|
+
submitted via a pull request.
|
|
61
|
+
required_status_checks: Choose which status checks must pass before the
|
|
62
|
+
ref is updated.
|
|
63
|
+
non_fast_forward: Prevent users with push access from force pushing to
|
|
64
|
+
refs.
|
|
65
|
+
commit_message_pattern: Parameters to be used for the
|
|
66
|
+
commit_message_pattern rule.
|
|
67
|
+
commit_author_email_pattern: Parameters to be used for the
|
|
68
|
+
commit_author_email_pattern rule.
|
|
69
|
+
committer_email_pattern: Parameters to be used for the
|
|
70
|
+
committer_email_pattern rule.
|
|
71
|
+
branch_name_pattern: Parameters to be used for the branch_name_pattern
|
|
72
|
+
rule.
|
|
73
|
+
tag_name_pattern: Parameters to be used for the tag_name_pattern rule.
|
|
74
|
+
file_path_restriction: Prevent commits that include changes in
|
|
75
|
+
specified file and folder paths.
|
|
76
|
+
max_file_path_length: Prevent commits that include file paths that
|
|
77
|
+
exceed the specified character limit.
|
|
78
|
+
file_extension_restriction: Prevent commits that include files with
|
|
79
|
+
specified file extensions.
|
|
80
|
+
max_file_size: Prevent commits with individual files that exceed the
|
|
81
|
+
specified limit.
|
|
82
|
+
workflows: Require all changes made to a targeted branch to pass the
|
|
83
|
+
specified workflows.
|
|
84
|
+
code_scanning: Choose which tools must provide code scanning results
|
|
85
|
+
before the reference is updated.
|
|
86
|
+
copilot_code_review: Request Copilot code review for new pull requests
|
|
87
|
+
automatically.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A list of rule objects to be used in a GitHub ruleset.
|
|
91
|
+
"""
|
|
92
|
+
rules: list[dict[str, Any]] = []
|
|
93
|
+
|
|
94
|
+
rule_map = {
|
|
95
|
+
"creation": creation,
|
|
96
|
+
"update": update,
|
|
97
|
+
"deletion": deletion,
|
|
98
|
+
"required_linear_history": required_linear_history,
|
|
99
|
+
"merge_queue": merge_queue,
|
|
100
|
+
"required_deployments": required_deployments,
|
|
101
|
+
"required_signatures": required_signatures,
|
|
102
|
+
"pull_request": pull_request,
|
|
103
|
+
"required_status_checks": required_status_checks,
|
|
104
|
+
"non_fast_forward": non_fast_forward,
|
|
105
|
+
"commit_message_pattern": commit_message_pattern,
|
|
106
|
+
"commit_author_email_pattern": commit_author_email_pattern,
|
|
107
|
+
"committer_email_pattern": committer_email_pattern,
|
|
108
|
+
"branch_name_pattern": branch_name_pattern,
|
|
109
|
+
"tag_name_pattern": tag_name_pattern,
|
|
110
|
+
"file_path_restriction": file_path_restriction,
|
|
111
|
+
"max_file_path_length": max_file_path_length,
|
|
112
|
+
"file_extension_restriction": file_extension_restriction,
|
|
113
|
+
"max_file_size": max_file_size,
|
|
114
|
+
"workflows": workflows,
|
|
115
|
+
"code_scanning": code_scanning,
|
|
116
|
+
"copilot_code_review": copilot_code_review,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for rule_type, rule_config in rule_map.items():
|
|
120
|
+
if rule_config is not None:
|
|
121
|
+
rule_obj: dict[str, Any] = {"type": rule_type}
|
|
122
|
+
if rule_config: # If there are parameters
|
|
123
|
+
rule_obj["parameters"] = rule_config
|
|
124
|
+
rules.append(rule_obj)
|
|
125
|
+
|
|
126
|
+
return rules
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def create_or_update_ruleset( # noqa: PLR0913
|
|
130
|
+
token: str,
|
|
131
|
+
owner: str,
|
|
132
|
+
repo_name: str,
|
|
133
|
+
*,
|
|
134
|
+
ruleset_name: str,
|
|
135
|
+
enforcement: Literal["active", "disabled", "evaluate"] = "active",
|
|
136
|
+
target: Literal["branch", "tag", "push"] = "branch",
|
|
137
|
+
bypass_actors: list[dict[str, Any]] | None = None,
|
|
138
|
+
conditions: dict[
|
|
139
|
+
Literal["ref_name"], dict[Literal["include", "exclude"], list[str]]
|
|
140
|
+
]
|
|
141
|
+
| None = None,
|
|
142
|
+
rules: list[dict[str, Any]] | None = None,
|
|
143
|
+
) -> Any:
|
|
144
|
+
"""Create a ruleset for the repository."""
|
|
145
|
+
repo = get_repo(token, owner, repo_name)
|
|
146
|
+
ruleset_id = ruleset_exists(
|
|
147
|
+
token=token, owner=owner, repo_name=repo_name, ruleset_name=ruleset_name
|
|
148
|
+
)
|
|
149
|
+
method = "PUT" if ruleset_id else "POST"
|
|
150
|
+
url = f"{repo.url}/rulesets"
|
|
151
|
+
|
|
152
|
+
if ruleset_id:
|
|
153
|
+
url += f"/{ruleset_id}"
|
|
154
|
+
|
|
155
|
+
payload: dict[str, Any] = {
|
|
156
|
+
"name": ruleset_name,
|
|
157
|
+
"enforcement": enforcement,
|
|
158
|
+
"target": target,
|
|
159
|
+
"conditions": conditions,
|
|
160
|
+
"rules": rules,
|
|
161
|
+
}
|
|
162
|
+
if bypass_actors:
|
|
163
|
+
payload["bypass_actors"] = bypass_actors
|
|
164
|
+
|
|
165
|
+
_headers, res = repo._requester.requestJsonAndCheck( # noqa: SLF001
|
|
166
|
+
method,
|
|
167
|
+
url,
|
|
168
|
+
headers={
|
|
169
|
+
"Accept": "application/vnd.github+json",
|
|
170
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
171
|
+
},
|
|
172
|
+
input=payload,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return res
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_all_rulesets(token: str, owner: str, repo_name: str) -> Any:
|
|
179
|
+
"""Get all rulesets for the repository."""
|
|
180
|
+
repo = get_repo(token, owner, repo_name)
|
|
181
|
+
url = f"{repo.url}/rulesets"
|
|
182
|
+
method = "GET"
|
|
183
|
+
_headers, res = repo._requester.requestJsonAndCheck( # noqa: SLF001
|
|
184
|
+
method,
|
|
185
|
+
url,
|
|
186
|
+
headers={
|
|
187
|
+
"Accept": "application/vnd.github+json",
|
|
188
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
return res
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_repo(token: str, owner: str, repo_name: str) -> Repository:
|
|
195
|
+
"""Get the repository."""
|
|
196
|
+
auth = Token(token)
|
|
197
|
+
github = Github(auth=auth)
|
|
198
|
+
return github.get_repo(f"{owner}/{repo_name}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def ruleset_exists(token: str, owner: str, repo_name: str, ruleset_name: str) -> int:
|
|
202
|
+
"""Check if the main protection ruleset exists."""
|
|
203
|
+
rulesets = get_all_rulesets(token, owner, repo_name)
|
|
204
|
+
main_ruleset = next((rs for rs in rulesets if rs["name"] == ruleset_name), None)
|
|
205
|
+
return main_ruleset["id"] if main_ruleset else 0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|
|
@@ -4,6 +4,9 @@ from abc import abstractmethod
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
import winipedia_utils
|
|
8
|
+
from winipedia_utils.modules.module import make_obj_importpath
|
|
9
|
+
from winipedia_utils.modules.package import get_src_package
|
|
7
10
|
from winipedia_utils.text.config import YamlConfigFile
|
|
8
11
|
from winipedia_utils.text.string import split_on_uppercase
|
|
9
12
|
|
|
@@ -11,22 +14,36 @@ from winipedia_utils.text.string import split_on_uppercase
|
|
|
11
14
|
class Workflow(YamlConfigFile):
|
|
12
15
|
"""Base class for workflows."""
|
|
13
16
|
|
|
17
|
+
@classmethod
|
|
14
18
|
@abstractmethod
|
|
15
|
-
def get_workflow_triggers(
|
|
19
|
+
def get_workflow_triggers(cls) -> dict[str, Any]:
|
|
16
20
|
"""Get the workflow triggers."""
|
|
17
21
|
|
|
22
|
+
@classmethod
|
|
18
23
|
@abstractmethod
|
|
19
|
-
def get_permissions(
|
|
24
|
+
def get_permissions(cls) -> dict[str, Any]:
|
|
20
25
|
"""Get the workflow permissions."""
|
|
21
26
|
|
|
27
|
+
@classmethod
|
|
22
28
|
@abstractmethod
|
|
23
|
-
def get_jobs(
|
|
29
|
+
def get_jobs(cls) -> dict[str, Any]:
|
|
24
30
|
"""Get the workflow jobs."""
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
@classmethod
|
|
33
|
+
def get_parent_path(cls) -> Path:
|
|
27
34
|
"""Get the path to the config file."""
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
return Path(".github/workflows")
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
39
|
+
"""Get the workflow config."""
|
|
40
|
+
return {
|
|
41
|
+
"name": cls.get_workflow_name(),
|
|
42
|
+
"on": cls.get_workflow_triggers(),
|
|
43
|
+
"permissions": cls.get_permissions(),
|
|
44
|
+
"run-name": cls.get_run_name(),
|
|
45
|
+
"jobs": cls.get_jobs(),
|
|
46
|
+
}
|
|
30
47
|
|
|
31
48
|
@classmethod
|
|
32
49
|
def get_standard_job(
|
|
@@ -39,7 +56,7 @@ class Workflow(YamlConfigFile):
|
|
|
39
56
|
) -> dict[str, Any]:
|
|
40
57
|
"""Get a standard job."""
|
|
41
58
|
if name is None:
|
|
42
|
-
name = cls.
|
|
59
|
+
name = cls.get_filename()
|
|
43
60
|
|
|
44
61
|
if steps is None:
|
|
45
62
|
steps = []
|
|
@@ -57,31 +74,15 @@ class Workflow(YamlConfigFile):
|
|
|
57
74
|
job[name]["if"] = if_condition
|
|
58
75
|
return job
|
|
59
76
|
|
|
60
|
-
@classmethod
|
|
61
|
-
def get_standard_job_name(cls) -> str:
|
|
62
|
-
"""Get the standard job name."""
|
|
63
|
-
return "_".join(
|
|
64
|
-
split_on_uppercase(cls.__name__.removesuffix(Workflow.__name__))
|
|
65
|
-
).lower()
|
|
66
|
-
|
|
67
77
|
@classmethod
|
|
68
78
|
def get_workflow_name(cls) -> str:
|
|
69
79
|
"""Get the workflow name."""
|
|
70
80
|
return " ".join(split_on_uppercase(cls.__name__))
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
@classmethod
|
|
83
|
+
def get_run_name(cls) -> str:
|
|
73
84
|
"""Get the workflow run name."""
|
|
74
|
-
return f"{
|
|
75
|
-
|
|
76
|
-
def get_configs(self) -> dict[str, Any]:
|
|
77
|
-
"""Get the workflow config."""
|
|
78
|
-
return {
|
|
79
|
-
"name": self.get_workflow_name(),
|
|
80
|
-
"on": self.get_workflow_triggers(),
|
|
81
|
-
"permissions": self.get_permissions(),
|
|
82
|
-
"run-name": self.get_run_name(),
|
|
83
|
-
"jobs": self.get_jobs(),
|
|
84
|
-
}
|
|
85
|
+
return f"{cls.get_workflow_name()}"
|
|
85
86
|
|
|
86
87
|
@classmethod
|
|
87
88
|
def get_checkout_step(cls, fetch_depth: int | None = None) -> dict[str, Any]:
|
|
@@ -145,13 +146,6 @@ class Workflow(YamlConfigFile):
|
|
|
145
146
|
"run": "curl -sSL https://install.python-poetry.org | python3 -",
|
|
146
147
|
}
|
|
147
148
|
)
|
|
148
|
-
steps.append(
|
|
149
|
-
{
|
|
150
|
-
"name": "Extract Version from pyproject.toml",
|
|
151
|
-
"id": "version",
|
|
152
|
-
"run": 'version=$(poetry version -s) && echo "Project version: $version" && echo "version=v$version" >> $GITHUB_OUTPUT', # noqa: E501
|
|
153
|
-
},
|
|
154
|
-
)
|
|
155
149
|
if configure_pipy_token:
|
|
156
150
|
steps.append(
|
|
157
151
|
{
|
|
@@ -163,56 +157,91 @@ class Workflow(YamlConfigFile):
|
|
|
163
157
|
steps.append({"name": "Install Dependencies", "run": "poetry install"})
|
|
164
158
|
return steps
|
|
165
159
|
|
|
166
|
-
@
|
|
167
|
-
def get_release_steps() -> list[dict[str, Any]]:
|
|
160
|
+
@classmethod
|
|
161
|
+
def get_release_steps(cls) -> list[dict[str, Any]]:
|
|
168
162
|
"""Get the release steps."""
|
|
169
163
|
return [
|
|
170
164
|
{
|
|
171
165
|
"name": "Create and Push Tag",
|
|
172
|
-
"run": f"git tag {
|
|
166
|
+
"run": f"git tag {cls.get_version()} && git push origin {cls.get_version()}", # noqa: E501
|
|
173
167
|
},
|
|
174
168
|
{
|
|
175
169
|
"name": "Build Changelog",
|
|
176
170
|
"id": "build_changelog",
|
|
177
171
|
"uses": "mikepenz/release-changelog-builder-action@develop",
|
|
178
|
-
"with": {"token":
|
|
172
|
+
"with": {"token": cls.get_github_token()},
|
|
179
173
|
},
|
|
180
174
|
{
|
|
181
175
|
"name": "Create GitHub Release",
|
|
182
176
|
"uses": "ncipollo/release-action@main",
|
|
183
177
|
"with": {
|
|
184
|
-
"tag":
|
|
185
|
-
"name":
|
|
178
|
+
"tag": cls.get_version(),
|
|
179
|
+
"name": cls.get_repo_and_version(),
|
|
186
180
|
"body": "${{ steps.build_changelog.outputs.changelog }}",
|
|
187
181
|
},
|
|
188
182
|
},
|
|
189
183
|
]
|
|
190
184
|
|
|
191
|
-
@
|
|
192
|
-
def
|
|
185
|
+
@classmethod
|
|
186
|
+
def get_extract_version_step(cls) -> dict[str, Any]:
|
|
187
|
+
"""Get the extract version step."""
|
|
188
|
+
return {
|
|
189
|
+
"name": "Extract Version from pyproject.toml",
|
|
190
|
+
"id": "version",
|
|
191
|
+
"run": 'version=$(poetry version -s) && echo "Project version: $version" && echo "version=v$version" >> $GITHUB_OUTPUT', # noqa: E501
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def get_publish_to_pypi_step(cls) -> dict[str, Any]:
|
|
193
196
|
"""Get the publish step."""
|
|
194
197
|
return {"name": "Build and publish to PyPI", "run": "poetry publish --build"}
|
|
195
198
|
|
|
196
|
-
@
|
|
197
|
-
def get_pre_commit_step() -> dict[str, Any]:
|
|
199
|
+
@classmethod
|
|
200
|
+
def get_pre_commit_step(cls) -> dict[str, Any]:
|
|
198
201
|
"""Get the pre-commit step.
|
|
199
202
|
|
|
200
203
|
using pre commit in case other hooks are added later
|
|
201
204
|
and bc it fails if files are changed,
|
|
202
205
|
setup script shouldnt change files
|
|
203
206
|
"""
|
|
204
|
-
|
|
207
|
+
step: dict[str, Any] = {
|
|
205
208
|
"name": "Run Hooks",
|
|
206
209
|
"run": "poetry run pre-commit run --all-files --verbose",
|
|
207
210
|
}
|
|
211
|
+
if get_src_package() == winipedia_utils:
|
|
212
|
+
step["env"] = {"REPO_TOKEN": cls.get_repo_token()}
|
|
213
|
+
return step
|
|
208
214
|
|
|
209
|
-
@
|
|
210
|
-
def
|
|
215
|
+
@classmethod
|
|
216
|
+
def get_commit_step(cls) -> dict[str, Any]:
|
|
217
|
+
"""Get the commit step."""
|
|
218
|
+
return {
|
|
219
|
+
"name": "Commit Changes",
|
|
220
|
+
"run": "poetry run git commit --no-verify -m 'CI/CD: Committing possible changes to pyproject.toml and poetry.lock' && poetry run git push", # noqa: E501
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def get_protect_repository_step(cls) -> dict[str, Any]:
|
|
225
|
+
"""Get the protect repository step."""
|
|
226
|
+
from winipedia_utils.git.github.repo import ( # noqa: PLC0415
|
|
227
|
+
protect, # avoid circular import
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"name": "Protect Repository",
|
|
232
|
+
"run": f"poetry run python -m {make_obj_importpath(protect)}",
|
|
233
|
+
"env": {
|
|
234
|
+
"REPO_TOKEN": cls.get_repo_token(),
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def get_repository_name(cls) -> str:
|
|
211
240
|
"""Get the repository name."""
|
|
212
241
|
return "${{ github.event.repository.name }}"
|
|
213
242
|
|
|
214
|
-
@
|
|
215
|
-
def get_ref_name() -> str:
|
|
243
|
+
@classmethod
|
|
244
|
+
def get_ref_name(cls) -> str:
|
|
216
245
|
"""Get the ref name."""
|
|
217
246
|
return "${{ github.ref_name }}"
|
|
218
247
|
|
|
@@ -221,7 +250,22 @@ class Workflow(YamlConfigFile):
|
|
|
221
250
|
"""Get the version."""
|
|
222
251
|
return "${{ steps.version.outputs.version }}"
|
|
223
252
|
|
|
224
|
-
@
|
|
225
|
-
def get_repo_and_version() -> str:
|
|
253
|
+
@classmethod
|
|
254
|
+
def get_repo_and_version(cls) -> str:
|
|
226
255
|
"""Get the repository name and ref name."""
|
|
227
|
-
return f"{
|
|
256
|
+
return f"{cls.get_repository_name()} {cls.get_version()}"
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def get_ownwer(cls) -> str:
|
|
260
|
+
"""Get the repository owner."""
|
|
261
|
+
return "${{ github.repository_owner }}"
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def get_github_token(cls) -> str:
|
|
265
|
+
"""Get the GitHub token."""
|
|
266
|
+
return "${{ secrets.GITHUB_TOKEN }}"
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def get_repo_token(cls) -> str:
|
|
270
|
+
"""Get the repository token."""
|
|
271
|
+
return "${{ secrets.REPO_TOKEN }}"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Contains the pull request workflow.
|
|
2
|
+
|
|
3
|
+
This workflow is used to run tests on pull requests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from winipedia_utils.git.github.workflows.base.base import Workflow
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HealthCheckWorkflow(Workflow):
|
|
12
|
+
"""Pull request workflow.
|
|
13
|
+
|
|
14
|
+
This workflow is triggered by a pull request.
|
|
15
|
+
It runs tests on the pull request.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_workflow_triggers(cls) -> dict[str, Any]:
|
|
20
|
+
"""Get the workflow triggers."""
|
|
21
|
+
return {
|
|
22
|
+
"pull_request": {
|
|
23
|
+
"types": ["opened", "synchronize", "reopened"],
|
|
24
|
+
},
|
|
25
|
+
"schedule": [
|
|
26
|
+
{
|
|
27
|
+
# run every day at 6 am
|
|
28
|
+
"cron": "0 6 * * *",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
"workflow_dispatch": {},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_permissions(cls) -> dict[str, Any]:
|
|
36
|
+
"""Get the workflow permissions."""
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get_jobs(cls) -> dict[str, Any]:
|
|
41
|
+
"""Get the workflow jobs."""
|
|
42
|
+
return {
|
|
43
|
+
**cls.get_standard_job(
|
|
44
|
+
steps=[
|
|
45
|
+
*(
|
|
46
|
+
cls.get_poetry_setup_steps(
|
|
47
|
+
install_dependencies=True,
|
|
48
|
+
)
|
|
49
|
+
),
|
|
50
|
+
cls.get_protect_repository_step(),
|
|
51
|
+
cls.get_pre_commit_step(),
|
|
52
|
+
cls.get_extract_version_step(),
|
|
53
|
+
],
|
|
54
|
+
),
|
|
55
|
+
}
|
|
@@ -5,8 +5,8 @@ This workflow is used to publish the package to PyPI with poetry.
|
|
|
5
5
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from winipedia_utils.git.workflows.base.base import Workflow
|
|
9
|
-
from winipedia_utils.git.workflows.release import ReleaseWorkflow
|
|
8
|
+
from winipedia_utils.git.github.workflows.base.base import Workflow
|
|
9
|
+
from winipedia_utils.git.github.workflows.release import ReleaseWorkflow
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class PublishWorkflow(Workflow):
|
|
@@ -16,7 +16,8 @@ class PublishWorkflow(Workflow):
|
|
|
16
16
|
It publishes the package to PyPI with poetry.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_workflow_triggers(cls) -> dict[str, Any]:
|
|
20
21
|
"""Get the workflow triggers."""
|
|
21
22
|
return {
|
|
22
23
|
"workflow_run": {
|
|
@@ -25,22 +26,24 @@ class PublishWorkflow(Workflow):
|
|
|
25
26
|
},
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_permissions(cls) -> dict[str, Any]:
|
|
29
31
|
"""Get the workflow permissions."""
|
|
30
32
|
return {
|
|
31
33
|
"contents": "read",
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
@classmethod
|
|
37
|
+
def get_jobs(cls) -> dict[str, Any]:
|
|
35
38
|
"""Get the workflow jobs."""
|
|
36
|
-
return
|
|
39
|
+
return cls.get_standard_job(
|
|
37
40
|
steps=[
|
|
38
41
|
*(
|
|
39
|
-
|
|
42
|
+
cls.get_poetry_setup_steps(
|
|
40
43
|
configure_pipy_token=True,
|
|
41
44
|
)
|
|
42
45
|
),
|
|
43
|
-
|
|
46
|
+
cls.get_publish_to_pypi_step(),
|
|
44
47
|
],
|
|
45
48
|
if_condition="${{ github.event.workflow_run.conclusion == 'success' }}",
|
|
46
49
|
)
|