Skip to content

Commit 1d6fe54

Browse files
committed
pkg_in_pipe: write a machine readable version of the report
The report is written as a json file that other scripts can consume, e.g. to generate the release post or to move the plane cards of the builds to another state. Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
1 parent 26db50c commit 1d6fe54

4 files changed

Lines changed: 222 additions & 8 deletions

File tree

scripts/pkg_in_pipe/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
report.html
2+
report.json
23
.cache

scripts/pkg_in_pipe/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,19 @@ A plane token with enough rights to list the cards in the XCPNG project must be
1515
environment variable or the `--plane-token` command line option.
1616

1717
An extra `--generated-info` command line option may be used to add some info about the report generation process.
18-
18+
19+
A machine readable version of the report can also be generated with the `--json-output` option:
20+
21+
```sh
22+
pkg_in_pipe --json-output report.json report.html
23+
```
24+
25+
The json report can be validated against its schema with:
26+
27+
```sh
28+
python -m jsonschema -i report.json pkg_in_pipe.schema.json
29+
```
30+
1931
# Run in docker
2032

2133
Before running in docker, the docker image must be built with:

scripts/pkg_in_pipe/pkg_in_pipe.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ def tag_priority(tag):
208208
else:
209209
return -1
210210

211-
def find_previous_build_commit(build_tag, build):
212-
"""Find the previous build in an higher priority koji tag and return its commit."""
211+
def find_previous_build(build_tag, build):
212+
"""Find the previous build of the package in an higher priority koji tag and return it."""
213213
tagged = KOJI.listTagged(build_tag, package=build['package_name'], inherit=True)
214214
tagged = sorted(tagged, key=lambda t: (tag_priority(t['tag_name']), -t['build_id']))
215215
build_tag_priority = tag_priority(build_tag)
@@ -218,10 +218,27 @@ def find_previous_build_commit(build_tag, build):
218218
]
219219
if not tagged:
220220
return None
221-
previous_build = get_koji_build(tagged[0]['build_id'])
222-
if not previous_build.get('source'):
223-
return None
224-
return parse_source(previous_build['source'])[1]
221+
return get_koji_build(tagged[0]['build_id'])
222+
223+
def released_tags(build_tag):
224+
"""Return the tags holding the released builds of the version, e.g. v8.3-updates and v8.3-base."""
225+
version = build_tag.split('-')[0][1:] # v8.3-candidates -> 8.3
226+
return [] if not version else [f'v{version}-updates', f'v{version}-base']
227+
228+
def find_released_build(build_tag, package_name):
229+
"""Find the newest released build of the package in the updates and base tags, updates first.
230+
231+
Some versions may not have an updates or base tag yet, which koji reports as an error, so a
232+
missing tag is treated as an empty one.
233+
"""
234+
for tag in released_tags(build_tag):
235+
try:
236+
tagged = KOJI.listTagged(tag, package=package_name)
237+
except koji.GenericError:
238+
continue
239+
if tagged:
240+
return get_koji_build(max(tagged, key=lambda t: t['build_id'])['build_id'])
241+
return None
225242

