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
22 changes: 19 additions & 3 deletions STATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ git show metrics:downloads.csv | grep 'mac-arm64.dmg'

`badge.json`, on the same branch, holds the number the README badge shows, in the format a [shields.io endpoint badge](https://shields.io/badges/endpoint-badge) reads. It is **not** the total GitHub reports, and it is deliberately neither the larger nor the smaller number:

- **Stable tags only.** Release candidates and betas are excluded, by the `-` in the tag rather than by GitHub's `prerelease` flag, which is set on `v0.1.1` by mistake. A download of `rc.1` two months after 1.0.0 shipped is not somebody adopting the app. They hold 18 downloads between them, frozen since the snapshot of 2026-08-24.
- **Withdrawn assets still count.** An asset that disappears from the snapshots keeps whatever it had earned. Four macOS `.dmg` files were replaced when the signing key was rotated, and the live API now reports nothing for the 89 downloads that preceded that.
- **Stable tags only.** Release candidates and betas are excluded, by the `-` in the tag rather than by GitHub's `prerelease` flag. A download of `rc.1` a month after 1.0.0 shipped is not somebody adopting the app. They hold 38 downloads between them all-time; the assets still published hold 29 of those, flat at 18 from 2026-08-24 to 2026-09-07 and then up 11 in the snapshot of 2026-09-11, which is the kind of jump on tags nobody has a reason to install that makes the case for leaving them out.
- **`v0.1.1` is the exception, and the reason the flag is not used.** That tag is titled "v0.1.1 draft" and GitHub published it as a prerelease, not as a release: this project has shipped four, v0.1.0, v0.1.2, v1.0.0 and v1.0.1. But its three assets are the v0.1.0 binaries (`…Setup.0.1.0.exe`, `…-0.1.0.AppImage`, `…-0.1.0-arm64.dmg`), so their 42 downloads are downloads of the app as it then stood and count as v0.1.0's. Filtering on GitHub's flag would drop them.
- **Withdrawn assets still count.** An asset that disappears from the snapshots keeps whatever it had earned. Four macOS `.dmg` files on the shipped releases were replaced when the signing key was rotated, and the live API now reports nothing for the 89 downloads that preceded that. Three more went with them on the prerelease tags, worth another 9 that the badge does not count anyway.
- **A re-upload is a new asset, not a correction.** The replacement is counted on top of what the old file had, matched by `asset_id` rather than by guessing from a counter that went down. A counter that does go down is bad data and fails the run, because GitHub cannot produce one.

[`scripts/download-total.cjs`](scripts/download-total.cjs) does this, over the whole of `downloads.csv`, every time the workflow runs. To reproduce it:
Expand All @@ -56,12 +57,27 @@ node scripts/download-total.cjs downloads.csv

The consequence worth knowing: the badge moves once a week, when the workflow runs, not the moment someone downloads something.

### By release

Downloads recorded against each shipped release, as of the snapshot of 2026-09-11. The cumulative column is what the badge shows.

| Release | Published | Downloads | Cumulative | macOS | Windows | Linux |
|---|---|---|---|---|---|---|
| v0.1.0 | 2025-10-09 | 127 | 127 | 62 | 39 | 26 |
| v0.1.2 | 2026-07-31 | +7 | 134 | 68 | 40 | 26 |
| v1.0.0 | 2026-08-14 | +65 | 199 | 89 | 73 | 37 |
| v1.0.1 | 2026-08-21 | +41 | 240 | 106 | 90 | 44 |

The platform columns are cumulative too, so the last row is the badge broken down. v0.1.0's 127 is its own 85 plus the 42 recorded under the `v0.1.1` draft tag, for the reason above. The release candidates and the beta are not in this table and not in the badge.

1.0 is the whole story here: 106 downloads in the four weeks since, against the 126 those three tags had accumulated by the last snapshot before it. They are on 134 now, still gaining a download here and there.

### What the numbers are not

**They count HTTP requests, not people.** A re-download, a `curl` in a CI script, a retry after a dropped connection and a bot crawling the releases page each add one. Treat them as an interest signal, not an install count.

**Source archives and clones are not included.** Only uploaded release assets are counted — the auto-generated `.zip`/`.tar.gz` and `git clone` are not.

**The counter belongs to the asset, not the release.** Deleting a release file and re-uploading it restarts that file at zero, which is part of why these snapshots exist: they are the only record that survives a rename. It has already happened once, to the three artifacts replaced on the v0.1.2 draft, and again to the macOS `.dmg` files when the signing key was rotated. Since the snapshots record `asset_id`, a future one is visible rather than merely suspected.
**The counter belongs to the asset, not the release.** Deleting a release file and re-uploading it restarts that file at zero, which is part of why these snapshots exist: they are the only record that survives a rename. It has already happened once, to the three artifacts replaced while v0.1.2 was still a draft, and again to the macOS `.dmg` files when the signing key was rotated. Since the snapshots record `asset_id`, a future one is visible rather than merely suspected.

**History starts when the workflow did.** Everything before the first snapshot is unrecoverable — GitHub never stored it.
13 changes: 9 additions & 4 deletions scripts/download-total.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// does, gets the number wrong twice over.
//
// It undercounts, because the counter belongs to the asset and dies with it. Four macOS `.dmg`
// files were deleted and re-uploaded when the signing key was rotated, so 89 downloads that
// really happened are gone from the API. They survive only in the snapshots, which is the whole
// files on the shipped releases were deleted and re-uploaded when the signing key was rotated,
// so 89 downloads that really happened are gone from the API. They survive only in the snapshots, which is the whole
// reason the snapshots exist.
//
// And it overcounts, because it includes release candidates and betas. A download of `rc.1` two
Expand All @@ -25,8 +25,13 @@ function assetKey({ id, tag, asset }) {
return id ? `id:${id}` : JSON.stringify([tag, asset]);
}

// A prerelease is the semver suffix, not GitHub's `prerelease` flag: v0.1.1 is flagged
// prerelease on the API and is a real release with real users.
// A prerelease is the semver suffix, not GitHub's `prerelease` flag. The two disagree on
// `v0.1.1`, and the flag is the one that loses downloads: GitHub published that tag as a
// prerelease titled "v0.1.1 draft", never as a release, but its three assets are v0.1.0 binaries
// (`...Setup.0.1.0.exe`, `...-0.1.0.AppImage`, `...-0.1.0-arm64.dmg`). Their downloads are
// downloads of the app as it then stood, so they belong in the total, and they belong to v0.1.0.
// Filtering on the flag would silently drop 42 of them. The four releases this project has
// shipped are v0.1.0, v0.1.2, v1.0.0 and v1.0.1.
function isStableTag(tag) {
return !tag.replace(/^v/, '').includes('-');
}
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/download-total.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ test('isStableTag rejects the semver prerelease suffix', () => {
assert.equal(isStableTag('v1.1.0'), true);
});

test('isStableTag keeps v0.1.1, which GitHub flags as a prerelease by mistake', () => {
test('isStableTag keeps v0.1.1, whose assets are the v0.1.0 binaries', () => {
// GitHub flags that tag as a prerelease and it was never published as a release, but the
// files under it are v0.1.0 builds. Filtering on the flag would drop 42 downloads of the
// app as it then stood.
assert.equal(isStableTag('v0.1.1'), true);
});

Expand Down
Loading