Skip to content

Commit 1d10a67

Browse files
committed
chore:ruff formatting
1 parent c25f7a7 commit 1d10a67

4 files changed

Lines changed: 630 additions & 557 deletions

File tree

gha_cli/cli.py

Lines changed: 105 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,28 @@
77
import click
88
import coloredlogs
99
import yaml
10-
from github import Github, Workflow, UnknownObjectException
10+
from github import Github, UnknownObjectException
1111
from github.Organization import Organization
1212
from github.PaginatedList import PaginatedList
1313

1414
from gha_cli.scanner import Org, print_orgs_as_csvs
1515

16-
coloredlogs.install(level='INFO')
16+
coloredlogs.install(level="INFO")
1717
logger = logging.getLogger()
1818

19-
ActionVersion = namedtuple('ActionVersion', ['name', 'current', 'latest'])
19+
ActionVersion = namedtuple("ActionVersion", ["name", "current", "latest"])
2020

2121
FLAG_COMPARE_EXACT_VERSION = False
2222

2323

2424
def compare_versions(v1: str, v2: str) -> int:
25-
"""Compare two versions, return 1 if v1 > v2, 0 if v1 == v2, -1 if v1 < v2
26-
"""
27-
if v1.startswith('v'):
25+
"""Compare two versions, return 1 if v1 > v2, 0 if v1 == v2, -1 if v1 < v2"""
26+
if v1.startswith("v"):
2827
v1 = v1[1:]
29-
if v2.startswith('v'):
28+
if v2.startswith("v"):
3029
v2 = v2[1:]
31-
v1 = v1.split('.')
32-
v2 = v2.split('.')
30+
v1 = v1.split(".")
31+
v2 = v2.split(".")
3332
try:
3433
compare_count = max(len(v1), len(v2)) if FLAG_COMPARE_EXACT_VERSION else 1
3534
for i in range(compare_count):
@@ -40,7 +39,7 @@ def compare_versions(v1: str, v2: str) -> int:
4039
if v1_i < v2_i:
4140
return -1
4241
except ValueError:
43-
logging.warning(f'Could not compare versions {v1} and {v2}')
42+
logging.warning(f"Could not compare versions {v1} and {v2}")
4443
return 0
4544

4645

@@ -53,47 +52,45 @@ def __init__(self, github_token: str):
5352

5453
@staticmethod
5554
def is_local_repo(repo_name: str) -> bool:
56-
return os.path.exists(repo_name) and os.path.exists(os.path.join(repo_name, '.git'))
55+
return os.path.exists(repo_name) and os.path.exists(os.path.join(repo_name, ".git"))
5756

5857
@staticmethod
5958
def list_full_paths(path: str) -> set[str]:
6059
if not os.path.exists(path):
6160
return set()
62-
return {os.path.join(path, file)
63-
for file in os.listdir(path)
64-
if file.endswith(('.yml', '.yaml'))}
61+
return {os.path.join(path, file) for file in os.listdir(path) if file.endswith((".yml", ".yaml"))}
6562

6663
def get_workflow_actions(self, repo_name: str, workflow_path: str) -> Set[str]:
6764
workflow_content = self._get_workflow_file_content(repo_name, workflow_path)
6865
workflow = yaml.load(workflow_content, Loader=yaml.CLoader)
6966
res = set()
70-
for job in workflow.get('jobs', dict()).values():
71-
for step in job.get('steps', list()):
72-
if 'uses' in step:
73-
res.add(step['uses'])
67+
for job in workflow.get("jobs", dict()).values():
68+
for step in job.get("steps", list()):
69+
if "uses" in step:
70+
res.add(step["uses"])
7471
return res
7572

