winipedia-utils 0.3.43__py3-none-any.whl → 0.4.18__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 +118 -54
- winipedia_utils/git/github/workflows/health_check.py +57 -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.43.dist-info → winipedia_utils-0.4.18.dist-info}/METADATA +23 -8
- {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.18.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.43.dist-info → winipedia_utils-0.4.18.dist-info}/WHEEL +0 -0
- {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.18.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,38 +74,28 @@ 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
|
-
def get_checkout_step(
|
|
88
|
+
def get_checkout_step(
|
|
89
|
+
cls,
|
|
90
|
+
fetch_depth: int | None = None,
|
|
91
|
+
*,
|
|
92
|
+
token: bool = False,
|
|
93
|
+
) -> dict[str, Any]:
|
|
88
94
|
"""Get the checkout step.
|
|
89
95
|
|
|
90
96
|
Args:
|
|
91
97
|
fetch_depth: The fetch depth to use. If None, no fetch depth is specified.
|
|
98
|
+
token: Whether to use the repository token.
|
|
92
99
|
|
|
93
100
|
Returns:
|
|
94
101
|
The checkout step.
|
|
@@ -98,7 +105,10 @@ class Workflow(YamlConfigFile):
|
|
|
98
105
|
"uses": "actions/checkout@main",
|
|
99
106
|
}
|
|
100
107
|
if fetch_depth is not None:
|
|
101
|
-
step
|
|
108
|
+
step.setdefault("with", {})["fetch-depth"] = fetch_depth
|
|
109
|
+
|
|
110
|
+
if token:
|
|
111
|
+
step.setdefault("with", {})["token"] = cls.get_repo_token()
|
|
102
112
|
return step
|
|
103
113
|
|
|
104
114
|
@classmethod
|
|
@@ -109,6 +119,7 @@ class Workflow(YamlConfigFile):
|
|
|
109
119
|
fetch_depth: int | None = None,
|
|
110
120
|
configure_pipy_token: bool = False,
|
|
111
121
|
force_main_head: bool = False,
|
|
122
|
+
token: bool = False,
|
|
112
123
|
) -> list[dict[str, Any]]:
|
|
113
124
|
"""Get the poetry steps.
|
|
114
125
|
|
|
@@ -119,11 +130,12 @@ class Workflow(YamlConfigFile):
|
|
|
119
130
|
force_main_head: Whether to exit if the running branch or current commit is not
|
|
120
131
|
equal to the most recent commit on main. This is useful for workflows that
|
|
121
132
|
should only run on main.
|
|
133
|
+
token: Whether to use the repository token.
|
|
122
134
|
|
|
123
135
|
Returns:
|
|
124
136
|
The poetry steps.
|
|
125
137
|
"""
|
|
126
|
-
steps = [cls.get_checkout_step(fetch_depth)]
|
|
138
|
+
steps = [cls.get_checkout_step(fetch_depth, token=token)]
|
|
127
139
|
if force_main_head:
|
|
128
140
|
# exit with code 1 if the running branch is not main
|
|
129
141
|
steps.append(
|
|
@@ -132,6 +144,7 @@ class Workflow(YamlConfigFile):
|
|
|
132
144
|
"run": 'git fetch origin main --depth=1; main_sha=$(git rev-parse origin/main); if [ "$GITHUB_SHA" != "$main_sha" ]; then echo "Tag commit is not the latest commit on main."; exit 1; fi', # noqa: E501
|
|
133
145
|
}
|
|
134
146
|
)
|
|
147
|
+
steps.append(cls.get_setup_git_step())
|
|
135
148
|
steps.append(
|
|
136
149
|
{
|
|
137
150
|
"name": "Setup Python",
|
|
@@ -145,13 +158,6 @@ class Workflow(YamlConfigFile):
|
|
|
145
158
|
"run": "curl -sSL https://install.python-poetry.org | python3 -",
|
|
146
159
|
}
|
|
147
160
|
)
|
|
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
161
|
if configure_pipy_token:
|
|
156
162
|
steps.append(
|
|
157
163
|
{
|
|
@@ -163,56 +169,99 @@ class Workflow(YamlConfigFile):
|
|
|
163
169
|
steps.append({"name": "Install Dependencies", "run": "poetry install"})
|
|
164
170
|
return steps
|
|
165
171
|
|
|
166
|
-
@
|
|
167
|
-
def get_release_steps() -> list[dict[str, Any]]:
|
|
172
|
+
@classmethod
|
|
173
|
+
def get_release_steps(cls) -> list[dict[str, Any]]:
|
|
168
174
|
"""Get the release steps."""
|
|
169
175
|
return [
|
|
170
176
|
{
|
|
171
177
|
"name": "Create and Push Tag",
|
|
172
|
-
"run": f"git tag {
|
|
178
|
+
"run": f"git tag {cls.get_version()} && git push && git push origin {cls.get_version()}", # noqa: E501
|
|
173
179
|
},
|
|
174
180
|
{
|
|
175
181
|
"name": "Build Changelog",
|
|
176
182
|
"id": "build_changelog",
|
|
177
183
|
"uses": "mikepenz/release-changelog-builder-action@develop",
|
|
178
|
-
"with": {"token":
|
|
184
|
+
"with": {"token": cls.get_github_token()},
|
|
179
185
|
},
|
|
180
186
|
{
|
|
181
187
|
"name": "Create GitHub Release",
|
|
182
188
|
"uses": "ncipollo/release-action@main",
|
|
183
189
|
"with": {
|
|
184
|
-
"tag":
|
|
185
|
-
"name":
|
|
190
|
+
"tag": cls.get_version(),
|
|
191
|
+
"name": cls.get_repo_and_version(),
|
|
186
192
|
"body": "${{ steps.build_changelog.outputs.changelog }}",
|
|
187
193
|
},
|
|
188
194
|
},
|
|
189
195
|
]
|
|
190
196
|
|
|
191
|
-
@
|
|
192
|
-
def
|
|
197
|
+
@classmethod
|
|
198
|
+
def get_extract_version_step(cls) -> dict[str, Any]:
|
|
199
|
+
"""Get the extract version step."""
|
|
200
|
+
return {
|
|
201
|
+
"name": "Extract Version from pyproject.toml",
|
|
202
|
+
"id": "version",
|
|
203
|
+
"run": 'version=$(poetry version -s) && echo "Project version: $version" && echo "version=v$version" >> $GITHUB_OUTPUT', # noqa: E501
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def get_publish_to_pypi_step(cls) -> dict[str, Any]:
|
|
193
208
|
"""Get the publish step."""
|
|
194
209
|
return {"name": "Build and publish to PyPI", "run": "poetry publish --build"}
|
|
195
210
|
|
|
196
|
-
@
|
|
197
|
-
def get_pre_commit_step() -> dict[str, Any]:
|
|
211
|
+
@classmethod
|
|
212
|
+
def get_pre_commit_step(cls) -> dict[str, Any]:
|
|
198
213
|
"""Get the pre-commit step.
|
|
199
214
|
|
|
200
215
|
using pre commit in case other hooks are added later
|
|
201
216
|
and bc it fails if files are changed,
|
|
202
217
|
setup script shouldnt change files
|
|
203
218
|
"""
|
|
204
|
-
|
|
219
|
+
step: dict[str, Any] = {
|
|
205
220
|
"name": "Run Hooks",
|
|
206
221
|
"run": "poetry run pre-commit run --all-files --verbose",
|
|
207
222
|
}
|
|
223
|
+
if get_src_package() == winipedia_utils:
|
|
224
|
+
step["env"] = {"REPO_TOKEN": cls.get_repo_token()}
|
|
225
|
+
return step
|
|
208
226
|
|
|
209
|
-
@
|
|
210
|
-
def
|
|
227
|
+
@classmethod
|
|
228
|
+
def get_setup_git_step(cls) -> dict[str, Any]:
|
|
229
|
+
"""Get the setup git step."""
|
|
230
|
+
return {
|
|
231
|
+
"name": "Setup Git",
|
|
232
|
+
"run": 'git config --global user.email "github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]"', # noqa: E501
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def get_commit_step(cls) -> dict[str, Any]:
|
|
237
|
+
"""Get the commit step."""
|
|
238
|
+
return {
|
|
239
|
+
"name": "Commit added changes",
|
|
240
|
+
"run": "git commit --no-verify -m '[skip ci] CI/CD: Committing possible added changes (e.g.: pyproject.toml and poetry.lock)'", # noqa: E501
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def get_protect_repository_step(cls) -> dict[str, Any]:
|
|
245
|
+
"""Get the protect repository step."""
|
|
246
|
+
from winipedia_utils.git.github.repo import ( # noqa: PLC0415
|
|
247
|
+
protect, # avoid circular import
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"name": "Protect Repository",
|
|
252
|
+
"run": f"poetry run python -m {make_obj_importpath(protect)}",
|
|
253
|
+
"env": {
|
|
254
|
+
"REPO_TOKEN": cls.get_repo_token(),
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def get_repository_name(cls) -> str:
|
|
211
260
|
"""Get the repository name."""
|
|
212
261
|
return "${{ github.event.repository.name }}"
|
|
213
262
|
|
|
214
|
-
@
|
|
215
|
-
def get_ref_name() -> str:
|
|
263
|
+
@classmethod
|
|
264
|
+
def get_ref_name(cls) -> str:
|
|
216
265
|
"""Get the ref name."""
|
|
217
266
|
return "${{ github.ref_name }}"
|
|
218
267
|
|
|
@@ -221,7 +270,22 @@ class Workflow(YamlConfigFile):
|
|
|
221
270
|
"""Get the version."""
|
|
222
271
|
return "${{ steps.version.outputs.version }}"
|
|
223
272
|
|
|
224
|
-
@
|
|
225
|
-
def get_repo_and_version() -> str:
|
|
273
|
+
@classmethod
|
|
274
|
+
def get_repo_and_version(cls) -> str:
|
|
226
275
|
"""Get the repository name and ref name."""
|
|
227
|
-
return f"{
|
|
276
|
+
return f"{cls.get_repository_name()} {cls.get_version()}"
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def get_ownwer(cls) -> str:
|
|
280
|
+
"""Get the repository owner."""
|
|
281
|
+
return "${{ github.repository_owner }}"
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def get_github_token(cls) -> str:
|
|
285
|
+
"""Get the GitHub token."""
|
|
286
|
+
return "${{ secrets.GITHUB_TOKEN }}"
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def get_repo_token(cls) -> str:
|
|
290
|
+
"""Get the repository token."""
|
|
291
|
+
return "${{ secrets.REPO_TOKEN }}"
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
token=True,
|
|
49
|
+
)
|
|
50
|
+
),
|
|
51
|
+
cls.get_protect_repository_step(),
|
|
52
|
+
cls.get_pre_commit_step(),
|
|
53
|
+
cls.get_commit_step(),
|
|
54
|
+
cls.get_extract_version_step(),
|
|
55
|
+
],
|
|
56
|
+
),
|
|
57
|
+
}
|