Tracking Homebrew downloads with GitHub's API ============================================= By George Mandis https://george.mand.is/2026/05/tracking-homebrew-downloads-with-githubs-api Tuesday, May 26th 2026 If you distribute software through [Homebrew](https://brew.sh/) or [Scoop](https://scoop.sh/), you've probably wondered if anyone is actually instaslling your tools. Neither package manager gives you install analytics out of the box. But if your release pipeline flows through GitHub and produces platform-specific artifacts—tarballs, zips, binaries per operating-system and architecture combo—their API quietly tracks download counts on each release asset. This turns out to be a decent-ish heuristic depending on how you've setup your taps and buckets. ![Screenshot of the release page for Less Software which uses this technique](https://georgemandis.s3-us-west-1.amazonaws.com/media/less-software-release-page-preview.png) When you set up a Homebrew tap or Scoop bucket, the formula/manifest typically points to a specific release asset. These could be hosted anywhere, technically. If you're hosting them yoursef on an S3 or R2 bucket for example you can probably build better tools for tracking installs from package manangers—maybe even get [interesting user-agent data](/2026/05/who-reads-my-rss-feed/) to tell you more about how people are consuming them. On GitHub they track requests to your release assets for you! They don't publish them anywhere on the dedicated Releases page for your repo, but when someone runs `brew install georgemandis/tap/copycat` (for example), Homebrew downloads the tarball from the GitHub release for that project and GitHub increments the download count on that asset. This also happens if you click on an asset on the Releases page for your repo and download it from the browser. The GitHub API exposes this per-asset: ``` GET /repos/{owner}/{repo}/releases ``` Give it a look here for my CLI clipboard introspection tool [copycat](https://github.com/georgemandis/copycat): - [https://api.github.com/repos/georgemandis/copycat/releases](https://api.github.com/repos/georgemandis/copycat/releases) Each release object includes an `assets` array, and each asset has a `download_count` field. Sum those up across releases and you have a rough total. ![A screenshow showing the raw output of the API URL which you can pull up in the browser. 6 downloads. Voila](https://georgemandis.s3-us-west-1.amazonaws.com/media/github-releases-download-count.png) We can't distinguish between someone installing via Homebrew, downloading directly from the releases page, or `curl`-ing the URL from a script, but if the majority of your distribution is through package managers—and your releases have per-platform artifacts that only make sense to download through a tap or bucket—it's probably close enough to be useful. I'm using this right now over at [less.software](https://less.software), where I maintain brief pages for the tools I've been building. I wanted a [releases page](https://less.software/releases) that automatically pulls download stats, clone traffic, and version history for every project in my Homebrew tap and Scoop bucket. The setup is a bash script called `releases.sh` that runs during the site's GitHub Actions build. It lists the contents of my [`homebrew-tap`](https://github.com/georgemandis/homebrew-tap) and [`scoop-bucket`](https://github.com/georgemandis/scoop-bucket) repos via the API, takes the union of formula/manifest names, fetches release data for each project (versions, dates, per-asset download counts), pulls clone traffic from the [traffic API](https://docs.github.com/en/rest/metrics/traffic) (14-day rolling window, merged into a persistent `clone-history.json` so all-time numbers accumulate), and generates a Markdown page with the results. The workflow is simple: ```yaml - name: Fetch release data run: bash releases.sh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build site run: bash less.sh ``` The discovery step is convenient. When I add a new project to the tap or bucket it appears on the releases page automatically. If I'm on top of providing a nice description for my projects it even looks kind-of nice! Here's what the project discovery looks like in the script: ```bash # Discover projects from homebrew-tap homebrew_projects=$(cached_fetch \ "$API_BASE/repos/$GITHUB_OWNER/homebrew-tap/contents/Formula" \ | jq -r '.[].name | select(endswith(".rb")) | rtrimstr(".rb")') # Discover projects from scoop-bucket scoop_projects=$(cached_fetch \ "$API_BASE/repos/$GITHUB_OWNER/scoop-bucket/contents/" \ | jq -r '.[].name | select(endswith(".json")) | rtrimstr(".json")') # Union and deduplicate projects=$(printf '%s\n%s' "$homebrew_projects" "$scoop_projects" | sort -u) ``` And the per-release download counting: ```bash while IFS=$'\t' read -r tag_name published_at downloads; do total_downloads=$((total_downloads + downloads)) done < <(echo "$releases_json" | jq -r ' .[] | [ .tag_name, .published_at, ([.assets[].download_count] | add // 0) ] | @tsv ') ``` The result is a page like this, rebuilt daily: | Project | Versions | Downloads | Clones | | --------- | -------- | --------- | ------- | | copycat | 5 | 61 | 147 | | loupe | 5 | 35 | 81 | | whereami | 6 | 103 | 40 | | **Total** | ... | **232** | **442** | You can see the live version at [less.software/releases](https://less.software/releases). I extracted the core of this into a standalone script called [package-release-tracker.sh](https://github.com/georgemandis/package-release-tracker.sh). It takes a GitHub username, auto-discovers all `homebrew-*` and `scoop-*` repos for that user or org, and spits out a self-contained HTML page. No dependencies beyond `bash`, `curl`, and `jq`. Some fun ones to try it against: - [aws](https://github.com/aws) — SAM CLI, copilot, eksctl, and ~20 other tools in a centralized tap - [charmbracelet](https://github.com/charmbracelet) — gum (3.1M downloads), glow, vhs, soft-serve, and more - [goreleaser](https://github.com/goreleaser) — the tool many people use to build these release pipelines in the first place - [derailed](https://github.com/derailed) — k9s and popeye, distributed through separate per-project taps One important caveat: this only works for projects distributed through your own taps and buckets. Popular projects that graduate to [homebrew-core](https://github.com/Homebrew/homebrew-core) or Scoop's [main bucket](https://github.com/ScoopInstaller/Main) get installed from those central repos instead — their downloads no longer hit your GitHub release assets. If you look at a tap and find nothing but a `tap_migrations.json`, that's a sign the project has moved on. Something like `gcloud-cli`, for example, lives in `homebrew/cask` and downloads from Google's own CDN — completely invisible to this approach. The download count is cumulative and never resets. There's no time-series data for release assets. You know how many but not when. The clone traffic API gives you daily granularity, but only a 14-day rolling window, which is why I'm persisting and merging the data across builds on less.software. Like I mentioned earlier, there's no way to attribute downloads to a source. A `brew install` and a browser click on the `.tar.gz` link look identical. This is probably fine for 99% of projects. If you really care about a more accurate measurement you could explore hosting the assets yourself. Separate trick while I'm on the subject of GitHub Actions: my [GitHub profile](https://github.com/georgemandis) has a README that automatically updates daily with my latest blog posts and recent projects. I stole borrowed the idea from [Frank Chiarulli Jr.](https://github.com/fcjr), whose profile does the same thing with a Go script. Mine uses a short TypeScript file that runs with [Bun](https://bun.sh/): ```yaml name: Update README on: schedule: - cron: "0 0 * * *" push: branches: [main] workflow_dispatch: jobs: update-readme: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun run generate.ts - name: Commit and push run: | git diff --quiet README.md && exit 0 git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add README.md git commit -m "Update README with latest blog posts and projects" git push ``` The `generate.ts` script does two things: 1. Fetches my blog's [JSON feed](https://george.mand.is/feed.json) and extracts the five most recent posts 2. Hits the GitHub API for my most recently pushed repos, filters out forks and infrastructure repos (taps, buckets, the profile repo itself), and takes the top ten Then it writes a Markdown file with tables for each section. The workflow should commit and push only if the README actually changed. You need an RSS or JSON feed for the blog post part. If your blog already publishes one, you're most of the way there. The profile README repo is just a repo named after your GitHub username, and GitHub renders its `README.md` on your profile page. You can see the [workflow](https://github.com/georgemandis/georgemandis/blob/main/.github/workflows/update-readme.yml) and [generator script](https://github.com/georgemandis/georgemandis/blob/main/generate.ts) directly. The download tracking gives me a rough sense of whether anyone is using my tools. The README keeps my GitHub profile current without me thinking about it. Both run on free GitHub Actions minutes, which I'm keeping an eye on and may reduce to weekly if I start breaking out of the free threshold.