A freshness dashboard for the ossia / celtera / sat-mtl repositories. It answers, at a glance: is everything we depend on up to date, and is everything that depends on us up to date?
It tracks, across every configured repo:
- submodule pins — freshness vs the target repo, and whether the pin is even on the target's default branch;
- third-party dependencies — a unified inventory from
deps.yaml, shell version files (versions.sh), CMakeFetchContent/ExternalProject/release URLs, andvcpkg.json, each graded against the upstream's latest tag and HEAD, with commit-distance columns (vs main / vs latest / latest→main) and fork-revival + floating-branch detection; - open PRs — behind-base and idle time;
- branches — real unmerged commits (cherry-pick/rebase aware), PR-less;
- GitHub Actions —
uses:versions and mutable refs; - build environments — CI distro images / runners,
Dockerfile.llvm,tools/fetch-sdk.sh, vs endoflife.date and upstream releases; - downstream packaging — Repology, per project.
Everything is graded ok / info / warn / crit and the index page is a single
attention feed of everything at warn or above.
Architecture and the data model live in DESIGN.md. This README is the operations guide: how to run it and, above all, how to keep it up to date as the projects change.
npm install
npm run collect # gather data → data/snapshot.json
npm run build # render site → dist/
npm run dev # live preview at http://localhost:4321/dashboard
npm run check # type-check the collector + sitecollect and build are independent: build just renders whatever is in
data/snapshot.json. You can edit configs, re-run collect, and rebuild without
touching the network beyond what collect itself fetches.
| var | effect |
|---|---|
GITHUB_TOKEN |
enables live PR data (GraphQL), org discovery, and private-repo access. Without it those degrade gracefully (PRs fall back to fixtures/prs.json, org discovery is skipped). |
DASHBOARD_PAT |
CI only: a PAT that takes precedence over GITHUB_TOKEN, used to reach private repos in other orgs (e.g. sat-mtl/*) that the workflow token can't see. |
LOCAL_REPOS_DIR |
if set, the collector uses existing checkouts at $LOCAL_REPOS_DIR/<repo-name> instead of cloning. Handy for fast local iteration when the repos are already on disk. |
GITCACHE_DIR |
where bare commit-graph clones are cached (default .gitcache). CI persists this across runs with actions/cache. |
DASHBOARD_BASE |
site base path (default /dashboard; set to / for a custom domain). |
A normal local run with a token:
GITHUB_TOKEN=ghp_xxx npm run collect && npm run buildEvery section is failure-isolated: if a transport is unavailable (no token,
blocked host, timeout), that section is marked fixture or unavailable in the
snapshot and flagged in the site banner — the build never fails because of it.
The collect log ends with a section sources: line so you can see at a glance
which sections were live.
Almost everything is driven by the five files in config/. Adding coverage is
a config edit, not code. Each recipe below is self-contained.
| file | what it controls |
|---|---|
config/repos.yaml |
which repositories are tracked + dynamic org discovery |
config/dependencies.yaml |
non-submodule dependency sources (version files, CMake scan, vcpkg) |
config/updates.yaml |
automated update-PR scope (which sources/upstreams get bump proposals) |
config/watch.yaml |
watched file pins, CI image→EOL mapping, matrix coverage, Repology projects |
config/thresholds.yaml |
severity thresholds |
config/ignore.yaml |
silenced findings (false positives) |
Add it to the repos: list in config/repos.yaml:
repos:
- slug: ossia/score
depsRegistry: 3rdparty/deps.yaml # optional: path to a deps.yaml registry
- slug: ossia/my-new-repo # ← newThat one line opts the repo into every collector: submodules, branches, PRs,
actions, and CMake dependency scanning. depsRegistry is only needed if the repo
contains a deps.yaml-style registry (currently only ossia/score does).
The default branch is detected automatically (master vs main, etc.) — you
never specify it.
Instead of listing dozens of repos by hand, expand a name glob across an org. In
config/repos.yaml:
ignoreArchived: true # global (default true): archived repos are ignored
# everywhere — explicit list and discovery alike
orgs:
- org: ossia
include:
- "score-addon-*" # globs: * and ? supported
- "ossia-*"
exclude:
- "*-deprecated"
includeArchived: true # optional per-org override of the global settingAt collect time the GitHub API is queried and every matching repo is added to the
tracked set (deduped against the explicit repos: list). This needs a token
(GITHUB_TOKEN); without one it is silently skipped and only the explicit list is
used. Private repos appear only if the token can see them.
Archived repositories are dropped from the tracked set globally
(ignoreArchived: true, the default) — both discovered ones and any that end up
in the explicit repos: list. Set it to false, or a specific org's
includeArchived: true, to keep them.
Cost note: each discovered repo is cloned (shallow) and ls-remoted by the collectors. The
.gitcachemakes subsequent runs cheap, but the first run after adding a broad glob will be slower.
Dependencies come from five source kinds. Pick the one that matches how the dependency is pinned.
Nothing to do here — that registry is read automatically from every repo that
declares depsRegistry: in repos.yaml. To add or fix an entry, edit
deps.yaml in the score repo (it is the single source of truth, also consumed
by Renovate). An entry looks like:
- name: fmt
upstream: fmtlib/fmt # owner/repo, or gitlab:owner/repo
upstream_version: "10.2.1" # for tag-tracked deps
- name: some-fork
upstream: original/project
upstream_sha: "a1b2c3d4e5f6" # for SHA-tracked forksThese are export NAME_VERSION=value lines. Map each variable to its upstream in
config/dependencies.yaml:
versionFiles:
- repo: ossia/sdk
file: common/versions.sh
vars:
SDL_VERSION: { upstream: libsdl-org/SDL, prefix: "release-" }
FFMPEG_VERSION: { upstream: FFmpeg/FFmpeg, prefix: "n" }
MESON_VERSION: { upstream: mesonbuild/meson } # prefix omitted = bare tagsprefix is the string the upstream puts in front of the numeric version in its
git tags (e.g. SDL tags releases release-2.32.10, so prefix: "release-").
The pinned value in the file is usually the bare number; the prefix tells the
collector which tag namespace to compare against. Getting the prefix right is the
difference between a correct "latest" and a phantom one — see
Fixing a wrong "latest" below.
To track a new version file, add another entry under versionFiles. The repo
does not need to be in repos.yaml (the file is fetched directly).
Automatic. Every tracked repo's *.cmake and CMakeLists.txt files are
scanned for:
FetchContent_Declare(name GIT_REPOSITORY <url> GIT_TAG <ref>)ExternalProject_Add(name GIT_REPOSITORY <url> GIT_TAG <ref>)URL "https://github.com/owner/repo/releases/download/<tag>/..."
GIT_TAG is classified as a version tag, a commit SHA, or a floating branch
(master/main — flagged as non-reproducible). So just make sure the repo
containing the CMake file is tracked (recipe 1 or 2). To scan a repo that is not
otherwise tracked, add it under cmakeScan in config/dependencies.yaml:
cmakeScan:
repos:
- some-org/some-repo(Tracked repos are always scanned; this list is only for extra ones.)
Add the repo under vcpkg in config/dependencies.yaml:
vcpkg:
repos:
- sat-mtl/carto-tcp-avendishPorts are inventoried with any version overrides. vcpkg resolves ports against a registry baseline rather than a git tag, so these are listed for completeness and not version-compared.
For arbitrary pins in arbitrary files — a base image, a hard-coded CMake version,
an SDK tag — use a regex watch in config/watch.yaml:
watches:
- repo: ossia/score
file: cmake/Deployment/Linux/AppImage/Dockerfile.llvm
pins:
- name: AppImage base image
regex: 'FROM\s+almalinux:(\S+)' # exactly one capture group = current value
latest: eol:almalinux # resolver, see below
- name: AppImage CMake
regex: 'cmake-([0-9.]+)-linux'
latest: git-tag:Kitware/CMake # highest semver tag of that repoThe latest: resolver is one of:
| resolver | meaning |
|---|---|
git-tag:<owner/repo> |
highest version-looking git tag |
git-tag:<owner/repo>#<prefix> |
…restricted to tags starting with <prefix> |
eol:<product> |
an endoflife.date product; reports EOL status + newest cycle |
none |
just surface the value, no comparison |
The regex must have exactly one capture group (the current value). These appear on the environments page.
Container image: values and runs-on: runners are extracted from every
workflow automatically. To grade a distro against EOL dates, map its image prefix
to an endoflife.date product in config/watch.yaml:
imageProducts:
ubuntu: ubuntu
debian: debian
fedora: fedora
almalinux: almalinux
opensuse/leap: opensuseTo also get a "a newer release exists but isn't in the CI matrix" signal for a
distro family, list it under matrixCoverage:
matrixCoverage:
- ubuntu
- debian
- fedoraIn config/watch.yaml, add the repology project slug — the last path segment
of repology.org/project/<slug>/versions (check the project's repology page or
its README packaging badge):
repology:
- ossia-score
- libossia
- libremidi
- avendishA slug with no downstream packages renders harmlessly as "no data".
All thresholds live in config/thresholds.yaml (days unless noted). For example,
to consider a submodule critical only after two years and warn open PRs sooner:
submodule:
behindCritDays: 730
pr:
idleWarnDays: 14See the file for the full set (submodule behind/off-branch, fork-revival window, branch idle, PR idle/behind, action majors-behind, environment EOL window).
When a finding is known-fine, add an entry to config/ignore.yaml. The match
is a substring of the attention item id (<category>:<repo>:<subject>, shown
implicitly on the page; the format is in collector/lib/severity.ts). Every
entry requires a reason:.
ignore:
- match: "branch:ossia/score:gh-pages"
reason: generated documentation branch, never has a PR
- match: "dependency:ossia/score:deps.yaml:some-lib"
reason: intentionally held back pending API migration (see #1234)Ignored items are removed from the attention feed but still appear (ungraded) in their detail table.
If a dependency shows a nonsensical "latest tag" (e.g. a date or a conference codename), the upstream has junk tags polluting version detection. Two levers:
- Give it a prefix. For version files use
prefix:(recipe 3b); for file watches usegit-tag:<repo>#<prefix>(recipe 4). The collector then only considers tags beginning with that prefix and starting with a digit after it. - For
deps.yamlthe prefix is derived automatically from the pinned version's own leading non-digits (e.g. pinningv3.11.0only matchesv*tags). If that still picks junk, the pinned value and the real tags disagree on format — fix thedeps.yamlentry to use the upstream's actual tag string.
The "latest" selection rejects anything that isn't <prefix><digits...>, and
ignores prerelease tags (alpha/beta/rc/dev). If a project's stable releases are
tagged with such words, that's the one case needing a code tweak in
collector/lib/git.ts (versionKey).
The dashboard detects staleness; two complementary mechanisms turn that into PRs.
For anything Renovate can express — git submodules, deps.yaml, and standard
CMake FetchContent version tags — let Renovate own the whole PR lifecycle
(open, rebase, dedupe, respect closed PRs, group, schedule). It already runs on
score's deps.yaml. To cover a consumer that pins one of our libs via
FetchContent, add a customManager in that repo's renovate.json5:
{
customManagers: [
{
// bump: FetchContent_Declare(avendish GIT_REPOSITORY .../avendish GIT_TAG <sha|tag>)
customType: "regex",
managerFilePatterns: ["/\\.cmake$/", "/CMakeLists\\.txt$/"],
matchStrings: [
"GIT_REPOSITORY\\s+\"?https://github.com/(?<depName>[\\w.-]+/[\\w.-]+?)\"?\\s+GIT_TAG\\s+(?<currentDigest>[0-9a-f]{7,40})",
],
currentValueTemplate: "main", // branch the sha tracks
datasourceTemplate: "git-refs", // tracks branch HEAD → digest bumps
},
],
}Use datasourceTemplate: "github-tags" (with currentValue capturing the tag)
for version-tag pins instead of commit pins. This is the lowest-risk path: each
consumer repo opts in and Renovate does the rest.
Renovate can't coordinate "our lib released → bump every consumer in one
sweep", and it doesn't cover the non-standard pins the inventory finds. The
dashboard already knows every pin, so it can. npm run collect writes
data/update-plan.json and the /updates page: for each eligible outdated
dependency, the exact bump (repo, file, current ref → target ref, idempotent
branch). It opens nothing.
Scope and behaviour are set in config/updates.yaml:
enabled: true
ourUpstreams: # only bump consumers of libraries we release
- celtera/avendish
- celtera/libremidi
- ossia/libossia
- ossia/score
sources: # editable single-line pins (submodules/deps.yaml → Renovate)
- cmake-fetchcontent
- cmake-externalproject
- cmake-url
- versions.sh
branchPrefix: "chore/bump-"Review the plan on /updates. When you want the PRs opened for real:
npm run apply-updates # dry run: prints intended edits, opens nothing
GITHUB_TOKEN=<write-PAT> npm run apply-updates -- --applyapply-updates opens one idempotent PR per proposal (stable branch per dep):
re-runs advance the same PR rather than duplicating, an already-open PR's branch
is refreshed in place, and a closed PR for that branch is respected (never
reopened). The edit is a surgical replacement of the pinned ref in the pinned
file; if the current ref isn't found verbatim, the proposal is skipped and
reported rather than guessed at. It needs a token with write access to the
target repos — wire it as a scheduled job only once you're happy with the plan.
Division of labour: Renovate for submodules +
deps.yaml+ standard FetchContent; the planner/apply step for cross-repo sweeps of our own libraries and the pins Renovate doesn't see.
The deploy workflow (.github/workflows/build.yml) needs:
GITHUB_TOKEN— provided automatically by Actions. The workflow grants itcontents: write(to commit the refreshed snapshot),pull-requests: read(PR data), and Pages permissions. This covers all public repos and the dashboard repo itself.DASHBOARD_PAT(optional) — a fine-grained PAT with read access to private repos in other orgs (e.g.sat-mtl/*). Add it under Settings → Secrets and variables → Actions. When present it is used instead ofGITHUB_TOKENfor collection, so private cross-org repos are covered. Without it, those repos are simply skipped (logged, not fatal).
- First-time setup: in the repo settings, set Pages → Source: GitHub
Actions, and ensure
mainis the default branch (scheduled workflows only run from the default branch). - The workflow runs on a 6-hour cron, on push to the default branch, and
on manual dispatch (Actions → collect & deploy → Run workflow). It
re-collects, commits the refreshed
data/snapshot.json(so the snapshot's history is queryable with plaingit log), builds, and deploys to Pages. - To force an immediate refresh, trigger the workflow manually or push any commit.
The config recipes cover new data within existing checks. A genuinely new category (a new collector) is a small amount of code:
- add a
collect/<name>.tsthat returns typed rows; - add the type + a
Snapshotfield incollector/lib/types.ts; - call it from
collector/index.ts(wrap insection(...)for failure isolation); - emit attention items + per-repo rollup in
collector/lib/severity.ts; - add a page under
src/pages/and a nav entry insrc/components/Layout.astro.
The existing collectors are small and parallel-structured; copy the closest one.
See DESIGN.md for the data model and the git-native helper layer
(collector/lib/git.ts: lsRemote, commitGraph, revListCount,
aheadBehind, latestTag, …).
| symptom | cause / fix |
|---|---|
| a section shows a "non-live data" banner | the transport was unavailable during collect; check the section sources: line and the warnings under it in the collect log. PRs need a token; repology/endoflife.date need network egress to those hosts. |
| PRs are from a fixture | no GITHUB_TOKEN, or the workflow lacks pull-requests: read. |
| a private/cross-org repo is missing | add DASHBOARD_PAT (see Secrets). |
score-addon-* (or other family) not appearing |
org discovery needs a token; confirm the orgs: glob and that the repo isn't archived (or set includeArchived: true). |
| a dependency's "latest" looks wrong | see Fixing a wrong "latest". |
| a CMake/version dep is missing entirely | confirm its repo is tracked (so its CMake files are scanned) or listed under cmakeScan/versionFiles; check the collect log for did not match / not readable warnings. |
| the site builds but data is stale | build only renders data/snapshot.json; run collect first (CI does this automatically). |
The fork→upstream registry is not duplicated in this repo: it is read from
ossia/score:3rdparty/deps.yaml, the same file Renovate consumes.