Skip to content

About

Collects every metric GitHub exposes about an account and keeps it with the date it happened

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

90 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ghchronicle

ghchronicle

CI Release Downloads Quality Gate Coverage Go Reference Go Version ghcr.io Docker Hub License: MIT Platform

Collects everything GitHub will tell you about an account, and keeps it with the date it happened.

GitHub answers most questions about the present and almost none about the past. The traffic API serves fourteen days and forgets. The activity feed keeps three hundred events, none older than thirty days. Inbox notifications are kept for three months unless they are saved. The star list will tell you when each star was given, but since July 2026 only to the repository's admins and collaborators. None of it is archived anywhere unless you archive it.

ghchronicle sweeps those surfaces on a schedule and writes every observation as a dated point, so a year from now the question "how fast were we merging in July" still has an answer.

Start

Two commands on a machine of your own:

curl -fsSL https://raw.githubusercontent.com/jmrplens/ghchronicle/main/install.sh | bash
ghchronicle -setup

The first takes the newest release and refuses anything whose checksum is not the one the release published. The second asks what a working configuration needs, checks each answer against the thing it names, and writes it: a token that cannot read the account says so there, not at the first sweep. It offers to set up a service too, and the installer offers to run it for you.

On Windows, irm https://raw.githubusercontent.com/jmrplens/ghchronicle/main/install.ps1 | iex and then the same -setup.

Or with Docker, where the whole stack comes up together:

# compose.yaml from https://jmrp.io/docs/ghchronicle/install/docker/
printf 'GITHUB_TOKEN=github_pat_...\nGITHUB_USER=your-login\n' > .env
docker compose up -d

Grafana is on http://localhost:3000 with the dashboard already in it: the collector publishes it on start and points it at the store beside it, so there is nothing to import and no datasource to fill in. The Docker page has one compose file per store, written by one generator that CI holds them to.

ghchronicle -config config.yaml    # what the service ends up running

What it draws

Five Grafana dashboards, one per store Grafana can query, generated from a single specification and shipped in the repository. This is one section of the seventeen, drawn from a demonstration account:

The contributions section of the InfluxDB dashboard: contributions over time, a contribution calendar, the commit mix, commits per week, per hour of day, per weekday and per repository, the yearly totals, and the commits a profile hides

And the headline section, which every dashboard opens with:

The overview section: repositories, stars and forks; views, unique visitors and clones in range; followers, following, sponsors and sponsoring; and the account's own figures

What it renders

A profile card, from the same sweep, in thirteen layouts and two families. The animated ones animate where the reader's browser lets them. Each card here is two files from one sweep, one per palette, and GitHub shows the one that matches the theme you read it in:

The animated-counters layout: a grid of large numbers over a contribution sparkline

The github-stats layout: a header band, rows of four monospace numbers and a language share bar with its legend

The badge-row layout: a horizontal row of small pill badges, each with a label and a number

ghchronicle -config config.yaml -card card.svg -card-layout animated-counters -card-theme both

The thirteen layouts, with what each one draws and how to put one in a profile README.

Documentation

The full documentation is at https://jmrp.io/docs/ghchronicle/, in English and Spanish: quickstart, the 95 measurements, choosing a store, the cost of a sweep and troubleshooting. Every page also serves itself as markdown at its own path with index.md on the end, and llms.txt indexes the lot. The copies under docs/ are generated from those pages.

CHANGELOG.md says what changed in each release and what was left unproven; the notes on each tag say what landed.

What it collects

Ninety-five measurements across thirty-four families, covering every surface a personal or organisation account exposes.

Area What is kept
Traffic Views, unique visitors and clones per day, referrers and paths. GitHub's window is 14 days; this rewrites it whole on every sweep, so a collector that was down for a day repairs itself on the next run
Stars Stars per day for every repository, from GitHub's daily star history, back to the first. Where the token may read the stargazer list (since July 2026: admins, collaborators), one point per star as well, naming who gave it and when
Repositories Stars, forks, watchers, open issues, size, age, idle days, licence, visibility, languages by bytes, topics, community profile score
Releases Downloads per release and per asset, asset sizes, draft and prerelease state, and the moment each release was published
Pull requests Per item: time to first review, time to merge, lines added and deleted, files changed, review rounds, comments, commits
Issues Per item: time to close, comments, reactions, label count
Actions Runs with duration and queue time, jobs, individual steps, workflows and their state, artifacts and their expiry, cache usage
Security Dependabot and code scanning alerts by severity, plus an explicit record of which features are switched on, so no data is distinguishable from no alerts
Contributions The whole profile calendar, one point per day at that day's date, plus totals and the per-repository commit breakdown
Activity The event feed, which GitHub caps at three hundred events and thirty days, and the notification inbox, which it keeps for three months unless saved
Billing Usage per day, product, SKU and repository, with gross, discount and net
Account Followers, following, packages, gists, social accounts, sponsors
Elsewhere Pull requests and issues in other people's repositories, with their size and what became of them, the comments and accepted answers left there, and the stars of each repository they went to

Where it writes

Eleven destinations, and more than one at a time is the normal arrangement. Everything but the Prometheus exporter is pushed, and Prometheus itself can be fed through its OTLP receiver, so nothing here needs to be scraped and the collector runs wherever it can reach its databases.

Store Keeps Good for
InfluxDB the dated history "how fast were we merging in July"
PostgreSQL / TimescaleDB the dated history, in a database it connects to or as SQL for psql a Grafana user who has a Postgres and no InfluxDB
Graphite the dated history an existing Graphite
Elasticsearch / OpenSearch the dated history, as documents search across everything collected
Prometheus the current value alerting, and a number on a wall
OpenTelemetry either, depending on the backend an existing collector pipeline
Loki the events, as log lines "what happened, in order"
Telegraf whatever Telegraf can reach Kafka, Graphite, Datadog, anything with a Telegraf output
File and stdout line protocol or JSON a shipper you already run, and a durable buffer

The difference that decides which to use is dating. InfluxDB keys a point by measurement, tag set and timestamp, so replaying the same fourteen-day traffic window every six hours converges on the right answer instead of accumulating copies; the whole backfill design rests on that. Prometheus cannot: it stamps a sample at scrape time and rejects meaningfully older ones. Measured against Prometheus 3.14 with the OTLP receiver enabled and a thirty-minute out-of-order window, a sample dated two days back comes back as HTTP 400. So the reduction to current values happens before Prometheus ever sees the data, which is also what stops it being served one series per star.

Dashboards

One dashboard, rendered once per store, in dashboards/. Same sections, same panels, same positions, whichever database you chose; where a store cannot answer a panel honestly the panel is still there and says why. All in English and in Grafana's shareable export format, so importing asks you to pick your own datasource.

Import from the Grafana UI (Dashboards, New, Import) or with the API. They are generated from one specification, internal/dashboards, by go run ./cmd/gen_dashboards; edit the specification rather than the JSON.

The other ways in

Start is the short one. The rest, for a machine where it does not apply. Build it yourself:

go install github.com/jmrplens/ghchronicle/v2/cmd/ghchronicle@latest

or take a binary from the releases page, or run the container:

docker run -v $PWD/config.yaml:/config.yaml:ro -v ghchronicle-state:/var/lib/ghchronicle \
  -e GITHUB_TOKEN ghcr.io/jmrplens/ghchronicle -config /config.yaml

with state_file: /var/lib/ghchronicle/state.json in the configuration. The image carries /var/lib/ghchronicle owned by its uid 65532, and a new named volume mounted there takes that owner, so the volume keeps the state and the cache from one container to the next with nothing to hand over; a host directory mounted there instead needs sudo chown 65532:65532 first, or every sweep warns state not saved and cache file not saved. Up to 2.6.0 the image had no such directory, and a new volume there belonged to root. A configuration that names no state_file writes to the container's working directory instead, which is writable and goes with the container.

For the whole path on one system rather than these few lines: Linux, macOS and Windows each name the archive, check it against checksums.txt and the cosign signature published beside it, and end with something that keeps the sweep running: a systemd unit, a launchd agent, a scheduled task. Docker and GitHub Actions are the two that want no host of your own.

Configure

Two decisions are enough to start, and none of the installs above leaves a file behind to copy:

github:
  token: ${GITHUB_TOKEN}
targets:
  user: your-login
sinks:
  stdout: true

A ${VAR} in a credential, an address or a file path is read from the environment, so the file can be committed while the secrets stay out of it; any other value is read as written. config.example.yaml in this repository is the documented version, with a comment on every option there is. It collects every family it knows except three that ship switched off, deps, history and joblogs, each turned on by giving it a cadence under every.families; and when groups names the groups you want, the rest are neither read nor written. ghchronicle -groups lists them.

ghchronicle -list          # the repositories that would be collected
ghchronicle -once          # one sweep, then exit
ghchronicle                # run on the configured schedule

