Skip to content

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

Description

@thecodedrift

Add README badges that report the versions of the technology the CLI is built on, so drift is visible at a glance rather than discovered when something breaks.

Wanted

Badge Shows Color
version published @taskless/cli version default
build Validate status on main default
vale | <version> the pinned @taskless/vale-* version yellow when a newer upstream Vale exists that we have not pinned
sg | <version> the pinned @ast-grep/cli-* version yellow when a newer upstream ast-grep exists that we have not pinned
nightly | npm static text linking to the npm package always blue

Three of these need no infrastructure

img.shields.io/npm/v/@taskless/cli
img.shields.io/github/actions/workflow/status/taskless/cli/validate.yml?branch=main
img.shields.io/badge/nightly-npm-blue

The nightly badge is deliberately STATIC — fixed text, fixed color, wrapped in a link to the npm package. It is a signpost, not a readout.

It carries no version on purpose. A nightly version is 0.11.0-20260821175223xf9f05c0 — 31 characters that would dominate a badge row and force every other badge to the side, while telling a reader nothing they can act on. Anyone who needs the exact version is one click from npm, where it is accurate by definition and never stale. img.shields.io/npm/v/@taskless/cli-nightly/latest was considered and rejected for exactly that: it is dynamic machinery bought to render a string nobody reads.

Because it is static, it needs no endpoint, no committed JSON, no workflow, and no permission.

Land these three first. They are zero-risk and settle nothing.

vale and sg need a shields endpoint

Shields cannot compute "is there a newer upstream we have not pinned." The mechanism for any badge with a color rule is:

img.shields.io/endpoint?url=https://raw.githubusercontent.com/taskless/cli/main/.shields/vale.json

serving

{ "schemaVersion": 1, "label": "vale", "message": "3.17.1", "color": "green" }

Our side decides the color, so the rule lives in code we control.

Storage: .shields/*.json committed to the repo, served from raw.githubusercontent.com. No new publishing surface, no state outside the repo. Raw is cached, so a badge can lag by minutes — fine for a weekly upstream check.

Implementation: a dedicated update-badges.yml

A separate workflow, not a job bolted onto a publishing one. This satisfies the permission constraint structurally rather than by care: the workflow that needs contents: write is not the workflow that holds id-token: write, so there is no arrangement of jobs in one file for a later edit to get wrong.

It should reuse the existing detection, not reimplement it. .github/scripts/vale-detect.cjs run without --write already prints exactly the comparison the badge needs:

pinned: 3.17.1   upstream latest: v3.18.0
Upstream 3.18.0 is ahead of 3.17.1.

So the workflow runs the detect scripts read-only, writes .shields/*.json, and commits only when a value actually changed.

Trigger on schedule plus workflow_dispatch. It is answering "what is upstream doing," which has nothing to do with our own release events.

There is a live case to build against right now: upstream Vale is 3.18.0 and packages/cli/package.json pins 3.17.1-20260810052605. The vale badge should be yellow the moment it ships. Do not wait for a synthetic test.

Why the badge must not be written inside the manifest PR

vale-detect.cjs opens a pull request when upstream is ahead. The badge has to be yellow while that PR is open and green once it merges — so the JSON cannot live in the manifest PR's own diff, or it would go yellow exactly when it should go green. It has to reach main independently of the PR it is warning about. That is another reason for a separate scheduled workflow rather than folding this into release-vale.yml.

The feedback loop — why the nightly badge is not committed

Worth recording, because it is why the nightly badge is a plain built-in.

validate.yml triggers on push: branches: [main] with no paths filter, and since #132 the nightly triggers on Validate completing. A workflow that commits a badge derived from each nightly would therefore loop:

commit .shields/nightly.json → Validate → nightly workflow_run
  → gate 1 true (changesets pending)
  → gate 2 sees a NEW sha, so it does not dedupe
    → publishes a nightly → writes the badge → commits → …

Gate 2 cannot stop this: it dedupes per-sha, and every badge commit is a new sha.

A static nightly badge breaks the loop by construction. The only committed values are vale and sg, which change when upstream moves, not when we push. A badge commit still triggers one Validate run and one extra nightly publish, but the next badge run finds nothing changed, writes nothing, and the chain terminates. No paths-ignore needed, so the repo's "no filters, let the workflow decide inside it" rule (adopted after #103) stays intact.

ast-grep has no upstream check at all

The @ast-grep/cli-* pins sit at 0.41.0 in packages/cli/package.json and nothing compares them against upstream — unlike Vale, which has vale-detect.cjs. The sg badge therefore surfaces a gap rather than merely reporting a value, and needs a detect script written from scratch. Scope accordingly; sg is materially more work than vale.

Acceptance

  • version and build render from shields built-ins; nightly renders as a static badge linking to the npm package, carrying no version string
  • vale is yellow against the current 3.18.0-vs-3.17.1 drift, and goes green when the pin is bumped — observed, not reasoned about
  • sg does the same, with whatever detect script that requires
  • update-badges.yml holds contents: write; no workflow holds it alongside id-token: write
  • A badge commit does not start an unbounded publish chain — demonstrate it, do not assert it
  • A stale badge is distinguishable from a correct one: record when each JSON was last written, so "nothing changed upstream" and "the job stopped running" do not look identical

That last one matters more than it reads. A badge that is confidently wrong is worse than no badge, and an absent signal reading as a passing one is the failure this repo keeps hitting.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions