-
Notifications
You must be signed in to change notification settings - Fork 88
packages in pipe report #680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
622fa7e
package in pipe html report generator
glehmann 74c05e8
run pkg_in_pipe in docker
glehmann bf0346c
don't fail when plane or koji are not responding
glehmann dc7daa3
sort the packages by reversed build id
glehmann 725ad1d
add the pull request matching the koji build
glehmann 37f8c7f
also search the plane cards with the PR urls
glehmann 7fbe070
include all the PRs merged after the previous build
glehmann ec8652f
cache github calls
glehmann 0a5bd0c
generate the report even without github
glehmann 31f4928
add a maintainer column
glehmann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
report.html | ||
.cache |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
FROM fedora:41 | ||
RUN dnf install -y koji python3-requests python3-pip python3-pygithub | ||
RUN pip install specfile diskcache | ||
VOLUME /tmp/pkg_in_pipe.cache | ||
ADD pkg_in_pipe.py / | ||
ENTRYPOINT ["/pkg_in_pipe.py"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Packages in pipe report generator | ||
|
||
Generates an html report with the packages in the current tags. | ||
|
||
# Requirements | ||
|
||
You'll need a few extra python modules: | ||
* koji | ||
* requests | ||
* specfile | ||
* pygithub | ||
|
||
The user running the generator must have a working configuration for koji (in `~/.koji`). | ||
A plane token with enough rights to list the cards in the XCPNG project must be passed either through the `PLANE_TOKEN` | ||
environment variable or the `--plane-token` command line option. | ||
|
||
An extra `--generated-info` command line option may be used to add some info about the report generation process. | ||
|
||
# Run in docker | ||
|
||
Before running in docker, the docker image must be built with: | ||
|
||
```sh | ||
docker build -t pkg_in_pipe . | ||
``` | ||
|
||
A volume needs to be available to store the cache: | ||
|
||
```sh | ||
docker volume create pkg_in_pipe_cache | ||
``` | ||
|
||
Several options are required to run the generator in docker: | ||
|
||
* a `PLANE_TOKEN` environment variable with the rights required to request all the cards in the XCPNG project; | ||
* a `GITHUB_TOKEN` environment variable with (at least) the `public_repo` scope; | ||
* a (read only) mount of a directory containing the requeried certificates to connect to koji in `/root/.koji` | ||
* a mount of the output directory in `/output` | ||
* the path of the generated report | ||
|
||
```sh | ||
docker run \ | ||
-v ~/.koji:/root/.koji:z \ | ||
-e PLANE_TOKEN=<plane token> \ | ||
-e GITHUB_TOKEN=<github token> \ | ||
-v /out/dir:/output:z \ | ||
-v pkg_in_pipe_cache:/tmp/pkg_in_pipe.cache \ | ||
pkg_in_pipe /output/index.html | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
#!/usr/bin/env python | ||
|
||
import argparse | ||
import io | ||
import json | ||
import os | ||
import re | ||
from datetime import datetime | ||
from textwrap import dedent | ||
ydirson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
from typing import cast | ||
from urllib.request import urlopen | ||
|
||
import diskcache | ||
import github | ||
import koji | ||
import requests | ||
from github.Commit import Commit | ||
from github.GithubException import BadCredentialsException | ||
from github.PullRequest import PullRequest | ||
|
||
|
||
def print_header(out): | ||
print(dedent(''' | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>XCP-ng Package Update</title> | ||
<script src="https://cdn.tailwindcss.com"></script> | ||
</head> | ||
<body class="bg-gray-400 text-center"> | ||
'''), file=out) | ||
|
||
def print_plane_warning(out): | ||
print(dedent(''' | ||
<div class="px-3 py-3"> | ||
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert"> | ||
<p class="font-bold">Plane malfunction</p> | ||
<p>The issues could not be retrieved from plane.</p> | ||
</div> | ||
</div>'''), file=out) | ||
|
||
def print_github_warning(out): | ||
print(dedent(''' | ||
<div class="px-3 py-3"> | ||
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert"> | ||
<p class="font-bold">Github access problem</p> | ||
<p>The pull requests come from the cache and may not be up to date.</p> | ||
</div> | ||
</div>'''), file=out) | ||
|
||
def print_koji_error(out): | ||
print(dedent(''' | ||
<div class="px-3 py-3"> | ||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert"> | ||
<strong class="font-bold">Koji error!</strong> | ||
<span class="block sm:inline">The report can't be generated.</span> | ||
</div> | ||
</div>'''), file=out) | ||
|
||
def print_generic_error(out): | ||
print(dedent(''' | ||
<div class="px-3 py-3"> | ||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert"> | ||
<strong class="font-bold">Unknown error!</strong> | ||
<span class="block sm:inline">The report can't be generated.</span> | ||
</div> | ||
</div>'''), file=out) | ||
|
||
def print_footer(out, generated_info): | ||
now = datetime.now() | ||
print(dedent(f''' | ||
Last generated at {now}. {generated_info or ''} | ||
</body> | ||
</html> | ||
'''), file=out) | ||
|
||
def print_table_header(out, tag): | ||
print(dedent(f''' | ||
<div class="px-3 py-3"> | ||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg"> | ||
<table class="table-fixed w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400"> | ||
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white dark:text-white dark:bg-gray-800"> | ||
{tag} | ||
</caption> | ||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"> | ||
<tr> | ||
<th scope="col" class="px-6 py-3"> | ||
Build | ||
</th> | ||
<th scope="col" class="px-6 py-3"> | ||
Cards | ||
</th> | ||
<th scope="col" class="px-6 py-3"> | ||
Pull Requests | ||
</th> | ||
<th scope="col" class="px-6 py-3"> | ||
Built by | ||
</th> | ||
<th scope="col" class="px-6 py-3"> | ||
Maintained by | ||
</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
'''), file=out) # nopep8 | ||
|
||
def print_table_footer(out): | ||
print(dedent(''' | ||
</tbody> | ||
</table> | ||
</div> | ||
</div> | ||
'''), file=out) | ||
|
||
def print_table_line(out, build, link, issues, built_by, prs: list[PullRequest], maintained_by): | ||
issues_content = '\n'.join([ | ||
f'''<li> | ||
<a class="font-medium text-blue-600 dark:text-blue-500 hover:underline" | ||
href="https://project.vates.tech/vates-global/browse/XCPNG-{i['sequence_id']}/">XCPNG-{i['sequence_id']} | ||
</a> | ||
</li>''' | ||
for i in issues | ||
]) | ||
prs_content = '\n'.join([ | ||
f'''<li> | ||
<a class="font-medium text-blue-600 dark:text-blue-500 hover:underline" | ||
href="{pr.html_url}">{pr.title} #{pr.number} | ||
</a> | ||
</li>''' | ||
for pr in prs | ||
]) | ||
print(f''' | ||
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 border-gray-200"> | ||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> | ||
<a class="font-medium text-blue-600 dark:text-blue-500 hover:underline" href="{link}">{build}</a> | ||
</th> | ||
<td class="px-6 py-4"> | ||
<ul> | ||
{issues_content} | ||
</ul> | ||
</td> | ||
<td class="px-6 py-4"> | ||
<ul> | ||
{prs_content} | ||
</ul> | ||
</td> | ||
<td class="px-6 py-4"> | ||
{built_by} | ||
</td> | ||
<td class="px-6 py-4"> | ||
{maintained_by if maintained_by is not None else ''} | ||
</td> | ||
</tr> | ||
''', file=out) # nopep8 | ||
|
||
def parse_source(source: str) -> tuple[str, str]: | ||
ydirson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
groups = re.match(r'git\+https://github\.com/([\w-]+/[\w-]+)(|\.git)#([0-9a-f]{40})', source) | ||
assert groups is not None, "can't match the source to the expected github url" | ||
return (groups[1], groups[3]) | ||
|
||
def filter_issues(issues, urls): | ||
res = [] | ||
for issue in issues: | ||
for url in urls: | ||
url = url.strip('/') | ||
if f'href="{url}"' in issue['description_html'] or f'href="{url}/"' in issue['description_html']: | ||
res.append(issue) | ||
break | ||
return res | ||
|
||
|
||
TAG_ORDER = ['incoming', 'ci', 'testing', 'candidates', 'updates', 'base', 'lab'] | ||
|
||
def tag_priority(tag): | ||
# drop the version in the tag — v8.3-incoming -> incoming | ||
tag = tag.split('-')[-1] | ||
return TAG_ORDER.index(tag) | ||
|
||
def find_previous_build_commit(session, build_tag, build): | ||
ydirson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Find the previous build in an higher priority koji tag and return its commit.""" | ||
tagged = session.listTagged(build_tag, package=build['package_name'], inherit=True) | ||
tagged = sorted(tagged, key=lambda t: tag_priority(t['tag_name'])) | ||
build_tag_priority = tag_priority(build_tag) | ||
tagged = [t for t in tagged if tag_priority(t['tag_name']) > build_tag_priority] | ||
if not tagged: | ||
return None | ||
previous_build = session.getBuild(tagged[0]['build_id']) | ||
if not previous_build.get('source'): | ||
return None | ||
return parse_source(previous_build['source'])[1] | ||
|
||
def find_commits(gh, repo, start_sha, end_sha) -> list[Commit]: | ||
""" | ||
List the commits in the range [start_sha,end_sha[. | ||
|
||
Note: these are the commits listed by Github starting from start_sha up to end_sha excluded. | ||
A commit older that the end_sha commit and added by a merge commit won't appear in this list. | ||
""" | ||
cache_key = f'commits-{start_sha}-{end_sha}' | ||
if cache_key in CACHE: | ||
return cast(list[Commit], CACHE[cache_key]) | ||
commits = [] | ||
if gh: | ||
for commit in gh.get_repo(repo).get_commits(start_sha): | ||
if commit.sha == end_sha: | ||
break | ||
commits.append(commit) | ||
CACHE[cache_key] = commits | ||
return commits | ||
|
||
def find_pull_requests(gh, repo, start_sha, end_sha): | ||
"""Find the pull requests for the commits in the [start_sha,end_sha[ range.""" | ||
prs = set() | ||
for commit in find_commits(gh, repo, start_sha, end_sha): | ||
cache_key = f'commit-prs-{commit.sha}' | ||
if cache_key in CACHE: | ||
prs.update(cast(list[PullRequest], CACHE[cache_key])) | ||
elif gh: | ||
commit_prs = list(commit.get_pulls()) | ||
CACHE[cache_key] = commit_prs | ||
prs.update(commit_prs) | ||
return sorted(prs, key=lambda p: p.number, reverse=True) | ||
|
||
parser = argparse.ArgumentParser(description='Generate a report of the packages in the pipe') | ||
parser.add_argument('output', nargs='?', help='Report output path', default='report.html') | ||
parser.add_argument('--generated-info', help="Add this message about the generation in the report") | ||
parser.add_argument( | ||
'--plane-token', help="The token used to access the plane api", default=os.environ.get('PLANE_TOKEN') | ||
) | ||
parser.add_argument( | ||
'--github-token', help="The token used to access the Github api", default=os.environ.get('GITHUB_TOKEN') | ||
) | ||
parser.add_argument('--cache', help="The cache path", default="/tmp/pkg_in_pipe.cache") | ||
args = parser.parse_args() | ||
|
||
CACHE = diskcache.Cache(args.cache) | ||
|
||
# load the issues from plane, so we can search for the plane card related to a build | ||
resp = requests.get( | ||
'https://project.vates.tech/api/v1/workspaces/vates-global/projects/43438eec-1335-4fc2-8804-5a4c32f4932d/issues/', | ||
headers={'x-api-key': args.plane_token}, | ||
) | ||
issues = resp.json().get('results', []) | ||
|
||
# connect to github | ||
if args.github_token: | ||
gh = github.Github(auth=github.Auth.Token(args.github_token)) | ||
try: | ||
gh.get_repo('xcp-ng/xcp') # check that the token is valid | ||
except BadCredentialsException: | ||
gh = None | ||
else: | ||
gh = None | ||
|
||
# load the packages maintainers | ||
with urlopen('https://github.com/xcp-ng/xcp/raw/refs/heads/master/scripts/rpm_owners/packages.json') as f: | ||
PACKAGES = json.load(f) | ||
|
||
ok = True | ||
with open(args.output, 'w') as out: | ||
print_header(out) | ||
if not issues: | ||
print_plane_warning(out) | ||
if not gh: | ||
print_github_warning(out) | ||
tags = [f'v{v}-{p}' for v in ['8.2', '8.3'] for p in ['incoming', 'ci', 'testing', 'candidates', 'lab']] | ||
temp_out = io.StringIO() | ||
try: | ||
# open koji session | ||
config = koji.read_config("koji") | ||
session = koji.ClientSession('https://kojihub.xcp-ng.org', config) | ||
session.ssl_login(config['cert'], None, config['serverca']) | ||
for tag in tags: | ||
print_table_header(temp_out, tag) | ||
for tagged in sorted(session.listTagged(tag), key=lambda build: int(build['build_id']), reverse=True): | ||
build = session.getBuild(tagged['build_id']) | ||
prs: list[PullRequest] = [] | ||
maintained_by = None | ||
previous_build_sha = find_previous_build_commit(session, tag, build) | ||
if build['source'] is not None: | ||
(repo, sha) = parse_source(build['source']) | ||
prs = find_pull_requests(gh, repo, sha, previous_build_sha) | ||
maintained_by = PACKAGES.get(repo.split('/')[-1], {}).get('maintainer') | ||
build_url = f'https://koji.xcp-ng.org/buildinfo?buildID={tagged["build_id"]}' | ||
build_issues = filter_issues(issues, [build_url] + [pr.html_url for pr in prs]) | ||
print_table_line( | ||
temp_out, tagged['nvr'], build_url, build_issues, tagged['owner_name'], prs, maintained_by | ||
) | ||
print_table_footer(temp_out) | ||
out.write(temp_out.getvalue()) | ||
except koji.GenericError: | ||
print_koji_error(out) | ||
raise | ||
except Exception: | ||
print_generic_error(out) | ||
raise | ||
finally: | ||
print_footer(out, args.generated_info) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.