History¶
tingle stores nothing: every run measures the tree in front of it. To watch a number drop over months, something has to keep the values — and on GitHub that something is a branch, not an artifact.
Artifacts are not history
upload-artifact gives you a file per run. Artifacts expire (90 days by
default), each is a separate download, and nothing plots them. Use them
to hand a number to the next job in the same workflow, not to remember
it.
The short version¶
tingle ships an action that records the numbers and publishes a chart of their history. History is a main-branch thing — one point per commit that landed — so the whole workflow is this:
name: Metrics
on:
push:
branches: [main]
jobs:
history:
runs-on: ubuntu-latest
permissions:
contents: write # the values are committed to a branch
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install tingle
- uses: fancysnake/tingle/actions/metrics-history@main
The action expects tingle on PATH, so it can also join a job that
already runs it — the CI gate, measuring the build the gate
judged. That job has to run on main pushes as well as pull requests, and
the step has to skip the pull requests:
- run: tingle check
- uses: fancysnake/tingle/actions/metrics-history@main
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
Record on main, not on pull requests
Without that if:, every pull request appends a point for a commit that
may never land — and one from a fork cannot append at all: its
GITHUB_TOKEN is read-only whatever permissions: asks for, so the
push fails and takes the job with it.
The recording step switches the checkout to the data branch and back, so the job's tracked files have to be clean by the time it runs — git will not switch away from a modified file. A build that rewrites a lockfile or a generated file has to commit or discard it first; the action stops with a named error rather than a raw git one.
Each run takes stat --json, appends the values to the gh-pages branch
under metrics/, and commits a page that plots one time-series per metric,
every point linked to the commit that produced it.
Turn Pages on — Settings → Pages → Deploy from a branch → gh-pages — and
the chart is at https://<owner>.github.io/<repo>/metrics/.
Pin the version
@main tracks the tip. Once it ships in a release, pin the tag —
@v0.5.0 — or a commit sha, like any other action.
Permissions¶
The job needs contents: write, as above: recording a value means pushing a
commit. The built-in GITHUB_TOKEN carries that scope once the job asks for
it; no personal access token is needed to push to your own repository.
Grant it on the job, not the workflow, so the rest of the build keeps the default read-only token.
The data branch¶
The action creates the branch when it does not exist yet — an orphan, so the history shares no commits with your source — and pushes to it from then on. Nothing to set up before the first run.
Inputs¶
| Input | Default | Meaning |
|---|---|---|
name |
tingle |
chart title; keep it stable, it keys the stored series |
config |
auto-discovered | path to tingle.toml |
data-branch |
gh-pages |
branch the history is committed to |
data-dir |
metrics |
directory in that branch holding the chart |
max-points |
every build | points kept per metric chart |
github-token |
github.token |
token used to push to the data branch |
A metric that fails does not lose the point¶
stat exits 1 when a metric raises — it names the metric on stderr, and
still reports every metric that worked. The action follows that: the broken
one is dropped from the chart, the rest are recorded. Only a run where
every metric failed has nothing to plot, and that fails the job.
When Pages is already taken¶
A repository has one Pages source. If yours is deployed from a workflow
artifact — MkDocs, Sphinx, any static generator, which is how these docs are
published — you cannot also serve the gh-pages branch. Point the data at a
branch of its own instead:
This is how tingle records its own metrics. Nothing renders that branch, but
the history is now a file you own: metrics/data.js, a single
window.BENCHMARK_DATA = {...} assignment. Its entries are keyed by chart
name — the name input — and each is an array of points, one per recorded
build, carrying that build's commit metadata and a benches list of
{name, unit, value} objects:
window.BENCHMARK_DATA = {
lastUpdate: 1767225600000,
repoUrl: "https://github.com/owner/repo",
entries: {
tingle: [
{
commit: { id: "…", timestamp: "…", message: "…", url: "…" },
date: 1767225600000,
tool: "customSmallerIsBetter",
benches: [{ name: "noqa-comments", unit: "count", value: 3 }]
}
]
}
};
Fetch that branch during your docs build and plot it into a page of your site, or read it from anywhere else that wants the numbers.
What it does, unrolled¶
The payload is two steps, if you would rather inline them — or adapt them to a CI that is not GitHub Actions. Every input above is left at its default here, so this is the action's own body:
- run: |
status=0
tingle stat --json > stat.json || status=$?
[ "$status" -le 1 ] || exit "$status"
jq '[.metrics[] | select(.error == null) | {name, unit: "count", value}]' \
stat.json > points.json
if [ "$(jq length points.json)" -eq 0 ]; then
echo "::error::every metric errored; there is nothing to record"
exit 1
fi
- uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
with:
name: tingle
tool: customSmallerIsBetter
output-file-path: points.json
github-token: ${{ github.token }}
auto-push: true
benchmark-data-dir-path: metrics
comment-on-alert: false
fail-on-alert: false
customSmallerIsBetter is that action's generic contract — a list of
{name, unit, value} objects, lower is better — which is exactly the shape
of a tingle metric. The jq drops metrics that errored, since they have no
value to plot, and the exit-status line keeps a single broken metric from
killing the run.
Inlined this way, the branch is your problem: the benchmark action fetches it and fails on a raw git error if it is missing. Create it once —
git checkout --orphan gh-pages && git commit --allow-empty -m "Start gh-pages"
git push origin gh-pages
— or use the action, which does it for you.
Leave the alerts off
The action can comment on, and fail, a run whose value regressed. This
one turns both off: tingle check already gates pull
requests, and it judges the branch's own impact rather than the raw jump
between two builds. The alert threshold is also a ratio, which says
nothing useful about a count sitting at 0.