Skip to content

Commit 9cb8554

Browse files
committed
Use Pydantic
1 parent 27820ee commit 9cb8554

4 files changed

Lines changed: 117 additions & 203 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ PyYAML~=6.0.0
22
packaging~=21.3
33
requests~=2.28.1
44
github-action-utils~=1.0.2
5+
pydantic==1.10.6

src/config.py

Lines changed: 82 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,97 @@
11
import json
22
import os
33
import time
4-
from collections.abc import Mapping
4+
from enum import Enum
55
from pathlib import Path
6-
from typing import Any, NamedTuple
6+
from typing import Any
77

88
import github_action_utils as gha_utils # type: ignore
9+
from pydantic import BaseSettings, validator
910

10-
LATEST_RELEASE_TAG = "release-tag"
11-
LATEST_RELEASE_COMMIT_SHA = "release-commit-sha"
12-
DEFAULT_BRANCH_COMMIT_SHA = "default-branch-sha"
1311

14-
UPDATE_VERSION_WITH_LIST = [
15-
LATEST_RELEASE_TAG,
16-
LATEST_RELEASE_COMMIT_SHA,
17-
DEFAULT_BRANCH_COMMIT_SHA,
18-
]
12+
class UpdateVersionWith(str, Enum):
13+
LATEST_RELEASE_TAG = "release-tag"
14+
LATEST_RELEASE_COMMIT_SHA = "release-commit-sha"
15+
DEFAULT_BRANCH_COMMIT_SHA = "default-branch-sha"
1916

20-
MAJOR_RELEASE = "major"
21-
MINOR_RELEASE = "minor"
22-
PATCH_RELEASE = "patch"
2317

24-
ALL_RELEASE_TYPES = [MAJOR_RELEASE, MINOR_RELEASE, PATCH_RELEASE]
18+
class ReleaseType(str, Enum):
19+
MAJOR = "major"
20+
MINOR = "minor"
21+
PATCH = "patch"
2522

2623

27-
class ActionEnvironment(NamedTuple):
24+
class ActionEnvironment(BaseSettings):
2825
repository: str
2926
base_branch: str
3027
event_name: str
31-
github_workspace: str
32-
33-
@classmethod
34-
def from_env(cls, env: Mapping[str, str]) -> "ActionEnvironment":
35-
return cls(
36-
repository=env["GITHUB_REPOSITORY"],
37-
base_branch=env["GITHUB_REF"],
38-
event_name=env["GITHUB_EVENT_NAME"],
39-
github_workspace=env["GITHUB_WORKSPACE"],
40-
)
28+
workspace: str
29+
30+
class Config:
31+
allow_mutation = False
32+
env_prefix = "GITHUB_"
33+
fields = {
34+
"base_branch": {
35+
"env": "GITHUB_REF",
36+
},
37+
}
4138

4239

43-
class Configuration(NamedTuple):
40+
class Configuration(BaseSettings):
4441
"""Configuration class for GitHub Actions Version Updater"""
4542

46-
github_token: str | None = None
43+
token: str | None = None
4744
skip_pull_request: bool = False
4845
git_committer_username: str = "github-actions[bot]"
4946
git_committer_email: str = "github-actions[bot]@users.noreply.github.com"
5047
pull_request_title: str = "Update GitHub Action Versions"
5148
pull_request_branch: str | None = None
5249
commit_message: str = "Update GitHub Action Versions"
53-
ignore_actions: set[str] = set()
54-
update_version_with: str = LATEST_RELEASE_TAG
55-
pull_request_user_reviewers: set[str] = set()
56-
pull_request_team_reviewers: set[str] = set()
57-
pull_request_labels: set[str] = set()
58-
release_types: list[str] = ALL_RELEASE_TYPES
59-
extra_workflow_paths: set[str] = set()
50+
ignore_actions: frozenset[str] = frozenset()
51+
update_version_with: UpdateVersionWith = UpdateVersionWith.LATEST_RELEASE_TAG
52+
pull_request_user_reviewers: frozenset[str] = frozenset()
53+
pull_request_team_reviewers: frozenset[str] = frozenset()
54+
pull_request_labels: frozenset[str] = frozenset()
55+
release_types: frozenset[ReleaseType] = frozenset(
56+
[
57+
ReleaseType.MAJOR,
58+
ReleaseType.MINOR,
59+
ReleaseType.PATCH,
60+
]
61+
)
62+
extra_workflow_locations: frozenset[str] = frozenset()
63+
64+
class Config:
65+
allow_mutation = False
66+
env_prefix = "INPUT_"
67+
fields = {
68+
"git_committer_username": {
69+
"env": "INPUT_COMMITTER_USERNAME",
70+
},
71+
"ignore_actions": {
72+
"env": "INPUT_IGNORE",
73+
},
74+
}
75+
76+
@classmethod
77+
def parse_env_var(cls, field_name: str, raw_val: str):
78+
if field_name in [
79+
"ignore_actions",
80+
"pull_request_user_reviewers",
81+
"pull_request_team_reviewers",
82+
"pull_request_labels",
83+
"release_types",
84+
"extra_workflow_locations",
85+
]:
86+
if raw_val.startswith("[") and raw_val.endswith("]"):
87+
return frozenset(json.loads(raw_val))
88+
return frozenset(s.strip() for s in raw_val.strip().split(",") if s)
89+
return raw_val
6090

