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
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.
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
version@taskless/cliversionbuildValidatestatus onmainvale | <version>@taskless/vale-*versionsg | <version>@ast-grep/cli-*versionnightly | npmThree of these need no infrastructure
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/latestwas 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.
valeandsgneed a shields endpointShields cannot compute "is there a newer upstream we have not pinned." The mechanism for any badge with a color rule is:
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/*.jsoncommitted to the repo, served fromraw.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.ymlA 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: writeis not the workflow that holdsid-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.cjsrun without--writealready prints exactly the comparison the badge needs:So the workflow runs the detect scripts read-only, writes
.shields/*.json, and commits only when a value actually changed.Trigger on
scheduleplusworkflow_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.jsonpins3.17.1-20260810052605. Thevalebadge 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.cjsopens 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 reachmainindependently of the PR it is warning about. That is another reason for a separate scheduled workflow rather than folding this intorelease-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.ymltriggers onpush: branches: [main]with nopathsfilter, and since #132 the nightly triggers onValidatecompleting. A workflow that commits a badge derived from each nightly would therefore loop: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
valeandsg, which change when upstream moves, not when we push. A badge commit still triggers oneValidaterun and one extra nightly publish, but the next badge run finds nothing changed, writes nothing, and the chain terminates. Nopaths-ignoreneeded, 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 at0.41.0inpackages/cli/package.jsonand nothing compares them against upstream — unlike Vale, which hasvale-detect.cjs. Thesgbadge therefore surfaces a gap rather than merely reporting a value, and needs a detect script written from scratch. Scope accordingly;sgis materially more work thanvale.Acceptance
versionandbuildrender from shields built-ins;nightlyrenders as a static badge linking to the npm package, carrying no version stringvaleis yellow against the current 3.18.0-vs-3.17.1 drift, and goes green when the pin is bumped — observed, not reasoned aboutsgdoes the same, with whatever detect script that requiresupdate-badges.ymlholdscontents: write; no workflow holds it alongsideid-token: writeThat 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.