Skip to content

Commit 36cb148

Browse files
committed
GitHub: Refactor and spread GitHub modules along the feature axis
- Naming things - Cleanups
1 parent a925c04 commit 36cb148

File tree

11 files changed

+529
-494
lines changed

11 files changed

+529
-494
lines changed

docs/backlog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
## Iteration +1
44
- GitHub: Export conversations
5-
- Refactoring: Spread GitHub modules along the feature axis
65

76
## Iteration +2
87
- GitHub: s/created/updated/
@@ -48,3 +47,4 @@
4847
- GitHub/Bugs: Add label `type: Bug`
4948
- Documentation: Include breadcrumbs into static docs, not just README
5049
- Opsgenie: Add subsystem
50+
- Refactoring: Spread GitHub modules along the feature axis

docs/guide/github.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Tap into the GitHub API and generate reports in Markdown format.
44

55
## Features
66

7-
- Report about user activity in [PPP] format.
8-
- Report about CI failures on [GHA].
9-
- Report about bugs and similar important items.
7+
- Actions: Report about CI failures on [GHA].
8+
- Activity: Report about user activity in [PPP] format.
9+
- Attention: Report about bugs and similar important items.
1010

1111
## Setup
1212

@@ -29,24 +29,24 @@ export GH_TOKEN=ghp_600VEZtdzinvalid7K2R86JTiKJAAp1wNwVP
2929
Note that many options are optional. Just omit them in order to expand the
3030
search scope.
3131

32-
### PPP reports
33-
Report about activities of individual authors.
32+
### Actions report
33+
Report about activities of GitHub Actions workflow runs, mostly failing ones.
3434
```shell
35-
rapporto gh ppp --organization=python --author=AA-Turner --timerange="2025-01-01..2025-01-31"
36-
rapporto gh ppp --organization=python --author=AA-Turner --timerange="2025W04"
35+
rapporto gh actions --repository=acme/acme-examples
36+
rapporto gh actions --repositories-file=acme-repositories.txt
3737
```
3838

39-
### CI reports
40-
Report about activities of GitHub Actions workflow runs, mostly failing ones.
39+
### Activity report
40+
Report about activities of individual authors.
4141
```shell
42-
rapporto gh ci --repository=acme/acme-examples
43-
rapporto gh ci --repositories-file=acme-repositories.txt
42+
rapporto gh activity --organization=python --author=AA-Turner --timerange="2025-01-01..2025-01-31"
43+
rapporto gh activity --organization=python --author=AA-Turner --timerange="2025W04"
4444
```
4545

46-
### Importance reports
46+
### Attention report
4747
Report about important items that deserve your attention, bugs first.
4848
```shell
49-
rapporto gh att --organization=python --timerange="2025W07"
49+
rapporto gh attention --organization=python --timerange="2025W07"
5050
```
5151
If you want to explore your personal repositories, please use the
5252
`--organization` option with your username, e.g. `--organization=AA-Turner`.

docs/index.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,39 +27,48 @@ project
2727

2828
## Synopsis
2929

30+
### GitHub
31+
3032
::::{tab-set}
3133

32-
:::{tab-item} GitHub: PPP
34+
:::{tab-item} Actions
3335
```{code-block} shell
34-
:caption: Report about user activity on [GitHub] in [PPP] format.
35-
rapporto gh ppp --organization=python --author=AA-Turner --timerange="2025W04"
36+
:caption: Report about CI failures on [GHA].
37+
rapporto gh actions --repository=acme/acme-examples
3638
```
3739
:::
3840

39-
:::{tab-item} GitHub: CI
41+
:::{tab-item} Activity
4042
```{code-block} shell
41-
:caption: Report about CI failures on [GHA].
42-
rapporto gh ci --repository=acme/acme-examples
43+
:caption: Report about user activity on [GitHub] in [PPP] format.
44+
rapporto gh activity --organization=python --author=AA-Turner --timerange="2025W04"
4345
```
4446
:::
4547

46-
:::{tab-item} GitHub: Importance
48+
:::{tab-item} Attention
4749
```{code-block} shell
4850
:caption: Report about bugs and similar important items on [GitHub].
49-
rapporto gh att --organization=python --timerange="2025W07"
51+
rapporto gh attention --organization=python --timerange="2025W07"
5052
```
5153
:::
5254

53-
:::{tab-item} Slack: Export thread
55+
::::
56+
57+
### Slack
58+
59+
::::{tab-set}
60+
61+
:::{tab-item} Export conversation
5462
```{code-block} shell
55-
:caption: Export [Slack] conversation / thread.
63+
:caption: Export [Slack] conversation thread.
5664
rapporto slack export https://acme.slack.com/archives/D018V8WDABA/p1738873838427919
5765
```
5866
:::
5967

6068
::::
6169

6270

71+
6372
```{include} readme.md
6473
:start-line: 15
6574
```

