diff --git a/.github/scripts/sg-detect.cjs b/.github/scripts/sg-detect.cjs index 91f01d43..bfa4debb 100644 --- a/.github/scripts/sg-detect.cjs +++ b/.github/scripts/sg-detect.cjs @@ -11,8 +11,7 @@ * WHY THIS EXISTS AT ALL. Vale has had `vale-detect.cjs` since the platform * packages were built, so a Vale release is noticed by a machine. ast-grep had * nothing: the eight `@ast-grep/cli*` pins sat wherever someone last put them - * and nobody compared them to upstream. The `sg` badge therefore reports a - * number that had no watcher, which is the whole reason it is worth rendering. + * and nobody compared them to upstream. * * WHERE IT DIVERGES FROM vale-detect.cjs, and why the divergence is not * carelessness: @@ -32,13 +31,13 @@ * ast-grep publishes something unexpected — an error message that lies * about which dependency is in trouble. Fifteen lines of comparator is a * better trade than a wrong error and a coupling that makes a Vale-specific - * edit able to break this badge. + * edit able to break the ast-grep comparison. * * Usage: * node .github/scripts/sg-detect.cjs [--json] [--write] [--notes-out ] * * --json print `{ pinned, upstream, ahead }` and nothing else, writing - * nothing. This is what update-badges.cjs calls. + * nothing. For a caller that wants the comparison as data. * * --write rewrite every `@ast-grep/cli*` pin in packages/cli/package.json to * the upstream version, and `AST_GREP_VERSION` in @@ -148,8 +147,8 @@ function setOutput(key, value) { * Parse `major.minor.patch`, rejecting anything else — including a range * (`^0.41.0`) and a prerelease. Both are real states this can meet and neither * has a defensible answer: a range means the pin is not a pin, and a - * prerelease on `latest` means upstream is doing something the badge should not - * quietly average over. + * prerelease on `latest` means upstream is doing something the comparison should + * not quietly average over. */ function parseVersion(text, what) { const match = VERSION_PATTERN.exec(String(text ?? "").trim()); @@ -177,11 +176,11 @@ function isAhead(pinned, upstream) { * The one version every `@ast-grep/cli*` dependency is pinned to. * * Disagreement among them is an error rather than a "pick the highest", - * because there is no version the badge could honestly show for a repository - * that pins two. That state is also a real bug — the platform packages are + * because there is no single version to compare for a repository that pins + * two. That state is also a real bug — the platform packages are * selected by optional dependency, so a straggler at an older version is a - * different ast-grep on one platform than on the others — and a badge that - * smoothed it over would hide exactly the drift it was added to expose. + * different ast-grep on one platform than on the others — and a comparison + * that smoothed it over would hide exactly the drift it exists to expose. */ function collectPins(packageJson) { const pins = new Map(); @@ -268,7 +267,7 @@ async function main({ const json = argv.includes("--json"); const write = argv.includes("--write"); const notesOut = readNotesOut(argv); - // --json is what the badge run calls, and a badge run proposes nothing. + // --json reports the comparison, and a comparison proposes nothing. if (json && (notesOut || write)) { throw new Error( "--json prints the comparison and nothing else; it cannot be combined with --notes-out or --write" @@ -357,8 +356,8 @@ async function main({ return comparison; } -// main() both prints and RETURNS the comparison, so update-badges.cjs can call -// it in-process and read the answer as data. Nothing should ever parse the +// main() both prints and RETURNS the comparison, so a caller can run it +// in-process and read the answer as data. Nothing should ever parse the // human line above to recover a version that this return value already holds. module.exports = { collectPinnedVersion, collectPins, isAhead, main }; diff --git a/.github/scripts/sg-detect.test.cjs b/.github/scripts/sg-detect.test.cjs index 07de2590..a9f55861 100644 --- a/.github/scripts/sg-detect.test.cjs +++ b/.github/scripts/sg-detect.test.cjs @@ -131,7 +131,7 @@ const pinnedAt = (version) => ({ test("sg-detect: the repository's own pins are exact and agree", () => { // Not a fixture. If someone changes a pin to `^0.41.0` or bumps one platform - // without the others, the badge has no honest value to show and this is + // without the others, there is no honest version to compare and this is // where that is caught. assert.match(collectPinnedVersion(CLI_PACKAGE_JSON), /^\d+\.\d+\.\d+$/); }); @@ -219,7 +219,7 @@ test("sg-detect: no @ast-grep dependency at all aborts", () => { }); test("sg-detect: an upstream prerelease on latest aborts loudly", () => { - // Better a failed run than a badge silently comparing 0.41.0 against + // Better a failed run than silently comparing 0.41.0 against // something it cannot order. assert.throws( () => isAhead("0.41.0", "0.46.0-alpha.1"), diff --git a/.github/scripts/update-badges.cjs b/.github/scripts/update-badges.cjs deleted file mode 100644 index cc5d4c20..00000000 --- a/.github/scripts/update-badges.cjs +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env node -// SPDX-License-Identifier: MIT -"use strict"; - -/** - * Write the shields endpoint payloads for the toolchain badges. - * - * `.shields/vale.json` and `.shields/sg.json` are served straight off - * raw.githubusercontent.com and rendered by - * `img.shields.io/endpoint?url=…`. Shields cannot ask "is there a newer - * upstream we have not pinned", so the color rule lives here, in code that is - * reviewed like everything else. - * - * THE MESSAGE CARRIES A DATE, AND THAT IS THE STALENESS SIGNAL. - * - * A badge that has stopped being updated looks exactly like a badge whose - * value has not changed — both render `vale | 3.17.1` forever. That is the - * failure this repository keeps hitting: an absent signal reading as a passing - * one. The fix has to be visible where the badge is read, because a - * `checkedAt` field buried in a JSON file nobody opens is a record, not a - * signal. So the message is ` · `, and a reader who sees - * a date from four months ago knows the job died rather than that upstream has - * been quiet. - * - * The date is deliberately NOT refreshed on every run, or the badge would be - * rewritten daily and every rewrite is a commit to `main` (see the loop note in - * update-badges.yml). It advances only once it is STALE_AFTER_DAYS old, so a - * daily schedule produces at most one commit a week per badge while still - * detecting an upstream release within a day of it happening. The date means - * "checked no earlier than this", which is the guarantee a reader needs, and - * the window of imprecision is bounded by the constant below. - * - * The payload is exactly the shields endpoint schema and carries no extra - * fields: the record and the signal are the same string, so they cannot drift - * apart, and there is no chance of shields rejecting an unrecognized key. - * - * Usage: - * node .github/scripts/update-badges.cjs [--write] - * - * --write rewrite the payloads whose rendered badge changed. Without it the - * script reports what it would do and touches nothing. - * - * Outputs (appended to $GITHUB_OUTPUT when set): - * changed "true" when at least one payload was (or would be) rewritten - */ - -const { - appendFileSync, - mkdirSync, - readFileSync, - writeFileSync, -} = require("node:fs"); -const { join } = require("node:path"); - -const { main: valeDetect } = require("./vale-detect.cjs"); -const { main: sgDetect } = require("./sg-detect.cjs"); - -const SHIELDS_DIRECTORY = join(__dirname, "..", "..", ".shields"); - -/** How old the recorded date may get before a run refreshes it. */ -const STALE_AFTER_DAYS = 7; - -const MESSAGE_SEPARATOR = " · "; - -const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; - -function setOutput(key, value) { - const file = process.env.GITHUB_OUTPUT; - if (file) { - appendFileSync(file, `${key}=${value}\n`); - } -} - -/** `2026-08-21`, in UTC, so the date does not depend on where a runner is. */ -function formatDate(date) { - return date.toISOString().slice(0, 10); -} - -/** Whole days between two `YYYY-MM-DD` strings, both read as UTC midnight. */ -function daysBetween(earlier, later) { - return Math.round( - (Date.parse(`${later}T00:00:00Z`) - Date.parse(`${earlier}T00:00:00Z`)) / - 86_400_000 - ); -} - -/** - * Split `3.17.1 · 2026-08-21` back into its parts. Anything that does not have - * that shape — a hand-edited file, a payload written before this format, a - * truncated write — yields no date, which makes the next run rewrite it. That - * is the right bias: an unreadable badge should be replaced, not preserved. - */ -function parseMessage(message) { - const [version, date] = String(message ?? "").split(MESSAGE_SEPARATOR); - return { - version, - date: DATE_PATTERN.test(date ?? "") ? date : undefined, - }; -} - -/** - * The badge a comparison should render, given what is already committed. - * - * Pure, and the only place the color rule and the date rule live. Keeping the - * committed date when nothing else moved is what stops a daily schedule from - * committing daily; keeping it only while it is fresh is what stops the badge - * from claiming a check that never happened. - */ -function planBadge({ label, comparison, today, previous }) { - const color = comparison.ahead ? "yellow" : "green"; - const before = parseMessage(previous?.message); - - // A committed date in the FUTURE — a skewed runner clock, a hand edit — has - // a negative age, which `age < STALE_AFTER_DAYS` would read as freshly - // written and keep forever. That is the one way this rule could produce a - // badge that never self-corrects, so a negative age counts as stale. - const age = daysBetween(before.date ?? today, today); - const keepDate = - before.date !== undefined && - before.version === comparison.pinned && - previous?.color === color && - age >= 0 && - age < STALE_AFTER_DAYS; - - return { - schemaVersion: 1, - label, - message: `${comparison.pinned}${MESSAGE_SEPARATOR}${keepDate ? before.date : today}`, - color, - }; -} - -/** The committed payload, or undefined when there is not a readable one. */ -function readBadge(path) { - try { - return JSON.parse(readFileSync(path, "utf8")); - } catch { - return undefined; - } -} - -function serialize(badge) { - return `${JSON.stringify(badge, null, 2)}\n`; -} - -async function main({ - argv = process.argv.slice(2), - directory = SHIELDS_DIRECTORY, - now = new Date(), - detect = { - // --json keeps each detect script's human narration off stdout; the answer - // comes back as the return value rather than out of the printed line, so - // nothing here parses anything. - vale: () => valeDetect({ argv: ["--json"] }), - sg: () => sgDetect({ argv: ["--json"] }), - }, -} = {}) { - const write = argv.includes("--write"); - const today = formatDate(now); - - // Concurrent, because the two lookups are independent: one hits the GitHub - // releases API and the other the npm registry, and neither informs the other. - // Awaiting them in sequence inside the array literal made a scheduled run - // wait out both round trips end to end for no reason. - const [vale, sg] = await Promise.all([detect.vale(), detect.sg()]); - const badges = [ - { label: "vale", file: "vale.json", comparison: vale }, - { label: "sg", file: "sg.json", comparison: sg }, - ]; - - let changed = false; - for (const { label, file, comparison } of badges) { - const path = join(directory, file); - const previous = readBadge(path); - const badge = planBadge({ label, comparison, today, previous }); - const next = serialize(badge); - - if (previous !== undefined && serialize(previous) === next) { - console.log(`${file}: unchanged (${badge.message}, ${badge.color})`); - continue; - } - - changed = true; - console.log(`${file}: ${badge.message}, ${badge.color}`); - if (write) { - mkdirSync(directory, { recursive: true }); - writeFileSync(path, next); - } - } - - if (!write) { - console.log("\nPass --write to update the payloads."); - } - setOutput("changed", String(changed)); - return { changed }; -} - -module.exports = { main, planBadge }; - -if (require.main === module) { - main().catch((error) => { - console.error(`\nupdate-badges failed: ${error.message}`); - process.exitCode = 1; - }); -} diff --git a/.github/scripts/update-badges.test.cjs b/.github/scripts/update-badges.test.cjs deleted file mode 100644 index 3071d669..00000000 --- a/.github/scripts/update-badges.test.cjs +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-License-Identifier: MIT -"use strict"; - -/** - * Tests for update-badges.cjs. - * - * Both detect scripts are stubbed and the payload directory is a temp one, so - * nothing here touches the network or the committed `.shields/` files. - * - * The rules under test are the two that are easy to get backwards: a run that - * finds nothing new must not rewrite anything (every rewrite becomes a commit - * to main, and a commit to main costs a Validate run and a nightly publish), - * and the date must not be allowed to sit still forever, or a dead job renders - * as a healthy one. - */ - -const test = require("node:test"); -const assert = require("node:assert/strict"); -const { - existsSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} = require("node:fs"); -const { tmpdir } = require("node:os"); -const { join } = require("node:path"); - -const { main, planBadge } = require("./update-badges.cjs"); - -const { isAhead } = require("./sg-detect.cjs"); - -/** - * A stubbed detect result, with `ahead` computed the way production computes - * it. `pinned !== upstream` was close enough for every case here, but it would - * have mismodelled the one that matters — upstream BEHIND the pin, which is - * `ahead: false` and must stay green — so a later test could have asserted - * against a comparison the real scripts can never produce. - */ -const comparisonOf = (pinned, upstream) => ({ - pinned, - upstream, - ahead: isAhead(pinned, upstream), -}); - -/** Run main() against a temp directory seeded with `files`. */ -async function run({ files = {}, vale, sg, now, argv = ["--write"] }) { - const directory = mkdtempSync(join(tmpdir(), "update-badges-test-")); - const silenced = console.log; - console.log = () => {}; - try { - for (const [name, contents] of Object.entries(files)) { - writeFileSync(join(directory, name), contents); - } - const result = await main({ - argv, - directory, - now: new Date(now), - detect: { vale: async () => vale, sg: async () => sg }, - }); - const read = (name) => { - const path = join(directory, name); - return existsSync(path) - ? JSON.parse(readFileSync(path, "utf8")) - : undefined; - }; - return { ...result, vale: read("vale.json"), sg: read("sg.json") }; - } finally { - console.log = silenced; - rmSync(directory, { recursive: true, force: true }); - } -} - -const badge = (message, color) => - `${JSON.stringify({ schemaVersion: 1, label: "vale", message, color }, null, 2)}\n`; - -test("badges: an empty .shields writes both payloads", async () => { - const result = await run({ - vale: comparisonOf("3.17.1", "3.18.0"), - sg: comparisonOf("0.41.0", "0.41.0"), - now: "2026-08-21T06:17:00Z", - }); - - assert.equal(result.changed, true); - // The live case this shipped against: Vale pinned at 3.17.1 with 3.18.0 - // upstream is yellow, ast-grep level with upstream is green. - assert.deepEqual(result.vale, { - schemaVersion: 1, - label: "vale", - message: "3.17.1 · 2026-08-21", - color: "yellow", - }); - assert.deepEqual(result.sg, { - schemaVersion: 1, - label: "sg", - message: "0.41.0 · 2026-08-21", - color: "green", - }); -}); - -test("badges: nothing new upstream writes nothing", async () => { - const result = await run({ - files: { - "vale.json": badge("3.17.1 · 2026-08-19", "yellow"), - "sg.json": badge("0.41.0 · 2026-08-19", "green").replace( - '"vale"', - '"sg"' - ), - }, - vale: comparisonOf("3.17.1", "3.18.0"), - sg: comparisonOf("0.41.0", "0.41.0"), - now: "2026-08-21T06:17:00Z", - }); - - assert.equal(result.changed, false); - // Two days on, the recorded date is still the truth it was written with. - assert.equal(result.vale.message, "3.17.1 · 2026-08-19"); -}); - -test("badges: a date older than the staleness horizon is refreshed", async () => { - const result = await run({ - files: { "vale.json": badge("3.17.1 · 2026-08-01", "yellow") }, - vale: comparisonOf("3.17.1", "3.18.0"), - sg: comparisonOf("0.41.0", "0.41.0"), - now: "2026-08-21T06:17:00Z", - }); - - assert.equal(result.changed, true); - assert.equal(result.vale.message, "3.17.1 · 2026-08-21"); -}); - -test("badges: bumping the pin turns the badge green and redates it", async () => { - const result = await run({ - files: { "vale.json": badge("3.17.1 · 2026-08-20", "yellow") }, - vale: comparisonOf("3.18.0", "3.18.0"), - sg: comparisonOf("0.41.0", "0.41.0"), - now: "2026-08-21T06:17:00Z", - }); - - assert.deepEqual(result.vale, { - schemaVersion: 1, - label: "vale", - message: "3.18.0 · 2026-08-21", - color: "green", - }); -}); - -test("badges: without --write nothing is written but the answer is reported", async () => { - const result = await run({ - vale: comparisonOf("3.17.1", "3.18.0"), - sg: comparisonOf("0.41.0", "0.45.1"), - now: "2026-08-21T06:17:00Z", - argv: [], - }); - - assert.equal(result.changed, true); - assert.equal(result.vale, undefined); - assert.equal(result.sg, undefined); -}); - -test("badges: an unreadable payload is replaced rather than preserved", async () => { - const result = await run({ - files: { "vale.json": "{ not json" }, - vale: comparisonOf("3.17.1", "3.18.0"), - sg: comparisonOf("0.41.0", "0.41.0"), - now: "2026-08-21T06:17:00Z", - }); - - assert.equal(result.vale.message, "3.17.1 · 2026-08-21"); -}); - -test("badges: the payload carries only the shields endpoint schema", () => { - // No `checkedAt` sidecar. Shields validates the payload it fetches, and a - // record nobody reads is not a staleness signal anyway — the date is in the - // message for exactly that reason. - const planned = planBadge({ - label: "vale", - comparison: comparisonOf("3.17.1", "3.18.0"), - today: "2026-08-21", - previous: undefined, - }); - - assert.deepEqual(Object.keys(planned), [ - "schemaVersion", - "label", - "message", - "color", - ]); -}); - -test("badges: an upstream version behind the pin stays green", () => { - // The case the old test helper could not express: `pinned !== upstream` is - // true here, but nothing is ahead of us and the badge must not go yellow. - const planned = planBadge({ - label: "sg", - comparison: comparisonOf("0.45.1", "0.41.0"), - today: "2026-08-22", - previous: undefined, - }); - - assert.equal(planned.color, "green"); -}); - -test("badges: a date in the future is treated as stale, not as fresh", () => { - // A skewed clock or a hand edit must not produce a date that outlives every - // later run. A negative age is stale. - const planned = planBadge({ - label: "vale", - comparison: comparisonOf("3.17.1", "3.18.0"), - today: "2026-08-22", - previous: { - schemaVersion: 1, - label: "vale", - message: "3.17.1 · 2027-01-01", - color: "yellow", - }, - }); - - assert.equal(planned.message, "3.17.1 · 2026-08-22"); -}); - -test("badges: a payload written the same day is left alone", () => { - const previous = { - schemaVersion: 1, - label: "sg", - message: "0.41.0 · 2026-08-21", - color: "green", - }; - - assert.deepEqual( - planBadge({ - label: "sg", - comparison: comparisonOf("0.41.0", "0.41.0"), - today: "2026-08-21", - previous, - }), - previous - ); -}); diff --git a/.github/scripts/vale-detect.cjs b/.github/scripts/vale-detect.cjs index 72f76f0d..ffeb3d33 100644 --- a/.github/scripts/vale-detect.cjs +++ b/.github/scripts/vale-detect.cjs @@ -29,12 +29,12 @@ * "what would this do?" run wants. * * --json print `{ pinned, upstream, ahead }` and nothing else, then stop - * before the checksums download. update-badges.cjs needs the - * comparison and nothing else, and the alternative — scraping the + * before the checksums download. For a caller that needs the + * comparison and nothing else, the alternative — scraping the * "pinned: X upstream latest: vY" line this script prints for a * human — would rebuild, with a regex, a fact this script already * holds as data. Implies read-only: --json never writes the - * manifest, because the badge run is not the run that proposes a + * manifest, because a comparison is not a run that proposes a * pin, and skipping the checksums fetch is not a shortcut but the * point (nothing is being verified here). * @@ -141,7 +141,7 @@ async function main({ // The structured answer, which is all a --json caller wanted. Reported here // rather than after the checksums fetch below: that download exists to - // propose a manifest, and a badge run proposes nothing. + // propose a manifest, and a --json run proposes nothing. const comparison = { pinned: manifest.valeVersion, upstream: upstreamVersion, diff --git a/.github/scripts/vale-detect.test.cjs b/.github/scripts/vale-detect.test.cjs index 5f34357c..27249b39 100644 --- a/.github/scripts/vale-detect.test.cjs +++ b/.github/scripts/vale-detect.test.cjs @@ -148,9 +148,8 @@ test("detect: an upstream tag behind the pin is also a no-op", async () => { }); test("detect: --json reports the comparison without fetching checksums", async () => { - // What update-badges.cjs consumes. It takes the RETURN value rather than the - // printed line, but both are asserted here: the printed line is the contract - // for anything calling the script from a shell. + // Both the RETURN value and the printed line are asserted here: the printed + // line is the contract for anything calling the script from a shell. const lines = []; const original = console.log; console.log = (line) => lines.push(line); @@ -172,7 +171,7 @@ test("detect: --json reports the comparison without fetching checksums", async ( }); assert.equal(lines.length, 1); assert.deepEqual(JSON.parse(lines[0]), result.comparison); - // The badge needs the comparison, not the digests, so the ahead path stops + // --json needs the comparison, not the digests, so the ahead path stops // before the download the manifest proposal would need. assert.deepEqual(result.fetched, []); assert.equal(result.outputs.update, "true"); diff --git a/.github/workflows/update-badges.yml b/.github/workflows/update-badges.yml deleted file mode 100644 index 99f8609a..00000000 --- a/.github/workflows/update-badges.yml +++ /dev/null @@ -1,137 +0,0 @@ -# SPDX-License-Identifier: MIT -# Refresh the shields endpoint payloads for the toolchain badges. -# -# `.shields/vale.json` and `.shields/sg.json` are the only badge values this -# repository computes for itself. Shields cannot answer "is there a newer -# upstream we have not pinned", so the comparison runs here and the color rule -# lives in `.github/scripts/update-badges.cjs`, under code review like anything -# else. Everything else in the README's badge row is a shields built-in or a -# static badge and needs nothing from this file. -# -# WHY THIS IS ITS OWN WORKFLOW AND NOT A JOB IN A PUBLISHING ONE. -# -# It needs `contents: write`, because writing a badge means committing to -# `main`. `release-cli.yml`, `release-cli-nightly.yml`, and `release-vale.yml` -# each hold `id-token: write` and an npm OIDC publishing identity. Keeping the -# two apart in SEPARATE FILES makes the separation structural: there is no -# arrangement of jobs here that a later edit could get wrong, because the -# credentialed workflows are not in this file to be edited. A job added to a -# release workflow would put a write token and a publishing identity one -# `permissions:` block away from each other, and the only thing keeping them -# apart would be whoever reviews the next change to that file. Do not merge -# this into a release workflow to save a file. -# -# WHY IT IS NOT FOLDED INTO release-vale.yml SPECIFICALLY. That workflow's -# detect phase opens a pull request when upstream Vale is ahead. The badge has -# to be YELLOW while that pull request is open and green once it merges, so the -# payload cannot live in that pull request's own diff — it would go yellow at -# exactly the moment it should go green. It has to reach `main` independently -# of the change it is warning about. -# -# WHY THE SCHEDULE, AND WHY A BADGE COMMIT DOES NOT LOOP. -# -# `validate.yml` triggers on every push to `main` with NO `paths` filter, and -# since #132 the nightly publishes on `Validate` completing. So a commit here -# costs one Validate run and, if changesets are pending, one nightly publish. -# That chain terminates only because what is committed depends on UPSTREAM -# moving, not on us pushing: -# -# badge commit → Validate → nightly workflow_run → nightly publishes -# → next scheduled badge run finds the same upstream versions -# → writes nothing → no commit → chain over. -# -# A badge derived from each nightly would NOT terminate: publish → commit → -# Validate → publish, forever. The nightly gate cannot stop it, because it -# dedupes per-sha and every badge commit is a new sha. That is why the nightly -# badge in the README is a plain static one with no version in it, and why -# nothing in this workflow ever reads a nightly version. -# -# The same reasoning bounds the cost of this workflow. The schedule is daily so -# an upstream release is noticed within a day, but `update-badges.cjs` rewrites -# a payload only when the rendered badge actually changes — and the date it -# renders advances only once a week — so a quiet week produces at most one -# commit per badge, not seven. -# -# THE DATE IN THE MESSAGE IS THE POINT, NOT DECORATION. A badge that stopped -# being updated looks identical to one whose value has not changed. Rendering -# `vale | 3.17.1 · 2026-08-21` means a reader can tell "upstream has been -# quiet" from "this job died in April" without opening anything. -# -# Action refs are pinned to commit SHAs; the trailing comment records the tag. - -name: Update badges - -on: - # Upstream's release cadence, not ours. This answers "what is upstream - # doing", which has nothing to do with our own pushes or releases — hence a - # schedule rather than a trigger on any repository event. - schedule: - - cron: "17 6 * * *" - - # For "upstream just released, do not wait until tomorrow". - workflow_dispatch: - -# No workflow-wide grants; the one job asks for exactly what it needs. -permissions: {} - -# One at a time, so two runs cannot both decide to commit the same payload. -concurrency: update-badges - -jobs: - update: - name: Refresh toolchain badges - runs-on: ubuntu-latest - permissions: - contents: write # commit the refreshed payloads to main - steps: - # Credentials persist because this job pushes. It holds no npm identity, - # no id-token, and runs nothing it downloaded. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: .nvmrc - - # No install step: the scripts are zero-dependency CommonJS. GITHUB_TOKEN - # is passed only to raise the GitHub API rate limit on the Vale release - # lookup; both endpoints (GitHub releases, the npm registry) are public. - - id: badges - run: node .github/scripts/update-badges.cjs --write - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Guarded on the script's own answer rather than on `git diff`, so the - # decision is made in the tested code path rather than restated in shell. - # Nothing from a workflow expression is interpolated into this body. - # - # RETRIED, because losing a race here is the ordinary case rather than an - # exceptional one: any pull request merging between the checkout and this - # push makes `main` a non-fast-forward, and a bare push would fail the job - # and leave the badge stale until tomorrow's schedule. Rebasing our single - # commit onto the new tip and pushing again is exactly what a human would - # do, and `.shields/` is touched by nothing else, so the rebase cannot - # meet a conflict from an ordinary merge. - # - # Bounded at three attempts. A push that keeps losing is not a race any - # more, and a job that retries forever hides whatever is actually wrong. - - name: Commit the refreshed payloads - if: steps.badges.outputs.changed == 'true' - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add .shields - git commit -m "chore(badges): refresh upstream toolchain badges" - - for attempt in 1 2 3; do - if git push origin HEAD:main; then - echo "Pushed on attempt ${attempt}." - exit 0 - fi - echo "Push rejected on attempt ${attempt}; rebasing onto the new main." - git fetch origin main - git rebase origin/main - done - - echo "::error::could not push the badge commit after 3 attempts" - exit 1 diff --git a/.shields/sg.json b/.shields/sg.json deleted file mode 100644 index 24bd3e05..00000000 --- a/.shields/sg.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schemaVersion": 1, - "label": "sg", - "message": "0.41.0 · 2026-08-22", - "color": "yellow" -} diff --git a/.shields/vale.json b/.shields/vale.json deleted file mode 100644 index 1a1812bb..00000000 --- a/.shields/vale.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "schemaVersion": 1, - "label": "vale", - "message": "3.17.1 · 2026-08-22", - "color": "yellow" -} diff --git a/packages/cli/README.md b/packages/cli/README.md index 88014868..73caf220 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -3,8 +3,6 @@ [![npm](https://img.shields.io/npm/v/@taskless/cli)](https://www.npmjs.com/package/@taskless/cli) [![build](https://img.shields.io/github/actions/workflow/status/taskless/cli/validate.yml?branch=main)](https://github.com/taskless/cli/actions/workflows/validate.yml?query=branch%3Amain) [![nightly](https://img.shields.io/badge/nightly-npm-blue)](https://www.npmjs.com/package/@taskless/cli-nightly) -[![vale](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/taskless/cli/main/.shields/vale.json)](https://github.com/errata-ai/vale/releases) -[![sg](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/taskless/cli/main/.shields/sg.json)](https://github.com/ast-grep/ast-grep/releases) CLI companion for [Taskless](https://taskless.io). Designed to work with agent skills to add constraints that improve coding agent output.