6191
def get_pull_request_branch_name(self) -> tuple[bool, str]:
6292
"""
6393
Get the pull request branch name.
64-
If the branch name is provided by the user set the force push flag to True
94+
If the branch name is provided by the user frozenset the force push flag to True
6595
"""
6696
if self.pull_request_branch is None:
6797
return (False, f"gh-actions-update-{int(time.time())}")
@@ -72,159 +102,42 @@ def git_commit_author(self) -> str:
72102
"""git_commit_author option"""
73103
return f"{self.git_committer_username} <{self.git_committer_email}>"
74104

75-
@classmethod
76-
def create(cls, env: Mapping[str, str | None]) -> "Configuration":
77-
"""
78-
Create a Configuration object from environment variables
79-
"""
80-
cleaned_user_config: dict[str, Any] = cls.clean_user_config(
81-
cls.get_user_config(env)
82-
)
83-
return cls(**cleaned_user_config)
84-
85-
@classmethod
86-
def get_user_config(cls, env: Mapping[str, str | None]) -> dict[str, str | None]:
87-
"""
88-
Read user provided input and return user configuration
89-
"""
90-
user_config: dict[str, str | None] = {
91-
"github_token": env.get("INPUT_TOKEN"),
92-
"skip_pull_request": env.get("INPUT_SKIP_PULL_REQUEST"),
93-
"git_committer_username": env.get("INPUT_COMMITTER_USERNAME"),
94-
"git_committer_email": env.get("INPUT_COMMITTER_EMAIL"),
95-
"pull_request_title": env.get("INPUT_PULL_REQUEST_TITLE"),
96-
"pull_request_branch": env.get("INPUT_PULL_REQUEST_BRANCH"),
97-
"commit_message": env.get("INPUT_COMMIT_MESSAGE"),
98-
"ignore_actions": env.get("INPUT_IGNORE"),
99-
"update_version_with": env.get("INPUT_UPDATE_VERSION_WITH"),
100-
"release_types": env.get("INPUT_RELEASE_TYPES"),
101-
"pull_request_user_reviewers": env.get("INPUT_PULL_REQUEST_USER_REVIEWERS"),
102-
"pull_request_team_reviewers": env.get("INPUT_PULL_REQUEST_TEAM_REVIEWERS"),
103-
"pull_request_labels": env.get("INPUT_PULL_REQUEST_LABELS"),
104-
"extra_workflow_paths": env.get("INPUT_EXTRA_WORKFLOW_LOCATIONS"),
105-
}
106-
return user_config
107-
108-
@classmethod
109-
def clean_user_config(cls, user_config: dict[str, str | None]) -> dict[str, Any]:
110-
cleaned_user_config: dict[str, Any] = {}
111-
112-
for key, value in user_config.items():
113-
if key in cls._fields:
114-
cleaned_value = getattr(cls, f"clean_{key.lower()}", lambda x: x)(value)
115-
116-
if cleaned_value is not None:
117-
cleaned_user_config[key] = cleaned_value
118-
119-
return cleaned_user_config
120-
121-
@staticmethod
122-
def clean_ignore_actions(value: Any) -> set[str] | None:
123-
if isinstance(value, str) and value.startswith("[") and value.endswith("]"):
124-
ignore_actions = json.loads(value)
125-
126-
if isinstance(ignore_actions, list) and all(
127-
isinstance(item, str) for item in ignore_actions
128-
):
129-
return set(ignore_actions)
130-
else:
131-
gha_utils.error(
132-
"Invalid input for `ignore` field, "
133-
f"expected JSON array of strings but got `{value}`"
134-
)
135-
raise SystemExit(1)
136-
elif value and isinstance(value, str):
137-
return {s.strip() for s in value.strip().split(",") if s}
138-
else:
139-
return None
140-
141-
@staticmethod
142-
def clean_pull_request_user_reviewers(value: Any) -> set[str] | None:
143-
if value and isinstance(value, str):
144-
return {s.strip() for s in value.strip().split(",") if s}
145-
return None
146-
147-
@staticmethod
148-
def clean_pull_request_team_reviewers(value: Any) -> set[str] | None:
149-
if value and isinstance(value, str):
150-
return {s.strip() for s in value.strip().split(",") if s}
151-
return None
152-
153-
@staticmethod
154-
def clean_pull_request_labels(value: Any) -> set[str] | None:
155-
if value and isinstance(value, str):
156-
return {s.strip() for s in value.strip().split(",") if s}
157-
return None
158-
159-
@staticmethod
160-
def clean_release_types(value: Any) -> list[str] | None:
161-
if value and isinstance(value, str):
162-
values = [s.strip() for s in value.lower().strip().split(",") if s]
163-
if values == ["all"]:
164-
return ALL_RELEASE_TYPES
165-
elif all(i in ALL_RELEASE_TYPES for i in values):
166-
return values
167-
else:
168-
gha_utils.error(
169-
"Invalid input for `release_types` field, "
170-
f"expected one/all of {ALL_RELEASE_TYPES} but got `{value}`"
171-
)
172-
raise SystemExit(1)
173-
return None
174-
175-
@staticmethod
176-
def clean_skip_pull_request(value: Any) -> bool | None:
177-
if value in [1, "1", True, "true", "True"]:
178-
return True
179-
return None
180-
181-
@staticmethod
182-
def clean_update_version_with(value: Any) -> str | None:
183-
if value and value not in UPDATE_VERSION_WITH_LIST:
184-
gha_utils.error(
185-
"Invalid input for `update_version_with` field, "
186-
f"expected one of {UPDATE_VERSION_WITH_LIST} but got `{value}`"
187-
)
188-
raise SystemExit(1)
189-
elif value:
190-
return value
191-
else:
192-
return None
105+
@validator("release_types", pre=True)
106+
def check_release_types(cls, value: frozenset[str]) -> frozenset[str]:
107+
if value == {"all"}:
108+
return frozenset(["major", "minor", "patch"])
193109

