ci: add shields badges for CLI version, build, and pinned toolchain versions - #139
Conversation
Three badges that need no infrastructure of their own: - `npm` reads the published @taskless/cli version from the registry - `build` reads the Validate workflow's status on main - `nightly` is a STATIC badge linking to @taskless/cli-nightly The nightly badge carries no version on purpose. A nightly version is `0.11.0-20260821175223xf9f05c0` — 31 characters that would dominate the row while telling a reader nothing they can act on, and anyone who wants the exact version is one click from npm, where it cannot be stale. `npm/v/@taskless/cli-nightly/latest` was considered and rejected for that. Being static also keeps it out of the publish loop: a badge derived from each nightly would be committed on every nightly, and that commit re-triggers Validate, which re-triggers the nightly. These live in the CLI README rather than the root one because all three describe the published package, and this is the README npm renders on the package page. `nightly-pack.cjs` swaps in a generated README at pack time for the nightly tarball only, so it is unaffected. Refs OSS-43
Shields cannot ask "is there a newer upstream we have not pinned", so the
comparison runs here and the color rule lives in code: `.shields/*.json`
is a shields endpoint payload, committed and served off raw.
- `vale-detect.cjs` gains `--json`, which prints and returns
{ pinned, upstream, ahead } and stops before the checksums download.
The badge needs the comparison the script already holds as data;
scraping its human "pinned: X upstream latest: vY" line would rebuild
that fact with a regex. `--json` is read-only and refuses `--write`.
- `sg-detect.cjs` is new, because ast-grep had NO upstream check at all:
the eight @ast-grep/cli* pins sat at 0.41.0 with nothing watching them
(upstream is 0.45.1). Its upstream is an npm dist-tag rather than a
GitHub release, its pinned version comes from the dependency pins
rather than a manifest, and disagreeing or non-exact pins abort rather
than resolve to a version the badge cannot honestly show.
- `update-badges.cjs` calls both in-process and writes a payload only
when the rendered badge actually changes.
STALENESS IS IN THE MESSAGE, not in a sidecar field. A badge that stopped
updating looks exactly like one whose value has not changed, so the
message is `<version> · <YYYY-MM-DD>`. A `checkedAt` key nobody opens is
a record, not a signal — and it risks shields rejecting an unrecognized
key. The date advances only once it is 7 days old, so the daily schedule
still notices an upstream release within a day while a quiet week costs
at most one commit per badge.
`update-badges.yml` is a SEPARATE workflow, never a job in a publishing
one: it needs `contents: write`, and the release workflows hold
`id-token: write` plus an npm OIDC identity. Separate files make that
separation structural rather than a matter of whoever reviews the next
edit. It also cannot live in release-vale.yml, whose detect phase opens
the very pull request the yellow badge is warning about.
The header records why a per-nightly badge would loop forever
(publish → commit → Validate → publish, which the nightly's per-sha
dedupe cannot break) and why a badge derived from upstream terminates.
Refs OSS-43
There was a problem hiding this comment.
Pull request overview
Adds a badge row to the published @taskless/cli README and introduces a scheduled GitHub Actions workflow + scripts to maintain shields “endpoint” payloads that surface toolchain drift (Vale and ast-grep) with a staleness signal.
Changes:
- Add shields badges (npm version, main build status, static nightly, and two endpoint-driven toolchain badges) to
packages/cli/README.md. - Add
.shields/vale.jsonand.shields/sg.jsonendpoint payloads, plus a scheduledupdate-badges.ymlworkflow that refreshes them and commits only on meaningful change. - Extend/reuse detection logic: add
--jsonmode tovale-detect.cjs, add newsg-detect.cjs, and add/update Node tests for both and for the badge updater.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/README.md | Adds the badge row rendered on the npm package page. |
| .shields/vale.json | Adds the shields endpoint payload for the Vale badge (with date-in-message). |
| .shields/sg.json | Adds the shields endpoint payload for the ast-grep badge (with date-in-message). |
| .github/workflows/update-badges.yml | New scheduled/manual workflow that refreshes endpoint payloads and commits to main when needed. |
| .github/scripts/vale-detect.test.cjs | Updates tests to cover new --json mode behavior and return value contract. |
| .github/scripts/vale-detect.cjs | Adds --json read-only mode and returns structured comparison data. |
| .github/scripts/update-badges.test.cjs | New tests for badge-planning and “rewrite only when needed” behavior. |
| .github/scripts/update-badges.cjs | New script that writes/updates shields payloads with bounded staleness updates. |
| .github/scripts/sg-detect.test.cjs | New tests for ast-grep pin collection + upstream comparison behavior. |
| .github/scripts/sg-detect.cjs | New script to detect upstream ast-grep drift via npm dist-tags and exact pins. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@claude /review |
|
Claude finished @thecodedrift's task in 3m 11s —— View job Review completeReview mode: incremental — read 3 prior review thread(s) before reviewing. All 3 prior threads (from the Copilot review) were unresolved and carried no addressed-marker, so all 3 are still open and were re-raised inline, prefixed
None of the underlying code changed since those were raised, so nothing new to add on them beyond confirming they're still present as described. Two
Overall assessmentThe core logic ( |
- Percent-encode the scope separator in the npm registry URL, as vale-gate.cjs does. The registry does serve the unescaped path today (verified against the real endpoint), but `/@scope%2Fname` is the documented form and relying on an unpromised redirect is a strange saving. - Treat a committed date in the FUTURE as stale. A negative age passed `age < STALE_AFTER_DAYS`, so a skewed clock or a hand edit would have produced a date no later run could correct — the one way this rule could stick. - Run the two detects with Promise.all. They are independent lookups against different hosts; awaiting them inside the array literal made every scheduled run wait out both round trips end to end. - Retry the badge push (bounded at 3) by rebasing onto the new tip. Any PR merging between checkout and push makes main a non-fast-forward, which would fail the job and leave the badge stale until tomorrow. `.shields/` is touched by nothing else, so the rebase cannot conflict with an ordinary merge. - Compute the test helper's `ahead` with the production comparator. `pinned !== upstream` happened to be right for every current case but mismodels the one that matters — upstream BEHIND the pin is not ahead and must stay green — so a later test could have asserted against a comparison the real scripts cannot produce. Added that case, plus the future-date regression. Refs OSS-43
All five findings are addressed in 3d5e382 and each inline thread carries a reply: the scoped registry path is now percent-encoded, a future date is treated as stale, the two detects run under Three things this PR still cannot demonstrate, stated rather than papered over: — AI Coding Agent |
Thanks — the summary matches what the PR does. All three inline findings (scoped registry URL, the future-date case in — AI Coding Agent |
Adds the README badge row from #138, in two commits so the zero-infrastructure half can be reverted without losing it.
The badges
npm@taskless/cliversionbuildValidatestatus onmainnightly@taskless/cli-nightlyvale@taskless/vale-*version, yellow when upstream is ahead.shields/vale.jsonsg@ast-grep/cli*version, yellow when upstream is ahead.shields/sg.jsonThey live in
packages/cli/README.md, not the root one: all five describe the published package, and this is the README npm renders on the package page.nightly-pack.cjssubstitutes a fully generated README at pack time for the nightly tarball only, so it is unaffected.Both dynamic badges are yellow right now, against real drift: Vale pinned
3.17.1vs upstream3.18.0, ast-grep pinned0.41.0vs upstream0.45.1.Why the nightly badge is static
It carries no version on purpose. A nightly version is
0.11.0-20260821175223xf9f05c0— 31 characters that would dominate the row while telling a reader nothing actionable, and anyone who needs the exact version is one click from npm, where it cannot be stale.npm/v/@taskless/cli-nightly/latestwas considered and rejected for that.Being static also keeps it out of the publish loop.
validate.ymlhas nopathsfilter and the nightly triggers onValidatecompleting, so a badge derived from each nightly would go publish → commit → Validate → publish forever; the nightly's gate 2 cannot break it, because it dedupes per-sha and every badge commit is a new sha. The committed badges depend on upstream moving instead, so the chain terminates on the next run that finds nothing new. That reasoning is recorded in theupdate-badges.ymlheader so nobody reintroduces it.Why
update-badges.ymlis its own workflowIt needs
contents: write.release-cli.yml,release-cli-nightly.yml, andrelease-vale.ymleach holdid-token: writeand an npm OIDC publishing identity. Keeping them in separate files makes the separation structural: there is no arrangement of jobs here for a later edit to get wrong, because the credentialed workflows are not in this file.It also cannot be folded into
release-vale.ymlspecifically. That workflow's detect phase opens the manifest pull request; the badge has to be yellow while that PR is open and green once it merges, so the payload cannot live in that PR's own diff.Trigger is
schedule(daily) plusworkflow_dispatch— it answers "what is upstream doing", which is unrelated to our release events.Staleness
.shields/*.jsonis exactly the shields endpoint schema and carries no extra fields. The last-written date is in the message:vale | 3.17.1 · 2026-08-22. AcheckedAtkey would be a record nobody opens rather than a signal, and an unrecognized key risks shields rejecting the payload. A reader who sees a four-month-old date knows the job died rather than that upstream has been quiet.The date advances only once it is 7 days old, so the daily schedule still notices an upstream release within a day, while a quiet week costs at most one commit per badge rather than seven — and a commit costs a
Validaterun and possibly a nightly publish.Consuming the existing detection
vale-detect.cjswas reused, not reimplemented. It gained a--jsonmode that prints and returns{ pinned, upstream, ahead }and stops before the checksums download (the badge proposes no manifest, so there is nothing to verify).update-badges.cjscalls it in-process and reads the return value, so nothing parses the human-readable line.--jsonis read-only and refuses to be combined with--write.sg-detect.cjsis new, because ast-grep had no upstream check at all. It diverges from the Vale script where the shape differs: upstream is an npm dist-tag rather than a GitHub release, the pinned version comes from the eight@ast-grep/cli*dependency pins rather than a manifest, and disagreeing or non-exact pins abort rather than resolving to a version the badge could not honestly show. One of its tests reads the repository's realpackages/cli/package.json, so loosening a pin to a range is caught.Verification
pnpm lint,pnpm typecheck— cleanpnpm test— 647 tests, 52 files, all passingnode --test .github/scripts/*.test.cjs— 198 tests passing (23 new)pnpm openspec validate --all --strict— 24 passed, 0 failed;openspec-visibility.cjscleanupdate-badges.ymlparses;permissions: {}at top level,contents: writeon the one jobimage/svg+xml. The endpoint badges were rendered against this branch's raw ref:vale: 3.17.1 · 2026-08-22andsg: 0.41.0 · 2026-08-22, both filled#d8b800(yellow)No OpenSpec change: nothing under
openspec/specs/covers badges or the README, and the infrastructure spec's workflow requirements are untouched.Fixes #138