7673
def check_for_updates(self, action_name: str) -> Optional[str]:
7774
"""Check whether an action has an update, and return the latest version if it does syntax for uses:
7875
https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
7976
"""
80-
if '@' not in action_name:
77+
if "@" not in action_name:
8178
return None
82-
repo_name, current_version = action_name.split('@')
83-
logging.debug(f'Checking for updates for {action_name}: Getting repo {repo_name}')
79+
repo_name, current_version = action_name.split("@")
80+
logging.debug(f"Checking for updates for {action_name}: Getting repo {repo_name}")
8481
if repo_name in self.actions_latest_release:
8582
latest_release = self.actions_latest_release[repo_name]
8683
logging.debug(f"Found in cache {repo_name}: {latest_release}")
8784
return latest_release if compare_versions(latest_release, current_version) else None
8885
repo = self.client.get_repo(repo_name)
89-
logging.debug(f'Getting latest release for repository: {repo_name}')
86+
logging.debug(f"Getting latest release for repository: {repo_name}")
9087
try:
9188
latest_release = repo.get_latest_release()
9289
if compare_versions(latest_release.tag_name, current_version):
9390
self.actions_latest_release[repo_name] = latest_release.tag_name
9491
return latest_release.tag_name
9592
except UnknownObjectException:
96-
logging.warning(f'No releases found for repository: {repo_name}')
93+
logging.warning(f"No releases found for repository: {repo_name}")
9794
return None
9895

