Skip to content

ci: add shields badges for CLI version, build, and pinned toolchain versions - #139

Merged
thecodedrift merged 3 commits into
mainfrom
feat/shields-badges
Aug 22, 2026
Merged

ci: add shields badges for CLI version, build, and pinned toolchain versions#139
thecodedrift merged 3 commits into
mainfrom
feat/shields-badges

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

Adds the README badge row from #138, in two commits so the zero-infrastructure half can be reverted without losing it.

The badges

Badge What it reads Infrastructure
npm published @taskless/cli version shields built-in
build Validate status on main shields built-in
nightly static text, links to @taskless/cli-nightly none
vale pinned @taskless/vale-* version, yellow when upstream is ahead .shields/vale.json
sg pinned @ast-grep/cli* version, yellow when upstream is ahead .shields/sg.json

They 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.cjs substitutes 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.1 vs upstream 3.18.0, ast-grep pinned 0.41.0 vs upstream 0.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/latest was considered and rejected for that.

Being static also keeps it out of the publish loop. validate.yml has no paths filter and the nightly triggers on Validate completing, 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 the update-badges.yml header so nobody reintroduces it.

Why update-badges.yml is its own workflow

It needs contents: write. release-cli.yml, release-cli-nightly.yml, and release-vale.yml each hold id-token: write and 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.yml specifically. 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) plus workflow_dispatch — it answers "what is upstream doing", which is unrelated to our release events.

Staleness

.shields/*.json is 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. A checkedAt key 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 Validate run and possibly a nightly publish.

Consuming the existing detection

vale-detect.cjs was reused, not reimplemented. It gained a --json mode 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.cjs calls it in-process and reads the return value, so nothing parses the human-readable line. --json is read-only and refuses to be combined with --write.

sg-detect.cjs is 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 real packages/cli/package.json, so loosening a pin to a range is caught.

Verification

  • pnpm lint, pnpm typecheck — clean
  • pnpm test — 647 tests, 52 files, all passing
  • node --test .github/scripts/*.test.cjs — 198 tests passing (23 new)
  • pnpm openspec validate --all --strict — 24 passed, 0 failed; openspec-visibility.cjs clean
  • update-badges.yml parses; permissions: {} at top level, contents: write on the one job
  • Every badge URL returns 200 with image/svg+xml. The endpoint badges were rendered against this branch's raw ref: vale: 3.17.1 · 2026-08-22 and sg: 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

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
Copilot AI lite review requested due to automatic review settings August 22, 2026 16:32
@thecodedrift thecodedrift added the skip-changeset PR intentionally ships no release note (bypasses the changeset requirement) label Aug 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json and .shields/sg.json endpoint payloads, plus a scheduled update-badges.yml workflow that refreshes them and commits only on meaningful change.
  • Extend/reuse detection logic: add --json mode to vale-detect.cjs, add new sg-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.

Comment thread .github/scripts/sg-detect.cjs Outdated
Comment thread .github/scripts/update-badges.cjs Outdated
Comment thread .github/scripts/update-badges.test.cjs
@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Aug 22, 2026

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 3m 11s —— View job


Review complete

Review 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 [Unchanged since last review]:

  • .github/scripts/sg-detect.cjs:161 — unescaped / in the npm registry URL for the scoped @ast-grep/cli package.
  • .github/scripts/update-badges.cjs:117keepDate treats a future/negative-age date as perpetually fresh.
  • .github/scripts/update-badges.test.cjs:35comparisonOf() test helper's ahead semantics don't match production's "upstream > pinned".

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 [New] findings, both minor (posted inline):

  • .github/scripts/update-badges.cjs:155-156 — the Vale and ast-grep detect calls are awaited sequentially inside an object literal rather than run concurrently with Promise.all; harmless but a free win on wall-clock time for the scheduled job.
  • .github/workflows/update-badges.yml:108-114 — the commit step pushes straight to main with no fetch/rebase before git push, so a normal PR merging in the checkout→push window causes a non-fast-forward failure instead of self-healing.

Overall assessment

The core logic (vale-detect.cjs --json mode, the new sg-detect.cjs, and update-badges.cjs's staleness/no-op-commit rules) is well covered by the new tests, and the design rationale in the code comments and PR description is thorough and consistent with what's implemented — --json genuinely stops before the checksums download, sg-detect.cjs genuinely never writes, and the "only rewrite when the rendered badge changes" invariant is exercised directly. packages/cli/README.md and the .shields/*.json payloads match the described schema with no extra fields. I didn't find anything new of correctness-blocking severity beyond the two minor items above and the three carried-forward threads.

Comment thread .github/scripts/sg-detect.cjs Outdated
Comment thread .github/scripts/update-badges.cjs Outdated
Comment thread .github/scripts/update-badges.test.cjs
Comment thread .github/scripts/update-badges.cjs Outdated
Comment thread .github/workflows/update-badges.yml Outdated
- 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
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task in 3m 11s —— Review complete. Review mode: incremental…"
#139 (comment)

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 Promise.all, the badge push retries by rebasing onto the new tip (bounded at three attempts), and the test helper computes ahead with the production comparator plus two new cases.

Three things this PR still cannot demonstrate, stated rather than papered over: update-badges.yml has never executed, because a scheduled workflow only fires from the default branch; the loop-termination argument is reasoned from the trigger graph plus the write-only-on-change rule rather than observed; and the README's endpoint badges point at main, so they render shields' "invalid" state until this merges. The endpoint payloads themselves were rendered from this branch's raw ref and came back yellow as intended.

— AI Coding Agent

@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @copilot-pull-request-reviewer — "## Pull request overview — Adds a badge row to the published @taskless/cli README and introduces a scheduled GitHub Actions workflow…"

Thanks — the summary matches what the PR does. All three inline findings (scoped registry URL, the future-date case in keepDate, and the comparisonOf() helper's ahead semantics) are fixed in 3d5e382 and answered on their threads.

— AI Coding Agent

@thecodedrift
thecodedrift merged commit 5add21e into main Aug 22, 2026
10 of 11 checks passed
@thecodedrift
thecodedrift deleted the feat/shields-badges branch August 22, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR intentionally ships no release note (bypasses the changeset requirement)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add shields badges for CLI version, build status, and pinned Vale/ast-grep versions

2 participants