Skip to content

Commit 5223938

Browse files
committed
feat:github scanner
1 parent b7785de commit 5223938

3 files changed

Lines changed: 200 additions & 1 deletion

File tree

gha_cli/scanner.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import logging
2+
import os
3+
from dataclasses import dataclass, fields
4+
from datetime import datetime, timedelta
5+
from typing import List, Optional, Set
6+
7+
import click
8+
import coloredlogs
9+
from github import Github
10+
from github.Organization import Organization
11+
from github.PaginatedList import PaginatedList
12+
from github.Repository import Repository
13+
14+
logging.basicConfig(level=logging.INFO)
15+
THIS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
16+
17+
coloredlogs.install(level='DEBUG')
18+
19+
20+
@dataclass
21+
class CsvClass:
22+
IGNORE_FIELDS = []
23+
24+
def get_attributes(self) -> List[str]:
25+
return [a.name for a in fields(self.__class__)
26+
if a.name not in self.IGNORE_FIELDS]
27+
28+
def csv_header(self) -> str:
29+
return ','.join(self.get_attributes())
30+
31+
def csv_str(self) -> str:
32+
return ','.join([str(getattr(self, attr)) for attr in self.get_attributes()])
33+
34+
35+
@dataclass
36+
class Repo(CsvClass):
37+
name: str
38+
is_private: bool
39+
is_archived: bool
40+
branches_count: int
41+
collaborators_count: int
42+
is_active: bool
43+
has_issues: bool
44+
has_pull_requests: bool
45+
size: int
46+
large_repo: bool
47+
is_template: bool
48+
forks_count: int = 0
49+
50+
@classmethod
51+
def from_github_repo(cls, repo: Repository):
52+
return cls(
53+
name=repo.name,
54+
is_private=repo.private,
55+
is_archived=repo.archived,
56+
branches_count=repo.get_branches().totalCount,
57+
collaborators_count=repo.get_collaborators().totalCount,
58+
is_active=repo.get_commits(since=datetime.now() - timedelta(days=365)).totalCount > 0,
59+
has_issues=repo.has_issues,
60+
has_pull_requests=repo.get_pulls().totalCount > 0,
61+
size=repo.size,
62+
large_repo=repo.size > 1024 * 1024,
63+
is_template=repo.is_template,
64+
forks_count=repo.forks_count,
65+
)
66+
67+
68+
@dataclass
69+
class Org(CsvClass):
70+
IGNORE_FIELDS = ['repositories', ]
71+
name: str
72+
repositories: List[Repo]
73+
members_count: int
74+
teams_count: int
75+
repositories_count: int = 0
76+
77+
def __post_init__(self):
78+
self.repositories_count = len(self.repositories)
79+
80+
@classmethod
81+
def from_github_org(cls, org: Organization):
82+
gh_repositories: PaginatedList[Repository] = org.get_repos()
83+
repositories: List[Repo] = []
84+
for gh_repo in gh_repositories:
85+
repo = Repo.from_github_repo(gh_repo)
86+
repositories.append(repo)
87+
88+
return cls(
89+
name=org.name,
90+
repositories=repositories,
91+
members_count=org.get_members().totalCount,
92+
teams_count=org.get_teams().totalCount,
93+
)
94+
95+
96+
GITHUB_ACTION_NOT_PROVIDED_MSG = """GitHub connection token not provided.
97+
You might not be able to make the changes to remote repositories.
98+
You can provide it using GITHUB_TOKEN environment variable or --github-token option.
99+
"""
100+
101+
102+
@click.group(invoke_without_command=True)
103+
@click.option(
104+
'--github-token', default=os.getenv('GITHUB_TOKEN'), type=str, show_default=False,
105+
help='GitHub token to use, by default will use GITHUB_TOKEN environment variable')
106+
@click.pass_context
107+
def cli(ctx, github_token: Optional[str]):
108+
ctx.ensure_object(dict)
109+
if not github_token:
110+
click.secho(GITHUB_ACTION_NOT_PROVIDED_MSG, fg='yellow', err=True)
111+
exit(1)
112+
ctx.obj['gh'] = Github(github_token)
113+
if not ctx.invoked_subcommand:
114+
ctx.invoke(analyze_orgs)
115+
116+
117+
def _print_data(orgs: List[Org]):
118+
if len(orgs) == 0:
119+
return
120+
121+
click.echo(orgs[0].csv_header())
122+
for org in orgs:
123+
click.echo(org.csv_str())
124+
125+
for org in orgs:
126+
logging.info(f'Analyzing repos for {org.name}')
127+
if len(org.repositories) == 0:
128+
continue
129+
click.echo(org.repositories[0].csv_header())
130+
for repo in org.repositories:
131+
click.echo(repo.csv_str())
132+
133+
134+
@cli.command(help='Analyze organizations')
135+
@click.option('-x', '--exclude', multiple=True, default=[], help='Exclude orgs')
136+
@click.pass_context
137+
def analyze_orgs(ctx, exclude: Set[str] = None):
138+
gh_client: Github = ctx.obj['gh']
139+
exclude = exclude or {}
140+
exclude = set(exclude)
141+
current_user = gh_client.get_user()
142+
gh_orgs: PaginatedList[Organization] = current_user.get_orgs()
143+
logging.info(f'Analyzing {gh_orgs.totalCount} organizations')
144+
orgs: List[Org] = []
145+
for gh_org in gh_orgs:
146+
if gh_org.login in exclude:
147+
continue
148+
org = Org.from_github_org(gh_org)
149+
orgs.append(org)
150+
_print_data(orgs)
151+
152+
153+
if __name__ == '__main__':
154+
# print(Org(name='test', repositories=[], members_count=1, teams_count=1).csv_header())
155+
cli(obj={})

poetry.lock

Lines changed: 43 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry.scripts]
66
github-actions-cli = "gha_cli.cli:cli"
7+
github-scanner = "gha_cli.scanner:cli"
78

89
[tool.poetry]
910
name = "github-actions-cli"
@@ -47,6 +48,7 @@ pygithub = "^1.59"
4748
pyyaml = "^6.0"
4849
click = "^8.1"
4950
cffi = "^1.16"
51+
coloredlogs = "^15.0.1"
5052

5153
[tool.poetry.dev-dependencies]
5254
poetry = "^1.5"

0 commit comments

Comments
 (0)