194-
@staticmethod
195-
def clean_extra_workflow_paths(value: Any) -> set[str] | None:
196-
if not value or not isinstance(value, str):
197-
return None
110+
return value
198111

199-
workflow_file_paths = set()
200-
workflow_locations = {s.strip() for s in value.strip().split(",") if s}
112+
@validator("extra_workflow_locations")
113+
def check_extra_workflow_locations(value: frozenset[str]) -> frozenset[str]:
114+
workflow_file_paths = []
201115

202-
for workflow_location in workflow_locations:
116+
for workflow_location in value:
203117
if os.path.isdir(workflow_location):
204-
workflow_file_paths.update(
205-
{str(path) for path in Path(workflow_location).rglob("*.y*ml")}
118+
workflow_file_paths.extend(
119+
[str(path) for path in Path(workflow_location).rglob("*.y*ml")]
206120
)
207121
elif os.path.isfile(workflow_location):
208122
if workflow_location.endswith(".yml") or workflow_location.endswith(
209123
".yaml"
210124
):
211-
workflow_file_paths.add(workflow_location)
125+
workflow_file_paths.append(workflow_location)
212126
else:
213127
gha_utils.warning(
214128
f"Skipping '{workflow_location}' "
215129
"as it is not a valid file or directory"
216130
)
217131

218-
return workflow_file_paths
132+
return frozenset(workflow_file_paths)
219133

220-
@staticmethod
221-
def clean_pull_request_branch(value: Any) -> str | None:
222-
if value and isinstance(value, str):
134+
@validator("pull_request_branch")
135+
def check_pull_request_branch(value: Any) -> str | None:
136+
if isinstance(value, str):
223137
if value.lower() in ["main", "master"]:
224-
gha_utils.error(
138+
raise ValueError(
225139
"Invalid input for `pull_request_branch` field, "
226140
"the action does not support `main` or `master` branches"
227141
)
228-
raise SystemExit(1)
229142
return value
230143
return None

0 commit comments

Comments
 (0)