The token needs read access. Traffic additionally needs push access to the repository and, on a fine-grained token, the repository permission Administration (read), which a workflow's automatic GITHUB_TOKEN cannot be granted; Dependabot alerts need security_events, and the keys family needs read:public_key and read:gpg_key, which no other scope implies. Anything the token cannot see is recorded as unavailable and skipped, not treated as a failure: a repository with a feature switched off must not stop the sweep for the other forty.

What GitHub will not give you

Written down so nobody spends an afternoon rediscovering it. On a personal account, stats/code_frequency and stats/contributors answer 202 with an empty body indefinitely. The three per-product billing endpoints are 410 Gone and only the /users/{login}/ form of the usage report works. Custom repository properties, classic projects, cost centres and the audit log are organisation or enterprise only. workflows/{id}/timing returns 200 with an always-empty billable. GraphQL reports zero packages while REST lists them, so packages come from REST.

Rate limit

The collector never spends the last reserve_rate calls of any bucket, so whatever else uses the same token keeps working. GitHub runs fifteen independent budgets and names the one it charged in a header; the reserve is tracked per bucket and scaled to each, because search allows thirty requests a minute against core's five thousand. Responses are cached by ETag, and a 304 costs no quota at all, which is what makes short cadences affordable. The cache is kept in a file beside the state file, so a restart asks with the validators the process before it stored instead of paying for every answer again.

Each family has its own cadence because they move at very different speeds: workflow runs every fifteen minutes, the contribution calendar every hour, the account's SSH and GPG keys once a day. The cheapest thing here by far is GraphQL: one query returns the full 366-day contribution calendar, every contribution total, the per-repository commit breakdown and the social counts, for one point of a five thousand point budget.

A backfill is the opposite intention and says so: -backfill walks every surface to the end, bounded by a date you choose or by nothing at all, and when a bucket runs out it waits for the window to reset rather than giving up.

A backfill that GitHub cuts short is not thrown away. It keeps a checkpoint of what each family covered, -backfill-status reads that checkpoint and prints what is left without asking GitHub anything, and -backfill-retry 1h goes back an hour later for the families still missing, until a pass records nothing new or ten of them have run.

Three things cannot be backfilled at any price, and the documentation says so rather than letting you find out: the event feed keeps three hundred events and none older than thirty days, traffic is fourteen days, and job logs are deleted after the repository's retention period, ninety days by default. From 1 October 2026 workflow runs follow that same retention setting, so a backfill reaches only the runs it still keeps.

A card for a profile README

A side feature, not the point of the project. The point is the ingestion above.

ghchronicle -config config.yaml -card profile.svg -card-only

One sweep, one self-contained SVG: no webfont, no external stylesheet, no script, and byte-identical output for the same input so a scheduled job that commits it does not produce a diff on every run. Thirteen layouts in two visual families, one of them GitHub's own look, all with a choosable set of fields.

Nine of the thirteen animate, and the animation is a reveal, so it plays once and settles. -card-motion loop does not replay it: replaying a reveal hides what the reader has already been shown. It keeps going only what ends nothing, which is the terminal's cursor and the ticker's band, so on the other eleven loop draws what once draws, to the byte, and off draws the finished card with no animation at all.

-card-width sets the width in pixels, between the two ends each layout declares and -card-layouts prints. Most of them spread the same content wider; activity-heatmap spends the room on data instead, one more week of the contribution calendar at a time until the whole year is drawn, and badge-row ignores it, its width following its pills.

-card-speed is how fast that animation plays, a decimal from 0 to 1 and one number for the whole card: every animated layout scales by it, the cursor and the band with the reveals. 0.5 is the default and is exactly the card the renderer has always drawn, to the byte; 0 is the slowest animation and 1 the fastest. 0 is not a still card, -card-motion off is.

The repository ships as a composite Action:

- uses: jmrplens/ghchronicle@v2
  with:
    token: ${{ secrets.GHCHRONICLE_TOKEN }}
    mode: card
    card: generated/card.svg
    card-layout: animated-counters
    card-theme: both
    card-motion: once

Paste <picture><source media="(prefers-color-scheme: dark)" srcset="generated/card_dark.svg"><img src="generated/card.svg" alt="My GitHub statistics"></picture> into the README once; the workflow only ever replaces the files. The whole workflow, and what include-private would publish, is in A card in your profile README.

See docs/card.md, and .github/ACTION.md for how the Action is published and which token it needs.

Contributing

Issues and pull requests are welcome. CONTRIBUTING.md says how the repository is laid out, what has to pass before a pull request can be merged, and what a change to the collectors owes the documentation. SECURITY.md is where a vulnerability goes, and it is not a public issue.

Licence

MIT. See LICENSE.

About

Collects every metric GitHub exposes about an account and keeps it with the date it happened

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages