Skip to content

Commit f01e903

Browse files
committed
initial commit
0 parents  commit f01e903

4 files changed

Lines changed: 189 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.idea
2+
venv

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
GitHub Actions Utils
2+
====================
3+
4+
The purpose of this tool is to work with your GitHub Actions workflows in your repositories.
5+
6+
So far, three main flows are supported:
7+
8+
# List all workflows path and name in a specified repository.
9+
10+
Example:
11+
12+
```shell
13+
python ghautils.py cunla/fakeredis list-workflows
14+
```
15+
will return:
16+
17+
```text
18+
.github/workflows/publish.yml:Upload Python Package
19+
.github/workflows/test.yml:Unit tests
20+
dynamic/github-code-scanning/codeql:CodeQL
21+
dynamic/pages/pages-build-deployment:pages-build-deployment
22+
```
23+
24+
# List all actions `uses` in a workflow
25+
26+
Given a repo and a workflow path, return all actions in the workflow.
27+
28+
Example:
29+
```shell
30+
python ghautils.py cunla/fakeredis list-actions .github/workflows/publish.yml
31+
```
32+
33+
Result
34+
```text
35+
pypa/gh-action-pypi-publish@release/v1
36+
actions/checkout@v3
37+
actions/setup-python@v4
38+
```
39+
40+
# Update all actions in a repository workflow(s)
41+
Show the latest versions of actions used in a repository workflow.
42+
43+
Example:
44+
```shell
45+
python ghautils.py cunla/fakeredis update
46+
```
47+
Result:
48+
```text
49+
.github/workflows/publish.yml
50+
actions/setup-python @ v4 ==> v4.7.0
51+
pypa/gh-action-pypi-publish @ release/v1 ==> v1.8.8
52+
actions/checkout @ v3 ==> v3.5.3
53+
.github/workflows/test.yml
54+
actions/setup-python @ v4 ==> v4.7.0
55+
release-drafter/release-drafter @ v5 ==> v5.24.0
56+
actions/checkout @ v3 ==> v3.5.3
57+
```
58+
59+
# Help messages
60+
61+
```text
62+
usage: ghautils.py [-h] [--github-token GITHUB_TOKEN] repo {list-workflows,list-actions,update} ...
63+
64+
positional arguments:
65+
repo Repository to analyze
66+
{list-workflows,list-actions,update}
67+
list-workflows List github workflows
68+
list-actions List actions in a workflow
69+
update Update actions in github workflows
70+
71+
options:
72+
-h, --help show this help message and exit
73+
--github-token GITHUB_TOKEN
74+
GitHub token to use, by default will use GITHUB_TOKEN environment variable
75+
```

ghautils.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import argparse
2+
import os
3+
from typing import Optional, List, Set, Tuple, Dict
4+
5+
import yaml
6+
from github import Github, Workflow
7+
8+
9+
def parse_args() -> argparse.Namespace:
10+
parser = argparse.ArgumentParser()
11+
parser.add_argument('repo', help='Repository to analyze')
12+
parser.add_argument(
13+
'--github-token', dest='github_token', default=None,
14+
help='GitHub token to use, by default will use GITHUB_TOKEN environment variable')
15+
subcommands = parser.add_subparsers(dest='command', required=True)
16+
list_wfs_cmd_parser = subcommands.add_parser('list-workflows', help='List github workflows')
17+
list_actions_cmd_parser = subcommands.add_parser('list-actions', help='List actions in a workflow')
18+
list_actions_cmd_parser.add_argument('workflow_path', help='Workflow path')
19+
update_gha_cmd_parser = subcommands.add_parser('update', help='Update actions in github workflows')
20+
update_gha_cmd_parser.add_argument("--dry-run", action='store_true', dest='dryrun', help='List updates only')
21+
return parser.parse_args()
22+
23+
24+
class GithubActionsTools(object):
25+
workflows: dict[str, dict[str, Workflow]] = dict() # repo_name -> [path -> workflow]
26+
actions: dict[str, str] = dict() # action_name -> latest_release_tag
27+
28+
def __init__(self, github_token: Optional[str]):
29+
github_token = github_token or os.getenv('GITHUB_TOKEN')
30+
if github_token is None:
31+
raise ValueError('GITHUB_TOKEN must be set')
32+
self.client = Github(login_or_token=github_token)
33+
34+
def get_github_workflows(self, repo_name: str) -> List[Workflow]:
35+
if repo_name in self.workflows:
36+
return list(self.workflows[repo_name].values())
37+
repo = self.client.get_repo(repo_name)
38+
workflows = list(repo.get_workflows())
39+
self.workflows[repo_name] = {wf.path: wf for wf in workflows}
40+
return workflows
41+
42+
def get_workflow_actions(self, repo_name: str, workflow_path: str) -> Set[str]:
43+
44+
self.get_github_workflows(repo_name)
45+
if workflow_path not in self.workflows[repo_name]:
46+
raise ValueError(f'f{workflow_path} not found in workflows for repository {repo_name}, '
47+
f'possible values: {self.workflows[repo_name].keys()}')
48+
repo = self.client.get_repo(repo_name)
49+
workflow_content = repo.get_contents(workflow_path)
50+
workflow = yaml.load(workflow_content.decoded_content, Loader=yaml.CLoader)
51+
res = set()
52+
for job in workflow.get('jobs', dict()).values():
53+
for step in job.get('steps', list()):
54+
if 'uses' in step:
55+
res.add(step['uses'])
56+
return res
57+
58+
def check_for_updates(self, action_name: str) -> Optional[str]:
59+
"""Check whether an action has update, and return the latest version if it does
60+
syntax for uses: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
61+
"""
62+
if '@' not in action_name:
63+
return None
64+
repo_name, current_version = action_name.split('@')
65+
repo = self.client.get_repo(repo_name)
66+
latest_release = repo.get_latest_release()
67+
return latest_release.tag_name if latest_release.tag_name != current_version else None
68+
69+
def get_repo_actions_latest(self, repo_name: str) -> Dict[str, List[Tuple[str, str, Optional[str]]]]:
70+
workflows = self.get_github_workflows(repo_name)
71+
res = dict()
72+
for workflow in workflows:
73+
if not workflow.path.startswith('.github'):
74+
continue
75+
res[workflow.path] = list()
76+
actions = self.get_workflow_actions(repo_name, workflow.path)
77+
for action in actions:
78+
if '@' not in action:
79+
continue
80+
action_name, curr_version = action.split('@')
81+
latest = self.check_for_updates(action)
82+
res[workflow.path].append((action_name, curr_version, latest))
83+
return res
84+
85+
86+
def run():
87+
args = parse_args()
88+
gh = GithubActionsTools(args.github_token)
89+
gh.check_for_updates('actions/checkout@v2')
90+
if args.command == 'list-workflows':
91+
workflows = gh.get_github_workflows(args.repo)
92+
for workflow in workflows:
93+
print(f'{workflow.path}:{workflow.name}')
94+
elif args.command == 'list-actions':
95+
actions = gh.get_workflow_actions(args.repo, args.workflow_path)
96+
for action in actions:
97+
print(action)
98+
elif args.command == 'update':
99+
action_versions = gh.get_repo_actions_latest(args.repo)
100+
for wf in action_versions:
101+
print(f'{wf}:')
102+
for action in action_versions[wf]:
103+
s = f' {action[0]} @ {action[1]}'
104+
if action[2]:
105+
s += f' \t==> {action[2]}'
106+
print(s)
107+
108+
109+
if __name__ == '__main__':
110+
run()

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pygithub==1.59.0
2+
pyyaml==6.0.1

0 commit comments

Comments
 (0)