Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 58 additions & 23 deletions .samples/ci-svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@
"""Draws the CI of a sample branch by day, as the samples page of the template's site draws it:

ci-svg.py <reports/branch folder> <branch> <out.svg>
ci-svg.py --legend <out.svg>

The legend is a picture of its own, drawn once for every branch: a README shows it under the days and says
the same in words.

The README of a branch shows the picture: GitHub shows images there without running a script, so the grid
of the site cannot be the page itself. The same data (every run of every version in the folder), the same
rules and the same colours as site/samples.js and samples.css of DotNetSolutionKit:

- a day keeps the result of the branch's last run; a pass fades to ice and a failure rots to dark red over
30 days without a run, the colours mixed in OKLab as the site's color-mix does;
- 52 weeks back, the weeks to come fading out, Monday at the top;
- 52 weeks back, the weeks to come fading out, Monday at the top and Sunday at the bottom;
- light and dark follow the reader's colour scheme.

Days are UTC: the picture is drawn once, for every reader.
Expand Down Expand Up @@ -122,8 +126,7 @@ def svg(branch, runs, today):
grid = cells(runs, today)
weeks = grid[-1]["week"] + 1
width = LEFT + weeks * (CELL + GAP) + 12
legend_y = TOP + 7 * (CELL + GAP) + 18
height = legend_y + 26
height = TOP + 7 * (CELL + GAP) + 10

last = runs[-1] if runs else None
if last:
Expand All @@ -148,9 +151,9 @@ def svg(branch, runs, today):
dark = {c: k for k, c in enumerate(sorted({fill(c, "dark")[0] for c in grid}))}
css_dark_alias = []
body = [f'<rect class="g" width="{width}" height="{height}" rx="6"/>',
f'<text class="t x" x="{LEFT}" y="20">CI of {branch} by day</text>',
f'<text class="t x" x="{LEFT}" y="20">CI of {branch} by day, UTC</text>',
f'<text class="m" x="{LEFT}" y="35">{facts}</text>']
for label, row in (("Mon", 0), ("Wed", 2), ("Fri", 4)):
for label, row in (("Mon", 0), ("Wed", 2), ("Fri", 4), ("Sun", 6)):
body.append(f'<text class="m" x="4" y="{TOP + row * (CELL + GAP) + CELL - 2}">{label}</text>')
shown = set()
for cell in grid:
Expand All @@ -168,35 +171,67 @@ def svg(branch, runs, today):
body.append(f'<rect class="{cls}" x="{x}" y="{y}" width="{CELL}" height="{CELL}" rx="2"'
+ (f' opacity="{op:.2f}"' if op < 1 else "") + f'><title>{title}</title></rect>')

# the legend: what a pass and a failure become without a run
lx = LEFT
for label, colours in (("passed", ("ok", "sleep")), ("failed", ("bad", "rot"))):
body.append(f'<text class="m" x="{lx}" y="{legend_y + 9}">{label}</text>')
lx += 44
for k in range(5):
share = k / 4
body.append(f'<rect class="L{label}{k}" x="{lx}" y="{legend_y}" width="{CELL}" height="{CELL}" rx="2"/>')
for theme in ("light", "dark"):
t = THEMES[theme]
colour = mix(t[colours[1]], share, t[colours[0]])
rule = f".L{label}{k}{{fill:{colour}}}"
css.append(rule if theme == "light" else "@media (prefers-color-scheme: dark){" + rule + "}")
lx += CELL + GAP
body.append(f'<text class="m" x="{lx + 4}" y="{legend_y + 9}">over {FADE_DAYS} days without a run</text>')
lx += 190

for cls, lk, dk in css_dark_alias:
lfill = sorted({fill(c, "light")[0] for c in grid})[lk]
dfill = sorted({fill(c, "dark")[0] for c in grid})[dk]
css.append(f".{cls}{{fill:{lfill}}}")
css.append("@media (prefers-color-scheme: dark){." + cls + "{fill:" + dfill + "}}")

return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" '
f'role="img" aria-label="CI of {branch} by day: {facts}">\n'
f'role="img" aria-label="CI of {branch} by day, UTC: {facts}">\n'
f'<style>{chr(10).join(css)}</style>\n' + "\n".join(body) + "\n</svg>\n")


def legend():
"""What the colours of a day mean: one picture for every branch, under its days in the README."""
rows = (("passed", ("ok", "sleep"), f"a pass, fading to ice over {FADE_DAYS} days without a run"),
("failed", ("bad", "rot"), f"a failure, rotting to dark red over {FADE_DAYS} days without a run"))
swatches, label_w = 5, 48
words_x = LEFT + label_w + swatches * (CELL + GAP) + 8
width = words_x + 330
height = 34 + 4 * 20 + 4

css = ["text{font:11px system-ui,-apple-system,'Segoe UI',sans-serif}", ".t{font-size:13px;font-weight:600}"]

def themed(rule_of):
for theme in ("light", "dark"):
rule = rule_of(THEMES[theme])
css.append(rule if theme == "light" else "@media (prefers-color-scheme: dark){" + rule + "}")

themed(lambda t: f".g{{fill:{t['ground']}}}.x{{fill:{t['text']}}}.m{{fill:{t['muted']}}}.e{{fill:{t['empty']}}}")
body = [f'<rect class="g" width="{width}" height="{height}" rx="6"/>',
f'<text class="t x" x="{LEFT}" y="20">How to read the days</text>']

def cell(cls, k, y, extra=""):
x = LEFT + label_w + k * (CELL + GAP)
body.append(f'<rect class="{cls}" x="{x}" y="{y}" width="{CELL}" height="{CELL}" rx="2"{extra}/>')

y = 34
for label, (start, end), words in rows:
body.append(f'<text class="m" x="{LEFT}" y="{y + 9}">{label}</text>')
for k in range(swatches):
cls = f"L{label}{k}"
themed(lambda t, cls=cls, k=k: f".{cls}{{fill:{mix(t[end], k / (swatches - 1), t[start])}}}")
cell(cls, k, y)
body.append(f'<text class="m" x="{words_x}" y="{y + 9}">{words}</text>')
y += 20
cell("e", 0, y)
body.append(f'<text class="m" x="{words_x}" y="{y + 9}">no run yet</text>')
y += 20
for k in range(swatches):
cell("e", k, y, f' opacity="{max(0.15, 1 - k / (swatches - 1)):.2f}"')
body.append(f'<text class="m" x="{words_x}" y="{y + 9}">the weeks to come</text>')

return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" '
f'role="img" aria-label="How to read the days: a day keeps the result of the last run, a pass fades to ice '
f'and a failure rots to dark red over {FADE_DAYS} days without a run">\n'
f'<style>{chr(10).join(css)}</style>\n' + "\n".join(body) + "\n</svg>\n")


def main():
if len(sys.argv) == 3 and sys.argv[1] == "--legend":
open(sys.argv[2], "w", encoding="utf-8", newline="\n").write(legend())
return
if len(sys.argv) != 4:
raise SystemExit(__doc__)
folder, branch, out = sys.argv[1:]
Expand Down
21 changes: 20 additions & 1 deletion .samples/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ done < "$work/commands"
bash src/services/manual-add-projects.sh < /dev/null

about=$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .about' "$here/variants.json")
home=$(jq -r '.[] | select(.default) | .branch' "$here/variants.json")
raw="https://raw.githubusercontent.com/${GITHUB_REPOSITORY:-sawking-tech/DotNetSolutionKit.Samples}"
fade=$(sed -n 's/^FADE_DAYS = //p' "$here/ci-svg.py")

# What the colours of the days mean: one legend for every branch, drawn by ci-svg.py on the default
# branch, and the same in words.
legend() {
echo "![How to read the days]($raw/$home/.samples/legend.svg)"
echo
echo "Each square is a day in UTC, Monday at the top and Sunday at the bottom. A day shows the result of the"
echo "last CI run up to it: green for a pass, red for a failure. With no new run the colour changes over $fade"
echo "days, a pass fading to ice blue and a failure turning dark red, so an old result is told apart from a"
echo "fresh one. A pale square is a day before the first run; the fading squares on the right are the weeks"
echo "to come."
}
{
echo "# Sample: $branch"
echo
Expand All @@ -49,6 +64,8 @@ about=$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .about' "$here/va
# from its first run on.
echo "[![CI of $branch by day](reports/$branch/ci.svg)](https://dnsk.sawking.tech/samples.html#$branch)"
echo
# The default branch shows the legend once, under the days of every branch further down.
[ "$branch" = "$home" ] || { legend; echo; }
if [ "$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .ref // empty' "$here/variants.json")" ]; then
echo "Generated from DotNetSolutionKit $tag at [${commit:0:7}](https://github.com/$template_repo/commit/$commit), not a release, with:"
else
Expand All @@ -73,6 +90,7 @@ done
if [ "$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .default // false' "$here/variants.json")" = "true" ]; then
mkdir -p .samples .github/workflows
cp "$here/generate.sh" "$here/report.sh" "$here/ci-svg.py" "$here/variants.json" .samples/
python3 "$here/ci-svg.py" --legend .samples/legend.svg
cp "$here/../.github/workflows/regenerate.yml" "$here/../.github/workflows/report.yml" .github/workflows/
{
echo
Expand All @@ -85,8 +103,9 @@ if [ "$(jq -r --arg b "$branch" '.[] | select(.branch == $b) | .default // false
echo
echo "The CI of each branch by day, as [the samples page](https://dnsk.sawking.tech/samples.html) shows it:"
echo
raw="https://raw.githubusercontent.com/${GITHUB_REPOSITORY:-sawking-tech/DotNetSolutionKit.Samples}"
jq -r --arg raw "$raw" '.[] | "[![CI of \(.branch) by day](\($raw)/\(.branch)/reports/\(.branch)/ci.svg)](https://dnsk.sawking.tech/samples.html#\(.branch))\n"' "$here/variants.json"
legend
echo
echo "Once a day [regenerate](.github/workflows/regenerate.yml) checks the template's master: what reaches"
echo "master is a release, and every release branch is generated again from a new one, pushed, and its CI runs."
echo "nightly follows dev instead, where the next release is built: it is generated again when dev has moved."
Expand Down
50 changes: 50 additions & 0 deletions .samples/legend.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ The CI of each branch by day, as [the samples page](https://dnsk.sawking.tech/sa

[![CI of nightly by day](https://raw.githubusercontent.com/sawking-tech/DotNetSolutionKit.Samples/nightly/reports/nightly/ci.svg)](https://dnsk.sawking.tech/samples.html#nightly)

![How to read the days](https://raw.githubusercontent.com/sawking-tech/DotNetSolutionKit.Samples/full/.samples/legend.svg)

Each square is a day in UTC, Monday at the top and Sunday at the bottom. A day shows the result of the
last CI run up to it: green for a pass, red for a failure. With no new run the colour changes over 30
days, a pass fading to ice blue and a failure turning dark red, so an old result is told apart from a
fresh one. A pale square is a day before the first run; the fading squares on the right are the weeks
to come.

Once a day [regenerate](.github/workflows/regenerate.yml) checks the template's master: what reaches
master is a release, and every release branch is generated again from a new one, pushed, and its CI runs.
nightly follows dev instead, where the next release is built: it is generated again when dev has moved.
Expand Down