226243
def find_commits(gh, repo, start_sha, end_sha) -> list[Commit]:
227244
"""
@@ -377,6 +394,7 @@ def get_plane_issues_with_milestones(plane_token):
377394
'--package', '-p', dest='packages', help="The packages to include in the report", action='append', default=[]
378395
)
379396
parser.add_argument('--re-cache', help="Refresh the cache", action='store_true')
397+
parser.add_argument('--json-output', help="Also write a machine readable report in json format to this path")
380398
args = parser.parse_args()
381399

382400
CACHE = diskcache.Cache(args.cache)
@@ -411,6 +429,14 @@ def get_plane_issues_with_milestones(plane_token):
411429
with urlopen('https://github.com/xcp-ng/xcp/raw/refs/heads/master/scripts/rpm_owners/packages.json') as f:
412430
PACKAGES = json.load(f)
413431

432+
report_data = {
433+
'generated_at': started_at.isoformat(),
434+
'generated_info': args.generated_info,
435+
'warnings': {'plane': not issues, 'github': not GITHUB},
436+
'error': None,
437+
'tags': [],
438+
}
439+
414440
with io.StringIO() as out:
415441
print_header(out)
416442
if not issues:
@@ -424,6 +450,8 @@ def get_plane_issues_with_milestones(plane_token):
424450
KOJI = koji.ClientSession('https://kojihub.xcp-ng.org', config)
425451
KOJI.ssl_login(config['cert'], None, config['serverca'])
426452
for tag in tags:
453+
tag_data = {'tag': tag, 'builds': []}
454+
report_data['tags'].append(tag_data)
427455
tag_history = dict(
428456
(tl['build_id'], tl['create_ts'])
429457
for tl in KOJI.queryHistory(tag=tag, active=True)['tag_listing']
@@ -436,7 +464,11 @@ def get_plane_issues_with_milestones(plane_token):
436464
build = get_koji_build(tagged['build_id'])
437465
prs: list[PullRequest] = []
438466
maintained_by = None
439-
previous_build_sha = find_previous_build_commit(tag, build)
467+
previous_build = find_previous_build(tag, build)
468+
previous_build_sha = None
469+
if previous_build is not None and previous_build.get('source') is not None:
470+
previous_build_sha = parse_source(previous_build['source'])[1]
471+
released_build = find_released_build(tag, tagged['package_name'])
440472
if build['source'] is not None:
441473
(repo, sha) = parse_source(build['source'])
442474
prs = find_pull_requests(repo, sha, previous_build_sha)
@@ -446,12 +478,34 @@ def get_plane_issues_with_milestones(plane_token):
446478
print_table_line(
447479
temp_out, tagged['nvr'], build_url, build_issues, tagged['owner_name'], prs, maintained_by
448480
)
481+
tag_data['builds'].append({
482+
'nvr': tagged['nvr'],
483+
'previous_nvr': released_build['nvr'] if released_build is not None else None,
484+
'package': tagged['package_name'],
485+
'url': build_url,
486+
'built_by': tagged['owner_name'],
487+
'maintained_by': maintained_by,
488+
'issues': [{
489+
'sequence_id': i['sequence_id'],
490+
'url': f'https://project.vates.tech/vates-global/browse/XCPNG-{i["sequence_id"]}/',
491+
'milestones': sorted(set(i['milestones'])),
492+
} for i in build_issues],
493+
'pull_requests': [{
494+
'number': pr.number,
495+
'title': pr.title,
496+
'url': pr.html_url,
497+
'linked': issues_have_link(build_issues, pr.html_url),
498+
} for pr in prs],
499+
'build_linked': issues_have_link(build_issues, build_url),
500+
})
449501
print_table_footer(temp_out)
450502
out.write(temp_out.getvalue())
451503
except koji.GenericError:
504+
report_data['error'] = 'koji'
452505
print_koji_error(out)
453506
raise
454507
except Exception:
508+
report_data['error'] = 'unknown'
455509
print_generic_error(out)
456510
raise
457511
finally:
@@ -460,3 +514,7 @@ def get_plane_issues_with_milestones(plane_token):
460514
# write the actual output at once, in order to avoid a blank page during the processing
461515
with open(args.output, 'w') as f:
462516
f.write(out.getvalue())
517+
518+
if args.json_output:
519+
with open(args.json_output, 'w') as f:
520+
json.dump(report_data, f, indent=2)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://xcp-ng.org/schemas/pkg_in_pipe.json",
4+
"title": "XCP-ng package in the pipe report",
5+
"type": "object",
6+
"required": ["generated_at", "generated_info", "warnings", "error", "tags"],
7+
"properties": {
8+
"generated_at": {
9+
"description": "Timestamp of the start of the report generation, in ISO 8601 format",
10+
"type": "string",
11+
"format": "date-time"
12+
},
13+
"generated_info": {
14+
"description": "Message about the generation included in the report",
15+
"type": ["string", "null"]
16+
},
17+
"warnings": {
18+
"description": "Whether some external sources could not be reached",
19+
"type": "object",
20+
"required": ["plane", "github"],
21+
"properties": {
22+
"plane": {
23+
"description": "True if the issues could not be retrieved from plane",
24+
"type": "boolean"
25+
},
26+
"github": {
27+
"description": "True if github is not available and the pull requests may come from the cache",
28+
"type": "boolean"
29+
}
30+
}
31+
},
32+
"error": {
33+
"description": "Set when the report could not be fully generated",
34+
"enum": [null, "koji", "unknown"]
35+
},
36+
"tags": {
37+
"type": "array",
38+
"items": {
39+
"type": "object",
40+
"required": ["tag", "builds"],
41+
"properties": {
42+
"tag": {
43+
"description": "The koji tag this section is about, e.g. v8.3-incoming",
44+
"type": "string"
45+
},
46+
"builds": {
47+
"type": "array",
48+
"items": {
49+
"type": "object",
50+
"required": [
51+
"nvr", "package", "url", "built_by", "maintained_by", "issues", "pull_requests",
52+
"build_linked"
53+
],
54+
"properties": {
55+
"nvr": {
56+
"description": "The package name, version and release of the build",
57+
"type": "string"
58+
},
59+
"previous_nvr": {
60+
"description": "The NVR of the previously released build of the package (newest build in the updates or base tag), when one exists",
61+
"type": ["string", "null"]
62+
},
63+
"package": {
64+
"description": "The name of the package this build is about",
65+
"type": "string"
66+
},
67+
"url": {
68+
"description": "URL to the build on koji",
69+
"type": "string",
70+
"format": "uri"
71+
},
72+
"built_by": {
73+
"description": "The koji user who built the package",
74+
"type": "string"
75+
},
76+
"maintained_by": {
77+
"description": "The maintainer of the package, when known",
78+
"type": ["string", "null"]
79+
},
80+
"issues": {
81+
"description": "The plane cards related to this build or its pull requests",
82+
"type": "array",
83+
"items": {
84+
"type": "object",
85+
"required": ["sequence_id", "url", "milestones"],
86+
"properties": {
87+
"sequence_id": {
88+
"description": "The XCPNG sequence id of the card",
89+
"type": "string"
90+
},
91+
"url": {
92+
"description": "URL to the card on plane",
93+
"type": "string",
94+
"format": "uri"
95+
},
96+
"milestones": {
97+
"type": "array",
98+
"items": {"type": "string"},
99+
"uniqueItems": true
100+
}
101+
}
102+
}
103+
},
104+
"pull_requests": {
105+
"description": "The pull requests related to this build",
106+
"type": "array",
107+
"items": {
108+
"type": "object",
109+
"required": ["number", "title", "url", "linked"],
110+
"properties": {
111+
"number": {
112+
"description": "The pull request number on github",
113+
"type": "integer",
114+
"minimum": 1
115+
},
116+
"title": {
117+
"description": "The title of the pull request",
118+
"type": "string"
119+
},
120+
"url": {
121+
"description": "URL to the pull request on github",
122+
"type": "string",
123+
"format": "uri"
124+
},
125+
"linked": {
126+
"description": "True if the pull request is listed in one of the related cards",
127+
"type": "boolean"
128+
}
129+
}
130+
}
131+
},
132+
"build_linked": {
133+
"description": "True if the build is listed in one of the related cards",
134+
"type": "boolean"
135+
}
136+
}
137+
}
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)