A distribution check against the freshness table on 2026-08-13 came back like this: 331 rows, 153 of them sitting at exactly releases_365d = 30, nothing above 30, maximum 30.
That column is meant to say how many releases a project tagged in the last year. It is a per-repository count of a thing that varies enormously between projects, so a histogram with a wall at one value and a vacuum above it has nothing to do with release engineering. It has to do with the fetch.
One line, and it was never wrong-looking
gh.request(`/repos/${fullName}/releases?per_page=30`)Fetch a page of releases, filter the ones inside 365 days, count them. GitHub's default page size for that endpoint is 30, so writing it out changed nothing and hid nothing. The loop that should have followed it did not exist.
Every project shipping a release more often than about every twelve days therefore reported 30 and could not physically report 31. Nothing threw. Nothing logged. Thirty releases in a year is an entirely believable number for an active project, which is why it survived review and only fell out of a select over the whole column.
The comparison page for two spec frameworks printed "30 | 30" side by side and concluded that neither was pulling away. Both of those cells were the page size.

A task tracking dashboard with categorized items, status values, and color-coded indicators — The system renders live tracked work with structured organization and populated data columns.
The quieter damage was in the derived field
median_release_gap is the project's own typical interval between releases, and the score reads it to ask whether a project has broken its own cadence. It was computed over every release the fetch returned, which meant over whatever time span thirty releases happened to cover.
For a weekly project, thirty releases is about seven months of history. For one that ships twice a year, it is fifteen years. Both medians then went into the same column and were compared. Every row was internally consistent and no two rows were measured over the same thing.
The fix is that the window is now a time span, identical for every project, and the raw count only exists to fill it:
const cutoff = Date.now() - (WINDOW_DAYS + MARGIN_DAYS) * DAY;
for (let page = 1; page <= MAX_PAGES; page++) {
const res = await gh.request(
`/repos/${fullName}/releases?per_page=100&page=${page}`,
{ etagKey: `ss:releases:${fullName}:p${page}`, allow404: true },
);
if (res.status === 404 || !Array.isArray(res.data)) break;
data.push(...res.data);
if (res.data.length < 100) break; // end of the list
const oldest = res.data[res.data.length - 1]; // newest-first, so last is oldest
const at = oldest?.published_at || oldest?.created_at;
if (at && new Date(at).getTime() < cutoff) break; // window is covered
}Three details in there earned their place. MARGIN_DAYS is 60, because the median needs the gap that straddles the one year boundary, and a window cut exactly at 365 days silently drops the oldest gap from every project. The etagKey is per page: one key shared across pages would have each page overwrite the previous body in the cache and serve the wrong 304, which is a worse failure than the original because the result would look like real data. And the loop almost never runs to MAX_PAGES, because the cutoff condition fires first for everything except the extremes.
Verified against live GitHub with the paging in place:
| Repository | releases_365d before | after |
|---|---|---|
| anomalyco/opencode | 30 | 533 |
| langchain-ai/langchain | 30 | 381 |
| n8n-io/n8n | 30 | 384 |
| Significant-Gravitas/AutoGPT | 30 | 50 |
| open-webui/open-webui | 30 | 48 |
The new cap had to be able to announce itself
Replacing a silent ceiling with a higher silent ceiling is the same bug with a bigger number. MAX_PAGES was first set to 12, chosen against opencode at 533 releases a year, which read as better than 2x headroom. The first nightly run printed a warning for lobehub, which publishes 1,052 releases in 365 days and needs 13 pages to reach past the cutoff. The cap was one page short on its first night, and it said so, in a line that names the repository and states that the field is a floor rather than a count. It is 25 pages now.
There is a field in the same file that was always honest about its own clamp. recent_commits comes from a single 100 per page commits call, and the scorer renders it as 100+ rather than 100. Same shape of limit, one character of difference in the output, and nobody ever read it as a measurement.
That's how we built StillShipping.
One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did — read off the repository and the live site, not written from memory. Join the list.
← All writing