9996
def get_repo_actions_latest(self, repo_name: str) -> Dict[str, List[ActionVersion]]:
@@ -103,9 +100,9 @@ def get_repo_actions_latest(self, repo_name: str) -> Dict[str, List[ActionVersio
103100
res[path] = list()
104101
actions = self.get_workflow_actions(repo_name, path)
105102
for action in actions:
106-
if '@' not in action:
103+
if "@" not in action:
107104
continue
108-
action_name, curr_version = action.split('@')
105+
action_name, curr_version = action.split("@")
109106
if action not in self.actions_latest_release:
110107
latest = self.check_for_updates(action)
111108
self.actions_latest_release[action] = latest
@@ -121,33 +118,34 @@ def get_repo_workflow_names(self, repo_name: str) -> Dict[str, str]:
121118
try:
122119
content = self._get_workflow_file_content(repo_name, path)
123120
yaml_content = yaml.load(content, Loader=yaml.CLoader)
124-
res[path] = yaml_content.get('name', path)
121+
res[path] = yaml_content.get("name", path)
125122
except FileNotFoundError as ex:
126123
logging.warning(ex)
127124
return res
128125

129126
def update_actions(
130-
self, repo_name: str, workflow_path: str,
131-
updates: List[ActionVersion],
132-
commit_msg: str,
127+
self,
128+
repo_name: str,
129+
workflow_path: str,
130+
updates: List[ActionVersion],
131+
commit_msg: str,
133132
) -> None:
134133
workflow_content = self._get_workflow_file_content(repo_name, workflow_path)
135134
if isinstance(workflow_content, bytes):
136135
workflow_content = workflow_content.decode()
137136
for update in updates:
138137
if update.latest is None:
139138
continue
140-
current_action = f'{update.name}@{update.current}'
141-
latest_action = f'{update.name}@{update.latest}'
139+
current_action = f"{update.name}@{update.current}"
140+
latest_action = f"{update.name}@{update.latest}"
142141
workflow_content = workflow_content.replace(current_action, latest_action)
143142
self._update_workflow_content(repo_name, workflow_path, workflow_content, commit_msg)
144143

145-
def _update_workflow_content(
146-
self, repo_name: str, workflow_path: str, workflow_content: str, commit_msg: str):
144+
def _update_workflow_content(self, repo_name: str, workflow_path: str, workflow_content: str, commit_msg: str):
147145
if self.is_local_repo(repo_name):
148-
with open(workflow_path, 'w') as f:
146+
with open(workflow_path, "w") as f:
149147
f.write(workflow_content)
150-
click.secho(f'Updated workflow in {workflow_path}', fg='cyan')
148+
click.secho(f"Updated workflow in {workflow_path}", fg="cyan")
151149
return
152150

153151
# remote
@@ -159,24 +157,21 @@ def _update_workflow_content(
159157
workflow_content,
160158
current_content.sha,
161159
)
162-
click.secho(f'Committed changes to workflow in {repo_name}:{workflow_path}', fg='cyan')
160+
click.secho(f"Committed changes to workflow in {repo_name}:{workflow_path}", fg="cyan")
163161
return res
164162

165163
def _get_github_workflow_filenames(self, repo_name: str) -> Set[str]:
166164
if repo_name in self._wf_cache:
167165
return set(self._wf_cache[repo_name].keys())
168166
# local
169167
if self.is_local_repo(repo_name):
170-
return self.list_full_paths(os.path.join(repo_name, '.github', 'workflows'))
171-
if repo_name.startswith('.'):
172-
click.secho(f'{repo_name} is not a local repo and does not start with owner/repo', fg='red', err=True)
173-
raise ValueError(f'{repo_name} is not a local repo and does not start with owner/repo')
168+
return self.list_full_paths(os.path.join(repo_name, ".github", "workflows"))
169+
if repo_name.startswith("."):
170+
click.secho(f"{repo_name} is not a local repo and does not start with owner/repo", fg="red", err=True)
171+
raise ValueError(f"{repo_name} is not a local repo and does not start with owner/repo")
174172
# Remote
175173
repo = self.client.get_repo(repo_name)
176-
self._wf_cache[repo_name] = {
177-
wf.path: wf
178-
for wf in repo.get_workflows()
179-
if wf.path.startswith('.github/')}
174+
self._wf_cache[repo_name] = {wf.path: wf for wf in repo.get_workflows() if wf.path.startswith(".github/")}
180175
return set(self._wf_cache[repo_name].keys())
181176

182177
def _get_workflow_file_content(self, repo_name: str, workflow_path: str) -> Union[str, bytes]:
@@ -185,20 +180,24 @@ def _get_workflow_file_content(self, repo_name: str, workflow_path: str) -> Unio
185180
if self.is_local_repo(repo_name):
186181
if not os.path.exists(workflow_path):
187182
click.echo(
188-
f'f{workflow_path} not found in workflows for repository {repo_name}, '
189-
f'possible values: {workflow_paths}', err=True)
183+
f"f{workflow_path} not found in workflows for repository {repo_name}, "
184+
f"possible values: {workflow_paths}",
185+
err=True,
186+
)
190187
with open(workflow_path) as f:
191188
return f.read()
192189

193190
if workflow_path not in workflow_paths:
194191
click.echo(
195-
f'f{workflow_path} not found in workflows for repository {repo_name}, '
196-
f'possible values: {workflow_paths}', err=True)
192+
f"f{workflow_path} not found in workflows for repository {repo_name}, "
193+
f"possible values: {workflow_paths}",
194+
err=True,
195+
)
197196
repo = self.client.get_repo(repo_name)
198197
try:
199198
workflow_content = repo.get_contents(workflow_path)
200199
except UnknownObjectException:
201-
raise FileNotFoundError(f'Workflow not found in repository: {repo_name}, path: {workflow_path}')
200+
raise FileNotFoundError(f"Workflow not found in repository: {repo_name}, path: {workflow_path}")
202201
return workflow_content.decoded_content
203202

204203

@@ -209,95 +208,114 @@ def _get_workflow_file_content(self, repo_name: str, workflow_path: str) -> Unio
209208

210209

