|
| 1 | +#!/usr/bin/env python |
| 2 | +# |
| 3 | +# This is free and unencumbered software released into the public domain. |
| 4 | +# See the UNLICENSE file for details. |
| 5 | +# |
| 6 | +# ------------------------------------------------------------------------ |
| 7 | +# html-report.py |
| 8 | +# ------------------------------------------------------------------------ |
| 9 | +# Generates an HTML report of the release status of all |
| 10 | +# components in the SciJava BOM (org.scijava:pom-scijava). |
| 11 | + |
| 12 | +import logging, re, subprocess |
| 13 | + |
| 14 | +# -- Constants -- |
| 15 | + |
| 16 | +checkMark = "✔" |
| 17 | +xMark = "✕" |
| 18 | +repoBase = "https://maven.scijava.org" |
| 19 | + |
| 20 | +# -- Data -- |
| 21 | + |
| 22 | +def file2map(filepath, sep=' '): |
| 23 | + with open(filepath) as f: |
| 24 | + pairs = [line.strip().split(sep, 1) for line in f.readlines()] |
| 25 | + return {pair[0]: pair[1] for pair in pairs} |
| 26 | + |
| 27 | +badge_overrides = file2map('ci-badges.txt') |
| 28 | +timestamps = file2map('timestamps.txt') |
| 29 | +project_urls = file2map('projects.txt') |
| 30 | + |
| 31 | +group_orgs = { |
| 32 | + 'graphics.scenery': 'scenerygraphics', |
| 33 | + 'io.scif': 'scifio', |
| 34 | + 'net.imagej': 'imagej', |
| 35 | + 'net.imglib2': 'imglib', |
| 36 | + 'org.openmicroscopy': 'ome', |
| 37 | + 'org.scijava': 'scijava', |
| 38 | + 'sc.fiji': 'fiji', |
| 39 | + 'sc.iview': 'scenerygraphics', |
| 40 | +} |
| 41 | + |
| 42 | +# -- Functions -- |
| 43 | + |
| 44 | +def newest_releases(): |
| 45 | + """ |
| 46 | + Dumps the component list for the given BOM (pom-scijava by default). |
| 47 | + The format of each component is groupId:artifactId,bomVersion,newestVersion |
| 48 | + """ |
| 49 | + return [token.decode() for token in subprocess.check_output(['./newest-releases.sh']).split()] |
| 50 | + |
| 51 | +def project_url(ga): |
| 52 | + """ |
| 53 | + Gets the URL of a project from its G:A. |
| 54 | + """ |
| 55 | + if ga in project_urls: |
| 56 | + return project_urls[ga] |
| 57 | + |
| 58 | + g, a = ga.split(':', 1) |
| 59 | + |
| 60 | + if g == "sc.fiji": |
| 61 | + if a.startswith('bigdataviewer'): |
| 62 | + return f'https://github.com/bigdataviewer/{a}' |
| 63 | + if a.endswith('_'): |
| 64 | + a = a[:-1] |
| 65 | + |
| 66 | + if g in group_orgs: |
| 67 | + return f'https://github.com/{group_orgs[g]}/{a}' |
| 68 | + |
| 69 | + return '' |
| 70 | + |
| 71 | +def version_timestamps(g, a, v): |
| 72 | + """ |
| 73 | + Gets timestamps for the last time a component was released. |
| 74 | + Returns a (releaseTimestamp, lastDeployed) pair. |
| 75 | + """ |
| 76 | + gav, releaseTimestamp, lastDeployed = subprocess.check_output(['./version-timestamps.sh', f'{g}:{a}:{v}']).decode().strip('\n\r').split(' ') |
| 77 | + releaseTimestamp = int(releaseTimestamp) if releaseTimestamp else 0 |
| 78 | + lastDeployed = int(lastDeployed) if lastDeployed else 0 |
| 79 | + return releaseTimestamp, lastDeployed |
| 80 | + |
| 81 | +def badge(url): |
| 82 | + slug = None |
| 83 | + if url.startswith('https://github.com/'): |
| 84 | + slug = url[len('https://github.com/'):] |
| 85 | + if slug in badge_overrides: |
| 86 | + return badge_overrides[slug] |
| 87 | + if slug: |
| 88 | + return f"<td class=\"badge\"><a href=\"https://github.com/{slug}/actions\"><img src=\"https://github.com/{slug}/actions/workflows/build-main.yml/badge.svg\"></a></td>" |
| 89 | + return "<td>-</td>" |
| 90 | + |
| 91 | +def timestamp_override(g, a): |
| 92 | + """ |
| 93 | + Gets the timestamp when a project was last vetted. |
| 94 | +
|
| 95 | + Sometimes, we know a new snapshot has no release-worthy changes since |
| 96 | + the last release. In this scenario, we can record the last time we |
| 97 | + checked a given component in the timestamps.txt file. |
| 98 | +
|
| 99 | + This also gives us a hedge against problems with the Maven metadata, |
| 100 | + such as that particular G:A:V not be present in the remote repository |
| 101 | + for any reason (in which case, this function returns 0). |
| 102 | + """ |
| 103 | + return int(timestamps.get(f'{g}:{a}', 0)) |
| 104 | + |
| 105 | +def release_link(g, a, v): |
| 106 | + return f"<a href=\"{repoBase}/#nexus-search;gav~{g}~{a}~{v}~~\">{v}</a>" |
| 107 | + |
| 108 | +# -- Main -- |
| 109 | + |
| 110 | +# Emit the HTML header matter. |
| 111 | +print('<html>') |
| 112 | +print('<head>') |
| 113 | +print('<title>SciJava software status</title>') |
| 114 | +print('<link type="text/css" rel="stylesheet" href="status.css">') |
| 115 | +print('<link rel="icon" type="image/png" href="favicon.png">') |
| 116 | +print('<script type="text/javascript" src="sorttable.js"></script>') |
| 117 | +print('<script type="text/javascript" src="sortable-badges.js"></script>') |
| 118 | +print('</head>') |
| 119 | +print('<body onload="makeBadgesSortable()">') |
| 120 | +print('<!-- Generated via https://codepo8.github.io/css-fork-on-github-ribbon/ -->') |
| 121 | +print('<span id="forkongithub"><a href="https://github.com/scijava/status.scijava.org">Fix me on GitHub</a></span>') |
| 122 | +print('<table class="sortable">') |
| 123 | +print('<tr>') |
| 124 | +print('<th>Artifact</th>') |
| 125 | +print('<th>Release</th>') |
| 126 | +print('<th>OK</th>') |
| 127 | +print('<th>Last vetted</th>') |
| 128 | +print('<th>Last updated</th>') |
| 129 | +print('<th>OK</th>') |
| 130 | +print('<th>Action</th>') |
| 131 | +print('<th>Build</th>') |
| 132 | +print('</tr>') |
| 133 | + |
| 134 | +# List components of the BOM, and loop over them. |
| 135 | +logging.info("Generating list of components") |
| 136 | +for line in newest_releases(): |
| 137 | + ga, bomVersion, newestRelease = line.split(',') |
| 138 | + g, a = ga.split(':') |
| 139 | + |
| 140 | + logging.info(f"Processing {ga}") |
| 141 | + |
| 142 | + # Get project URL |
| 143 | + url = project_url(ga) |
| 144 | + |
| 145 | + # Compute badges |
| 146 | + ciBadge = badge(url) |
| 147 | + |
| 148 | + # Check BOM version vs. newest release |
| 149 | + if bomVersion == newestRelease: |
| 150 | + bomStatus = "bom-ok" |
| 151 | + bomOK = checkMark |
| 152 | + else: |
| 153 | + bomStatus = "bom-behind" |
| 154 | + bomOK = xMark |
| 155 | + |
| 156 | + # Discern timestamps for this component. |
| 157 | + # |
| 158 | + # Each component is "vetted" either by: |
| 159 | + # A) being released; or |
| 160 | + # B) adding an override to timestamps.txt. |
| 161 | + # Our goal here is to detect whether the component has changed since |
| 162 | + # the most recent release (not the release listed in the BOM). |
| 163 | + releaseTimestamp, lastUpdated = version_timestamps(g, a, newestRelease) |
| 164 | + timestampOverride = timestamp_override(g, a) |
| 165 | + lastVetted = max(releaseTimestamp, timestampOverride) |
| 166 | + |
| 167 | + # Compute time difference; >24 hours means a new release is needed. |
| 168 | + if lastUpdated - lastVetted > 1000000: |
| 169 | + # A SNAPSHOT was deployed more recently than the newest release. |
| 170 | + releaseStatus = "release-needed" |
| 171 | + releaseOK = xMark |
| 172 | + else: |
| 173 | + # No SNAPSHOT has happened more than 24 hours after newest release. |
| 174 | + releaseStatus = "release-ok" |
| 175 | + releaseOK = checkMark |
| 176 | + |
| 177 | + if lastUpdated < timestampOverride: |
| 178 | + # NB: Manually vetted more recently than last update; no bump needed. |
| 179 | + bomStatus = "bom-ok" |
| 180 | + bomOK = checkMark |
| 181 | + |
| 182 | + # Compute action items. |
| 183 | + if url and releaseOK == xMark: |
| 184 | + action = "Cut" |
| 185 | + actionKey = 1 |
| 186 | + elif bomOK == xMark: |
| 187 | + action = "Bump" |
| 188 | + actionKey = 2 |
| 189 | + else: |
| 190 | + action = "None" |
| 191 | + actionKey = 3 |
| 192 | + |
| 193 | + # Emit the HTML table row. |
| 194 | + gc = re.sub('[^0-9A-Za-z]', '-', g) |
| 195 | + ac = re.sub('[^0-9A-Za-z]', '-', a) |
| 196 | + print(f"<tr class=\"g-{gc} a-{ac} {bomStatus} {releaseStatus}\">") |
| 197 | + if url: |
| 198 | + print(f"<td><a href=\"{url}\">{g} : {a}</td>") |
| 199 | + else: |
| 200 | + print(f"<td>{g} : {a}</td>") |
| 201 | + |
| 202 | + if bomVersion == newestRelease: |
| 203 | + print(f"<td>{release_link(g, a, newestRelease)}</td>") |
| 204 | + else: |
| 205 | + print(f"<td>{release_link(g, a, bomVersion)} → {release_link(g, a, newestRelease)}</td>") |
| 206 | + |
| 207 | + print(f"<td>{bomOK}</td>") |
| 208 | + if lastVetted == 0: |
| 209 | + # Unknown status! |
| 210 | + print("<td class=\"unknown\">???</td>") |
| 211 | + elif lastVetted == timestampOverride: |
| 212 | + # Last vetted manually via timestamps.txt. |
| 213 | + print(f"<td class=\"overridden\">{lastVetted}</td>") |
| 214 | + elif timestampOverride == 0: |
| 215 | + # Last vetted automatically via release artifact; no timestamp override. |
| 216 | + print(f"<td>{lastVetted}</td>") |
| 217 | + else: |
| 218 | + # Last vetted automatically via release artifact; old timestamp override. |
| 219 | + print(f"<td class=\"wasOverridden\">{lastVetted}</td>") |
| 220 | + print(f"<td>{lastUpdated}</td>") |
| 221 | + print(f"<td>{releaseOK}</td>") |
| 222 | + print(f"<td sorttable_customkey=\"{actionKey}\">{action}</td>") |
| 223 | + print(ciBadge) |
| 224 | + print('</tr>') |
| 225 | + |
| 226 | +# Emit the HTML footer matter. |
| 227 | +print('</table>') |
| 228 | +with open('footer.html') as f: |
| 229 | + print(f.read().strip()) |
| 230 | +print('</body>') |
| 231 | +print('</html>') |
0 commit comments