Skip to content

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.yaml
InputDefaultWhat it does
tokenrequiredA 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
userthe repository ownerThe account to collect when no config file is given
modeonceonce, backfill, card or migrate
backfill-since""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-layoutsummaryOne of the thirteen registered layouts
card-themeautodark, light, auto, or both for a light card and its _dark twin
card-fields""Comma-separated fields. Empty means the layout’s default
card-motiononceonce, loop or off; loop changes only terminal and ticker
card-width""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-speed""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-privatefalsetrue counts private repositories when no config file is given. See the warning below
versionlatestThe 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

One sweep against a configuration file you supply, which is how a workflow feeds a database.

name: Collect
on:
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.

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.

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.

  1. Create a personal access token (see the token) and store it in <you>/<you> as the secret GHCHRONICLE_TOKEN. The automatic GITHUB_TOKEN will 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.

  2. Add .github/workflows/card.yml:

    name: Profile card
    on:
    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-card
    cancel-in-progress: false
    jobs:
    card:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v7
    - 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
    - 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 push
  3. Paste this line into README.md wherever 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>
  4. 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.

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.json

and 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.

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 }}
Written and maintained by
MIT licenceRelease history