211210
@click.group(invoke_without_command=True)
212-
@click.option('-v', '--verbose', count=True,
213-
help="Increase verbosity, can be used multiple times to increase verbosity")
214211
@click.option(
215-
'--repo', default='.', show_default=True, type=str,
216-
help='Repository to analyze, can be a local directory or a {OWNER}/{REPO} format', )
212+
"-v", "--verbose", count=True, help="Increase verbosity, can be used multiple times to increase verbosity"
213+
)
217214
@click.option(
218-
'--github-token', default=os.getenv('GITHUB_TOKEN'), type=str, show_default=False,
219-
help='GitHub token to use, by default will use GITHUB_TOKEN environment variable')
215+
"--repo",
216+
default=".",
217+
show_default=True,
218+
type=str,
219+
help="Repository to analyze, can be a local directory or a {OWNER}/{REPO} format",
220+
)
220221
@click.option(
221-
'--compare-exact-versions', is_flag=True, default=False,
222-
help="Compare versions using all semantic and not only major versions, e.g., v1 will be upgraded to v1.2.3", )
222+
"--github-token",
223+
default=os.getenv("GITHUB_TOKEN"),
224+
type=str,
225+
show_default=False,
226+
help="GitHub token to use, by default will use GITHUB_TOKEN environment variable",
227+
)
228+
@click.option(
229+
"--compare-exact-versions",
230+
is_flag=True,
231+
default=False,
232+
help="Compare versions using all semantic and not only major versions, e.g., v1 will be upgraded to v1.2.3",
233+
)
223234
@click.pass_context
224235
def cli(ctx, verbose: int, repo: str, github_token: Optional[str], compare_exact_versions: bool):
225236
if verbose == 1:
226-
coloredlogs.install(level='INFO')
237+
coloredlogs.install(level="INFO")
227238
if verbose > 1:
228-
coloredlogs.install(level='DEBUG')
239+
coloredlogs.install(level="DEBUG")
229240
ctx.ensure_object(dict)
230241
global FLAG_COMPARE_EXACT_VERSION
231242
FLAG_COMPARE_EXACT_VERSION = compare_exact_versions
232243
if not github_token:
233-
click.secho(GITHUB_ACTION_NOT_PROVIDED_MSG, fg='yellow', err=True)
234-
ctx.obj['gh'] = GithubActionsTools(github_token)
235-
ctx.obj['repo'] = repo
244+
click.secho(GITHUB_ACTION_NOT_PROVIDED_MSG, fg="yellow", err=True)
245+
ctx.obj["gh"] = GithubActionsTools(github_token)
246+
ctx.obj["repo"] = repo
236247
if not ctx.invoked_subcommand:
237248
ctx.invoke(update_actions)
238249

239250

240-
@cli.command(help='Show actions required updates in repository workflows')
251+
@cli.command(help="Show actions required updates in repository workflows")
241252
@click.option(
242-
'-u', '--update', is_flag=True, default=False,
243-
help='Update actions in workflows (For remote repos: make changes and commit, for local repos: update files', )
253+
"-u",
254+
"--update",
255+
is_flag=True,
256+
default=False,
257+
help="Update actions in workflows (For remote repos: make changes and commit, for local repos: update files",
258+
)
244259
@click.option(
245-
'-commit-msg',
246-
default='chore(ci):update actions', type=str, show_default=True,
247-
help='Commit msg, only relevant when remote repo')
260+
"-commit-msg",
261+
default="chore(ci):update actions",
262+
type=str,
263+
show_default=True,
264+
help="Commit msg, only relevant when remote repo",
265+
)
248266
@click.pass_context
249267
def update_actions(ctx, update: bool, commit_msg: str):
250-
gh, repo = ctx.obj['gh'], ctx.obj['repo']
251-
workflow_names = (gh.get_repo_workflow_names(repo))
268+
gh, repo = ctx.obj["gh"], ctx.obj["repo"]
269+
workflow_names = gh.get_repo_workflow_names(repo)
252270
workflow_action_versions = gh.get_repo_actions_latest(repo)
253271
max_action_name_length, max_version_length = 0, 0
254272
for workflow_path, actions in workflow_action_versions.items():
255273
for action in workflow_action_versions[workflow_path]:
256274
max_action_name_length = max(max_action_name_length, len(action.name))
257275
max_version_length = max(max_version_length, len(action.current))
258276
for workflow_path, workflow_name in workflow_names.items():
259-
click.secho(f'{workflow_path} ({click.style(workflow_name, fg="bright_cyan")}):', fg='bright_blue')
277+
click.secho(f"{workflow_path} ({click.style(workflow_name, fg='bright_cyan')}):", fg="bright_blue")
260278
for action in workflow_action_versions[workflow_path]:
261-
s = f'\t{action.name:<{max_action_name_length + 5}} {action.current:>{max_version_length + 2}}'
279+
s = f"\t{action.name:<{max_action_name_length + 5}} {action.current:>{max_version_length + 2}}"
262280
if action.latest:
263-
old_version = action.current.split('.')
264-
new_version = action.latest.split('.')
265-
color = 'red' if new_version[0] != old_version[0] else 'cyan'
266-
s += ' ==> ' + click.style(f'{action.latest}', fg=color)
281+
old_version = action.current.split(".")
282+
new_version = action.latest.split(".")
283+
color = "red" if new_version[0] != old_version[0] else "cyan"
284+
s += " ==> " + click.style(f"{action.latest}", fg=color)
267285
click.echo(s)
268286
if not update:
269287
return
270288
for workflow in workflow_action_versions:
271289
gh.update_actions(repo, workflow, workflow_action_versions[workflow], commit_msg)
272290

