11import json
22import os
33import time
4- from collections . abc import Mapping
4+ from enum import Enum
55from pathlib import Path
6- from typing import Any , NamedTuple
6+ from typing import Any
77
88import 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