The format follows Keep a Changelog, and versions follow Semantic Versioning — for an action, the "public API" is the config schema, the action inputs and outputs, and the generated files.
- Three new styles.
constellationdraws a night sky: rank sets each star's size and glow, near neighbours join up into constellations, and two depths of dust fill the dark between them.skylinedraws a city: weight sets each building's height, the avatar sits up top like a rooftop billboard, and each building draws one of six roof silhouettes, a wall width, and a glazing for its lit windows from a deterministic lottery.metrodraws a transit map: stations on a coloured route that snakes across the wall, one line per section — or one per role withrole_lines, andweavemakes those lines interleave and cross one another like a real network — with the heavier terminus rings at both ends.constellationandskylinehonour per-userscale(skyline in height);metrois a fixed lattice and ignores it.
- The
svg_pathoutput is emitted again. It is declared inaction.ymland documented as the alias kept for workflows written beforeoutputsexisted, but nothing ever wrote it — those workflows have been reading the empty string. It carries the same value aspaths. - A user returned by more than one API source keeps a real display name instead
of a login. The contributors API reports no name, so the login stood in for
one, and first-wins then shadowed the name a
sponsorsentry did carry. - Two outputs naming the same file through different spellings (
./wall.svgandwall.svg) are rejected as duplicates. The run used to render both, leave only the second on disk, and still report two paths for it. - The config reference in the README is a config the parser accepts. Its
groupslist named two sections while the source blocks below it asked for three others, sogroupsdid the job it exists to do and rejected the page outright — anyone pasting "every key on one page" got three errors and no mural. The specs now parse and validate every config the README shows, render every committed example config, and check that a run emits exactly the outputsaction.ymldeclares. - An
avatar_urlpointing at the runner's own network is refused instead of fetched. Redirects were already held to that rule, but the first request was never checked, so an address written straight into the config — a cloud metadata endpoint, a service on the host — was fetched, labelledimage/pngwhatever came back, and base64-embedded into a file the run then commits. On a workflow that builds a mural from a pull request, that address is the contributor's to choose. The check now also judges a host by the addresses it resolves to rather than by how it is spelled, which is what a name pointed at an internal address, or the decimal form of one, used to walk past — and[::1]no longer slips through the redirect rule either, where the brackets were being read as part of a hostname. - A skipped avatar says why. Every failure — a 404, a timeout, a refused address — was reported as "avatar could not be fetched", which does not tell anyone whether to fix a typo, a permission, or a URL. The fetcher already worked out the reason; now it survives as far as the log.
- A control character in a name, role or section title no longer costs the whole
file. XML carries three of them and no parser will read a document holding any
of the others, but
HTML.escapeleaves them alone because in HTML they are legal — so one stray byte in a display name did not garble a label, it made the mural unreadable, with the run still exiting 0 and still committing. They are dropped now, in the one place every name, role and title passes through, which also covers the API-sourced names that never reach config validation.
- Pagination stops fanning out past what
maxcan use. A window is requested before any of it is read, so amaxthat ended two pages in used to buy four and discard two — up to three wasted requests per source, against a quota of sixty an hour without a token. Results are unchanged: a page whose contents are filtered away still sends the walk round again. voronoino longer clips every cell against every other seed. A seed too far off to reach what is left of a cell is ruled out by a bound the weight clamp already guarantees, and the clipper works out of two buffers instead of allocating the polygon afresh for each of those cuts. A wall of 4000 faces renders in half the time, one of 600 in a third.mosaicpacking starts at the first row that still has a free cell instead of rescanning the filled rows from the top for every avatar. A wall of 4000 faces packs in an eighth of the time.
Both are exact: the same seeds cut the same cells and every avatar lands in the same square, so a regenerated mural is byte for byte the file it was before.
users:entries take ascale, a 1–2 size multiplier applied after each style's own ranking.mosaicmultiplies the tier span,spiralandorbitmultiply the avatar size and re-pack.grid,honeycomb,stencil, andvoronoihave no per-user size and warn when asked for one.
- HTTP connections are pooled and kept alive per host, so a mural of a few hundred faces no longer pays for a few hundred TLS handshakes. Generated files are byte-for-byte unchanged.
- Work that only waits now overlaps: sources fetch together, pages after the
first fan out off GitHub's
Linkheader,.pngoutputs convert side by side, and a multi-output config fetches each target's avatars once. A collection still stops at the configuredmax, so this costs no extra API requests.
- Every HTTP request carries connect, read, and write timeouts. A stalled socket used to hang the job until the runner killed it, with nothing in the log.
- A throttled avatar (
429, or a timeout) is retried instead of dropped from the mural. Both clients honorRetry-After, capped at 30 seconds; an exhausted hourly quota still fails fast. - Retry backoff is jittered on the API client, so sources running side by side cannot back off in lockstep and retrip the same limit.
- The examples invite readers to add their own login and open a PR.
- Source blocks take a
weight, applied to every user they yield, sousers:carries only the exceptions and adding a contributor needs no config change. widthandheightaction outputs, describing the first generated file, so an embed with explicit dimensions can be corrected in the same run.excludeaccepts*and?wildcards, matching whole logins. No[...]classes, so*[bot]means "ends with the literal[bot]".- Every run prints its version first, and config errors that enumerate accepted values name the version that rejected them.
- Each released ref names its own immutable image tag:
@v1.1.0runs:v1.1.0, and@v1follows releases by the git tag moving. Only@mainfloats. - Voronoi clip-path ids are prefixed
vcell-, whichcrate-ci/typosno longer reads as a misspelling once per cell.
VERSIONwas left at0.1.0through both releases. It now matches the shard, a spec keeps the two together, and the release workflow refuses a mismatched tag.
- How to run the action locally through Docker, and why it cannot commit: an
unset
GITHUB_ACTIONSstops it, independently ofno_commit. include_bots: falsefilters what GitHub types as a bot. Older service accounts are typedUserand need an explicitexcludeentry.
voronoi— stained glass. Avatars clipped into irregular cells that tile the block edge to edge, separated by a hairline lead. Cell area follows weight, and no cell can be squeezed to nothing.stencil— the wall as a word. Avatars fill the lit pixels oftextin a built-in 5x7 face, unfilled pixels stay as faint dots, and a crowd larger than the word splits pixels rather than dropping people.
First public release.
grid— avatars in rows, as circles, rounded squares, or squares, with optional name and role labels.honeycomb— hex-clipped avatars in offset rows.mosaic— weight-tiered packing, so the heaviest contributors take the largest tiles.spiral— golden-angle phyllotaxis; rank sets both size and distance from the centre.orbit— the top contributor at the centre, everyone else on rings.
- A curated
userslist,contributors,members,stargazers, andsponsors(tier amount becomes weight). Writing a block enables it, and everything merges into one mural. - Per-user
rolelabels and titledgroupsections. avatar_urlaccepts a repository-relative file, for logos or contributors without a GitHub account.
- Self-contained SVG with avatars embedded as base64, so it renders inside a README.
- PNG via
rsvg-convert, including light/dark pairs throughoutputs[].mode. - Theme presets (
github,midnight,paper,mono) and anautomode that follows the viewer's dark-mode preference. - Several files in one run through
outputs, sharing one set of avatar fetches.
- Runs a prebuilt multi-arch image from GHCR, so a consumer's runner starts in seconds instead of building Crystal on every run.