273291

274-
@cli.command(help='List actions in a workflow')
275-
@click.argument('workflow')
292+
@cli.command(help="List actions in a workflow")
293+
@click.argument("workflow")
276294
@click.pass_context
277295
def list_actions(ctx, workflow: str):
278-
actions = ctx.obj['gh'].get_workflow_actions(ctx.obj['repo'], workflow)
296+
actions = ctx.obj["gh"].get_workflow_actions(ctx.obj["repo"], workflow)
279297
for action in actions:
280298
click.echo(action)
281299

282300

283-
@cli.command(help='List workflows in repository')
301+
@cli.command(help="List workflows in repository")
284302
@click.pass_context
285303
def list_workflows(ctx):
286-
workflow_paths = (ctx.obj['gh'].get_repo_workflow_names(ctx.obj['repo']))
304+
workflow_paths = ctx.obj["gh"].get_repo_workflow_names(ctx.obj["repo"])
287305
for path, name in workflow_paths.items():
288-
click.echo(f'{path} - {name}')
306+
click.echo(f"{path} - {name}")
289307

290308

291-
@cli.command(help='Analyze organizations')
292-
@click.option('-x', '--exclude', multiple=True, default=[], help='Exclude orgs')
309+
@cli.command(help="Analyze organizations")
310+
@click.option("-x", "--exclude", multiple=True, default=[], help="Exclude orgs")
293311
@click.pass_context
294312
def analyze_orgs(ctx, exclude: Set[str] = None):
295-
gh_client: Github = ctx.obj['gh'].client
313+
gh_client: Github = ctx.obj["gh"].client
296314
exclude = exclude or {}
297315
exclude = set(exclude)
298316
current_user = gh_client.get_user()
299317
gh_orgs: PaginatedList[Organization] = current_user.get_orgs()
300-
logging.info(f'Analyzing {gh_orgs.totalCount} organizations')
318+
logging.info(f"Analyzing {gh_orgs.totalCount} organizations")
301319
orgs: List[Org] = []
302320
for gh_org in gh_orgs:
303321
if gh_org.login in exclude:
@@ -307,5 +325,5 @@ def analyze_orgs(ctx, exclude: Set[str] = None):
307325
print_orgs_as_csvs(orgs)
308326

309327

310-
if __name__ == '__main__':
328+
if __name__ == "__main__":
311329
cli(obj={})

0 commit comments

Comments
 (0)