From 825948bb801d012f4aa53a2736796c971ecafe70 Mon Sep 17 00:00:00 2001 From: "v.savkin" Date: Mon, 5 Oct 2026 12:43:04 +0600 Subject: [PATCH] feat: the CI pictures say UTC and Sunday, and one legend explains every branch's days - ci-svg.py labels Sunday beside Monday, Wednesday and Friday, and its title says the days are UTC - the legend left the picture of each branch: ci-svg.py --legend draws it once, .samples/legend.svg on the default branch: a pass fading to ice, a failure rotting to dark red over 30 days without a run, a day with no run yet, the weeks to come - generate.sh draws the legend on the default branch and puts it, with the same in words, under the days in the README of every branch and in the default branch's list of samples - the default branch's README carries the legend and the words now; the other branches get them when they are generated again, and every branch's picture is redrawn on its next CI run --- .samples/ci-svg.py | 81 +++++++++++++++++++++++++++++++------------- .samples/generate.sh | 21 +++++++++++- .samples/legend.svg | 50 +++++++++++++++++++++++++++ README.md | 8 +++++ 4 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 .samples/legend.svg 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.