diff --git a/.samples/ci-svg.py b/.samples/ci-svg.py index bd69ccc..1efb5e1 100644 --- a/.samples/ci-svg.py +++ b/.samples/ci-svg.py @@ -2,6 +2,10 @@ """Draws the CI of a sample branch by day, as the samples page of the template's site draws it: ci-svg.py + ci-svg.py --legend + +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 @@ -9,7 +13,7 @@ - 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. @@ -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: @@ -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'', - f'CI of {branch} by day', + f'CI of {branch} by day, UTC', f'{facts}'] - 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'{label}') shown = set() for cell in grid: @@ -168,23 +171,6 @@ def svg(branch, runs, today): body.append(f'{title}') - # 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'{label}') - lx += 44 - for k in range(5): - share = k / 4 - body.append(f'') - 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'over {FADE_DAYS} days without a run') - 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] @@ -192,11 +178,60 @@ def svg(branch, runs, today): css.append("@media (prefers-color-scheme: dark){." + cls + "{fill:" + dfill + "}}") return (f'\n' + f'role="img" aria-label="CI of {branch} by day, UTC: {facts}">\n' + f'\n' + "\n".join(body) + "\n\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'', + f'How to read the days'] + + def cell(cls, k, y, extra=""): + x = LEFT + label_w + k * (CELL + GAP) + body.append(f'') + + y = 34 + for label, (start, end), words in rows: + body.append(f'{label}') + 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'{words}') + y += 20 + cell("e", 0, y) + body.append(f'no run yet') + y += 20 + for k in range(swatches): + cell("e", k, y, f' opacity="{max(0.15, 1 - k / (swatches - 1)):.2f}"') + body.append(f'the weeks to come') + + return (f'\n' f'\n' + "\n".join(body) + "\n\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:] diff --git a/.samples/generate.sh b/.samples/generate.sh index 226bc1e..c622c9b 100644 --- a/.samples/generate.sh +++ b/.samples/generate.sh @@ -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 @@ -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 @@ -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 @@ -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." diff --git a/.samples/legend.svg b/.samples/legend.svg new file mode 100644 index 0000000..45c5f83 --- /dev/null +++ b/.samples/legend.svg @@ -0,0 +1,50 @@ + + + +How to read the days +passed + + + + + +a pass, fading to ice over 30 days without a run +failed + + + + + +a failure, rotting to dark red over 30 days without a run + +no run yet + + + + + +the weeks to come + diff --git a/README.md b/README.md index 208f251..9d4eadb 100644 --- a/README.md +++ b/README.md @@ -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.