src/rapporto/github/actions.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import dataclasses
2+
import datetime as dt
3+
import logging
4+
import typing as t
5+
from collections import OrderedDict
6+
from pathlib import Path
7+
8+
from munch import Munch, munchify
9+
from tqdm import tqdm
10+
11+
from rapporto.github.model import (
12+
MarkdownContent,
13+
)
14+
from rapporto.github.util import GitHubHttpClient
15+
from rapporto.util import sanitize_title
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class GitHubActionsReport:
21+
"""
22+
Report about failed outcomes of GitHub Actions workflow runs.
23+
"""
24+
25+
def __init__(self, inquiry: "MultiRepositoryInquiry"):
26+
self.inquiry = inquiry
27+
self.request = GitHubActionsRequest(inquiry)
28+
self.runs_failed = self.request.runs_failed
29+
self.runs_pr_success = self.request.runs_pr_success
30+
31+
@property
32+
def runs(self):
33+
"""
34+
All failed runs, modulo subsequent succeeding PR runs.
35+
"""
36+
for run in self.runs_failed:
37+
if run.event == "pull_request" and self.is_pr_successful(run):
38+
continue
39+
yield run
40+
41+
def is_pr_successful(self, run):
42+
"""
43+
Find out if a given run has others that succeeded afterward.
44+
"""
45+
for pr in self.runs_pr_success:
46+
if (
47+
run.repository.full_name == pr.repository.full_name
48+
and run.head_branch == pr.head_branch
49+
):
50+
if pr.conclusion == "success":
51+
return True
52+
return False
53+
54+
@property
55+
def markdown(self):
56+
mdc = MarkdownContent(labels=self.request.event_section_map)
57+
for run in self.runs:
58+
mdc.add(run.event, run.markdown)
59+
return f"""
60+
# CI failures report {dt.datetime.now().strftime("%Y-%m-%d")}
61+
A report about GitHub Actions workflow runs that failed recently (now-{self.request.DELTA_HOURS}h).
62+
{mdc.render()}
63+
""".strip() # noqa: E501
64+
65+
def print(self):
66+
print(self.markdown)
67+
68+
69+
class GitHubActionsRequest:
70+
"""
71+
Fetch outcomes of GitHub Actions workflow runs.
72+
73+
Possible event types are: dynamic, pull_request, push, schedule
74+
"""
75+
76+
DELTA_HOURS = 24
77+
78+
event_section_map = OrderedDict(
79+
schedule="Schedule",
80+
pull_request="Pull requests",
81+
# push="Pushes",
82+
dynamic="Dynamic",
83+
)
84+
85+
def __init__(self, inquiry: "MultiRepositoryInquiry"):
86+
self.inquiry = inquiry
87+
self.session = GitHubHttpClient.session
88+
89+
@property
90+
def yesterday(self) -> str:
91+
"""
92+
Compute the start timestamp in ISO format.
93+
Truncate the ISO format after the hour, to permit caching.
94+
"""
95+
return dt.datetime.strftime(
96+
dt.datetime.now() - dt.timedelta(hours=self.DELTA_HOURS), "%Y-%m-%dT%H"
97+
)
98+
99+
def fetch(self, filter: "ActionsFilter") -> t.List["ActionsOutcome"]: # noqa:A002
100+
outcomes = []
101+
for repository in tqdm(
102+
self.inquiry.repositories,
103+
desc=f"Fetching failed GitHub Actions outcomes for event={filter.event}",
104+
leave=False,
105+
):
106+
url = f"https://api.github.com/repos/{repository}/actions/runs?{filter.query}"
107+
logger.debug(f"Using API URL: {url}")
108+
response = self.session.get(url)
109+
if response.status_code == 404:
110+
continue
111+
response.raise_for_status()
112+
runs = munchify(response.json()).workflow_runs
113+
for run in runs:
114+
outcome = ActionsOutcome(
115+
id=run.id,
116+
event=run.event,
117+
status=run.status,
118+
conclusion=run.conclusion,
119+
repository=run.repository,
120+
name=run.display_title,
121+
url=run.html_url,
122+
started=run.run_started_at,
123+
head_branch=run.head_branch,
124+
)
125+
outcomes.append(outcome)
126+
return outcomes
127+
128+
@property
129+
def runs_failed(self):
130+
return self.fetch(filter=ActionsFilter(status="failure", created=f">{self.yesterday}"))
131+
132+
@property
133+
def runs_pr_success(self):
134+
return self.fetch(
135+
filter=ActionsFilter(
136+
event="pull_request", status="success", created=f">{self.yesterday}"
137+
)
138+
)
139+
140+
141+
@dataclasses.dataclass
142+
class MultiRepositoryInquiry:
143+
repositories: t.List[str]
144+
145+
@classmethod
146+
def make(cls, repository: str, repositories_file: Path = None) -> "MultiRepositoryInquiry":
147+
if repository:
148+
return cls(repositories=[repository])
149+
elif repositories_file:
150+
return cls(repositories=repositories_file.read_text().splitlines())
151+
else:
152+
raise ValueError("No repository specified")
153+
154+
155+
@dataclasses.dataclass
156+
class ActionsFilter:
157+
event: t.Optional[str] = None
158+
status: t.Optional[str] = None
159+
created: t.Optional[str] = None
160+
161+
@property
162+
def query(self) -> str:
163+
expression = []
164+
if self.event:
165+
expression.append(f"event={self.event}")
166+
if self.status:
167+
expression.append(f"status={self.status}")
168+
if self.created:
169+
expression.append(f"created={self.created}")
170+
return "&".join(expression)
171+
172+
173+
@dataclasses.dataclass
174+
class ActionsOutcome:
175+
id: int
176+
event: str
177+
status: str
178+
conclusion: str
179+
repository: Munch
180+
name: str
181+
url: str
182+
started: str
183+
head_branch: str
184+
185+
@property
186+
def markdown(self):
187+
title = sanitize_title(f"{self.repository.full_name}: {self.name}")
188+
return f"- [{title}]({self.url})"

0 commit comments

Comments
 (0)