GitHub Actions
The repository ships a composite Action, so a workflow needs no Go toolchain: it downloads a release binary and calls it.
- uses: jmrplens/ghchronicle@v2 with: token: ${{ secrets.GHCHRONICLE_TOKEN }} mode: once config: .github/ghchronicle.yamlInputs
Section titled “Inputs”| Input | Default | What it does |
|---|---|---|
token | required | A personal access token. The automatic GITHUB_TOKEN is not enough |
config | "" | Path to a configuration file. Omit to run with defaults built from user, whose state lasts one run and cannot be cached |
user | the repository owner | The account to collect when no config file is given |
mode | once | once, backfill, card or migrate |
backfill- | "" | Bound for backfill: a date, 90d, 2y or a Go duration. Empty means no bound |
card | "" | Path of the SVG to write. Empty means no card |
card- | summary | One of the thirteen registered layouts |
card- | auto | dark, light, auto, or both for a light card and its _dark twin |
card- | "" | Comma-separated fields. Empty means the layout’s default |
card- | once | once, loop or off; loop changes only terminal and ticker |
card- | "" | Card width in pixels. Empty draws the layout at its own width; each one draws between two ends of its own, stated in its section. Only activity-heatmap spends the room on data, a whole year of the calendar at its far end |
card- | "" | How fast an animated layout plays, as a decimal from 0 to 1. Empty means 0.5, the pace every card has always been drawn at; below it the card is slower, above it faster, and every animated layout scales together. 0 is the slowest animation and not a still card, card-motion: off is |
include- | false | true counts private repositories when no config file is given. See the warning below |
version | latest | The release to install: latest for the newest, or a release with or without its v, so 2.6.5 and v2.6.5 are the same one. The major tag v2 is what uses: takes, not a release, and is refused |
The four modes
Section titled “The four modes”One sweep against a configuration file you supply, which is how a workflow feeds a database.
name: Collecton: schedule: - cron: "*/30 * * * *" workflow_dispatch:
jobs: collect: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: jmrplens/ghchronicle@v2 with: token: ${{ secrets.GHCHRONICLE_TOKEN }} mode: once config: .github/ghchronicle.yaml env: INFLUX_TOKEN: ${{ secrets.INFLUX_TOKEN }}The config file refers to the database credentials as ${VAR}, so they go
in as secrets and never into the repository.
One sweep and one SVG, and nothing written to any database. This is the only mode that needs no store configured at all.
name: Profile cardon: schedule: - cron: "17 6 * * *" workflow_dispatch:
permissions: contents: write
jobs: card: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: jmrplens/ghchronicle@v2 with: token: ${{ secrets.GHCHRONICLE_TOKEN }} mode: card card: generated/github-stats.svg card-layout: github-stats card-theme: both - name: Commit if it changed run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add generated/ git diff --staged --quiet || git commit -m "Update the profile card" git pushThe SVG is byte-identical for the same input, so a day with no change produces no commit.
Reaches as far back as GitHub allows and waits for the rate limit rather than stopping. Run it once, by hand.
name: Backfillon: workflow_dispatch: inputs: since: description: "A date, 90d, 2y, or empty for no bound" default: "2y"
jobs: backfill: runs-on: ubuntu-latest timeout-minutes: 360 steps: - uses: actions/checkout@v7 - uses: jmrplens/ghchronicle@v2 with: token: ${{ secrets.GHCHRONICLE_TOKEN }} mode: backfill backfill-since: ${{ inputs.since }} config: .github/ghchronicle.yamlGive it a generous timeout-minutes: an unbounded backfill parks at every
rate limit reset, and a job that is killed half way has spent the quota and
kept part of the benefit.
Brings the stores the configuration writes along after an upgrade: it runs
-migrate -yes, which applies every change the
plan finds pending, the ones
that need somebody’s word too, and reads what it cleared again. Run it by
hand, once, after reading what -migrate says about the same
configuration.
name: Migrateon: workflow_dispatch:
jobs: migrate: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@v7 - uses: jmrplens/ghchronicle@v2 with: token: ${{ secrets.GHCHRONICLE_TOKEN }} mode: migrate config: .github/ghchronicle.yamlIt takes no card. A store holding rows of accounts this configuration does
not collect, or whose rows could not be compared with it, is held back and
the step fails, since only -migrate-others clears it, and that is a
decision to take at a terminal, not in a workflow. Without a state file
restored between runs, every
run of the Action starts from a new one, so a store that cannot be asked, a
SQL file, a Graphite or whatever is behind a Telegraf, has no history here
and is never taken to need a change; with one restored, those follow its
record as they would on a host. A job that timed out while reading the
history again leaves nothing the next job can resume from, since
actions/cache saves nothing for a job that did not succeed: run
mode: backfill for the same configuration after it.
In once and backfill, a start checks the stores the same way and warns
about what is pending. On a new state file, which is every run without one
restored, it applies nothing on its own even under migrate: auto: the state
file goes with the runner, and with it the record that a refill is still owed,
so a refill that failed or a job cancelled half way would leave a store cleared
and nothing saying so. With a state file restored it applies what is safe to
apply unattended, as a host does. The Action turns every line that says a
change is pending, one was applied at the start, one failed or a refill is
owed into an annotation on the run as the line is written, so it shows on the
workflow’s summary page rather than only in its log, a job cancelled half way
included.
A card in your profile README
Section titled “A card in your profile README”GitHub shows the README of the repository named after your account, <you>/<you>,
at the top of your profile. The card lives in that repository as a file, the
README points at it once, and a scheduled workflow replaces the file. The README
itself is never rewritten.
-
Create a personal access token (see the token) and store it in
<you>/<you>as the secretGHCHRONICLE_TOKEN. The automaticGITHUB_TOKENwill not do, even for public numbers: it is not a user, so the Action cannot list your repositories with it and the card comes out empty. -
Add
.github/workflows/card.yml:name: Profile cardon:schedule:- cron: "17 6 * * *"workflow_dispatch:permissions:contents: write# One run at a time: two that overlap would race each other to push.concurrency:group: profile-cardcancel-in-progress: falsejobs:card:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v7- uses: jmrplens/ghchronicle@v2with:token: ${{ secrets.GHCHRONICLE_TOKEN }}mode: cardcard: generated/card.svgcard-layout: animated-counterscard-theme: bothcard-motion: once- name: Commit if it changedrun: |git config user.name "github-actions[bot]"git config user.email "41898282+github-actions[bot]@users.noreply.github.com"git add generated/git diff --staged --quiet || git commit -m "Update the profile card"git push -
Paste this line into
README.mdwherever the card should appear, once:<picture><source media="(prefers-color-scheme: dark)" srcset="generated/card_dark.svg"><img src="generated/card.svg" alt="My GitHub statistics"></picture> -
Run the workflow by hand from the Actions tab the first time, so the files exist before the first scheduled run.
More than one card. Repeat the uses: jmrplens/ghchronicle@v2 step with
another card path and layout, and paste one <picture> per card. Each step is
a sweep of its own, and each draws every number again: a run that writes a card
collects every family whatever its cadence says, and one in card mode writes
nothing back to the state file. Several card steps can therefore share one
config, and so one state_file, and each card is the whole account. That is
also what each one costs: a card is a cold sweep of every family, so N cards
are N sweeps of the per-family price.
A README that is not at the root. This is for other repositories, since
GitHub shows a profile’s README only from the root of <you>/<you>. The paths
in the <picture> are relative to the README, so a docs/README.md points at
../generated/card.svg.
Two things a hosted runner does not keep
Section titled “Two things a hosted runner does not keep”The state file does not survive between runs. Without it every run is a
first run. Every family runs, whatever its cadence says, because nothing
remembers when it last did. The full stargazer walk is done again, and every
repository’s daily star history is read whole, a page per thirty weeks of its
life. achievements walks the account’s merged pull requests whole for its
co-authored count, which was 35 queries and 23.7 MB on an account with 2,315
of them. And with no cache file
beside the state, nothing is asked with an ETag, so no answer is a free 304. On
a small account that is a handful of calls; on an account with many stars, old
repositories or a long history, cache it.
Caching needs a config: file. Without one the Action writes a configuration
of its own and puts the state in a new temporary directory under RUNNER_TEMP
on every run, which no cache step can name. With one, point state_file at the
home directory:
state_file: ~/.ghchronicle/state.jsonand restore and save that directory with a step before the ghchronicle one:
- uses: actions/cache@v6 with: path: ~/.ghchronicle key: ghchronicle-state-${{ github.run_id }} restore-keys: ghchronicle-state-A ~ in state_file is the home directory from 2.6.1 on. An older release
reads it as written, a directory called ~ in the checkout that the cache step
never sees, so with a version older than 2.6.1 write the path out:
/home/runner/.ghchronicle/state.json on a GitHub-hosted Ubuntu runner.
The directory then holds the cache file as well, which a once step writes and
a backfill step only reads, so a run restored from it also asks GitHub with
the validators the last one stored, and pays only for what changed. A card mode run reads a restored state file, which
is what lets it skip both walks, and never writes one back, nor the cache file
beside it: it delivers its points to the card and to no store, so nothing it
learned may tell the next collection that a family is already done. What fills
the actions/cache entry is a once step, with both files, or a backfill
step, with the state file alone. No step keeps a write ledger there: a run that
ends with its sweep opens none.
Without the Action
Section titled “Without the Action”The same thing by hand, if you would rather not depend on it:
- uses: actions/setup-go@v7 with: go-version: stable- run: go install github.com/jmrplens/ghchronicle/v2/cmd/ghchronicle@latest- run: ghchronicle -config .github/ghchronicle.yaml -card profile.svg -card-only env: GITHUB_TOKEN: ${{ secrets.GHCHRONICLE_TOKEN }}