From 718377af9af25d5de83ab8b82cef0bec2d7173ba Mon Sep 17 00:00:00 2001 From: Jacob Wan Date: Mon, 10 Aug 2026 12:48:49 -0700 Subject: [PATCH 1/2] ci: key the pin-lag check on bump rather than an empty object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shape of `mise outdated --json` depends on whether the tools are installed. This job reports without installing, where mise emits one entry per tool carrying a null bump rather than an empty object — so comparing the payload against {} was true on every run, and the report has never been able to say clean. It has run three times and failed all three: twice with nothing behind at all, and once where a pin genuinely had moved, which is the only reason the mechanism ever looked like it worked. Keying on bump answers the question in either state, and the pins it finds are printed into the run log so a reader need not re-derive them from the table above. An empty payload is asserted against rather than read as a verdict, because that is the failure direction this monitor cannot survive. Nothing notifies on a green run, so a reporter that has gone blind — an unreachable mise.toml, a changed working directory — would otherwise report clean indefinitely. Each jq call is guarded and names its own failure, for the same reason: an unguarded one exits under errexit with no annotation, and a shared message would name the wrong cause. --- .github/workflows/toolchain.yml | 44 ++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/.github/workflows/toolchain.yml b/.github/workflows/toolchain.yml index ec068d3..370dc3f 100644 --- a/.github/workflows/toolchain.yml +++ b/.github/workflows/toolchain.yml @@ -71,15 +71,51 @@ jobs: # Decisive, and captured once so the reporter's own failure is told # apart from a stale pin: a failed or silent mise yields an empty - # substitution, which compared straight against {} would raise the - # pin-lag error and so name the wrong cause. Testing it inside `if` is - # what keeps errexit from exiting here with no annotation at all. + # substitution, and an empty result must never be read as a verdict + # about the pins. Testing it inside `if` is what keeps errexit from + # exiting here with no annotation at all. if ! outdated="$(mise outdated --bump --local --json)" || [ -z "$outdated" ]; then echo "::error::mise could not report pin status — the reporter failed, not the pins" exit 1 fi - if [ "$outdated" != "{}" ]; then + # Every jq call is guarded, and each names its own failure. Unguarded, + # jq would exit under errexit with no annotation; sharing one message + # would name the wrong cause. Both are the bare and misattributed + # exits this step exists to never produce. + if ! count="$(jq -r 'length' <<<"$outdated")"; then + echo "::error::jq could not process mise's output — the reporter failed, not the pins" + exit 1 + fi + + # A verdict is only readable from a payload that describes the pins. + # Reporting without installing means mise emits an entry per pinned + # tool, so nothing to count is the reporter having seen nothing — an + # unreachable or renamed mise.toml, say — rather than a clean bill. + # This is the direction that must fail loudly: a green run notifies + # no one, so a report gone blind would stay that way indefinitely. + if [ "$count" -eq 0 ]; then + echo "::error::mise reported no pinned tools — the reporter failed, not the pins" + exit 1 + fi + + # Keyed on bump rather than on the payload being empty, because the + # shape of --json depends on installation state: reporting without + # installing yields an entry per tool carrying a null bump, which + # makes an emptiness test true on every run. bump is non-null exactly + # when a pin trails upstream, in either state — verified against the + # mise release pinned above, which is where to re-check it. The count + # guard does not cover a renamed or dropped bump: entries would still + # be present, jq would match none of them, and the report would go + # quiet. So a run that stops failing across a mise upgrade is the + # symptom to distrust. + if ! behind="$(jq -r 'to_entries[] | select(.value.bump != null) | "\(.key) \(.value.requested) -> \(.value.bump)"' <<<"$outdated")"; then + echo "::error::jq could not process mise's output — the reporter failed, not the pins" + exit 1 + fi + + if [ -n "$behind" ]; then echo "::error::Toolchain pins are behind upstream — review per SECURITY.md" + printf '%s\n' "$behind" exit 1 fi From fd80c54d1c6026483fca3bee8cdaf85d9c475e61 Mon Sep 17 00:00:00 2001 From: Jacob Wan Date: Mon, 10 Aug 2026 14:31:42 -0700 Subject: [PATCH 2/2] docs: say what a red toolchain report can mean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report distinguishes a stale pin from a broken reporter, but the security policy still described red as meaning only the first — so a reader hitting a reporter failure had nothing telling them the pins were never assessed. The schema-drift gap is named alongside it, since that one fails quiet and a reader has no other way to learn it exists. --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 97bac45..e3a42a3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -43,7 +43,7 @@ field-docket holds no credentials of its own. It authenticates to nothing, reads - **CI pins its own toolchain; `mise.lock` covers local development.** Tool versions live in `mise.toml` with their resolved download URLs and checksums in the committed `mise.lock`, and `mise install` verifies against it — but no CI job installs through mise, so the lockfile governs a contributor's machine rather than the build. CI pins independently: Go comes from `go.mod` via `actions/setup-go`, golangci-lint and GoReleaser from their actions' explicit `version:` inputs. Read the lockfile as reproducibility for local work, not as a control on the published artifacts. - **Actions are pinned by commit digest.** Every `uses:` in every workflow names a full commit SHA with the version as a trailing comment — a tag can be repointed, a commit SHA cannot. - **Update automation covers Go modules and GitHub Actions.** Dependabot watches both weekly, and bumps the action SHAs along with their version comments. -- **The mise toolchain is reviewed manually.** No update bot covers `mise.toml`, so the **Toolchain currency** workflow checks the pins against upstream weekly and **fails when any is behind**. The deliberate failure is what surfaces the report at all, since `mise outdated` exits 0 whether or not a pin has moved. How far that failure reaches is not a property of this repository: GitHub notifies you about workflow runs *you* triggered, on every completed run unless you select the "only notify for failed workflows" option, and for a scheduled run the person notified is the workflow's creator — or whoever last changed its cron or re-enabled it. Keeping this report in its own workflow rather than alongside the scan above is what lets a red result name which of the two fired, since the run list identifies a run by its workflow. It blocks nothing (neither workflow is a required check). `just toolchain-outdated` runs the same query locally, against whatever mise is on your `PATH` rather than the pinned one, and it reports without failing. The pinned version of mise itself lives in that workflow rather than in `mise.toml`, so nothing reports it; it moves only when a maintainer moves it. +- **The mise toolchain is reviewed manually.** No update bot covers `mise.toml`, so the **Toolchain currency** workflow checks the pins against upstream weekly and **fails when any is behind**. The deliberate failure is what surfaces the report at all, since `mise outdated` exits 0 whether or not a pin has moved. A red run therefore means one of three things, and its annotation says which: a pin is behind and is named, or mise could not report, or its output could not be processed. Read the last two as the reporter having failed rather than as a verdict about the pins — and note that the report is not able to detect a change in mise's own output schema, which would make it fall quiet rather than loud, so a run that stops failing across a mise upgrade is worth distrusting. GitHub notifies you about workflow runs *you* triggered, on every completed run unless you select the "only notify for failed workflows" option, and for a scheduled run the person notified is the workflow's creator — or whoever last changed its cron or re-enabled it. Keeping this report in its own workflow rather than alongside the scan above is what lets a red result name which of the two fired, since the run list identifies a run by its workflow. It blocks nothing (neither workflow is a required check). `just toolchain-outdated` runs the same query locally, against whatever mise is on your `PATH` rather than the pinned one, and it reports without failing. The pinned version of mise itself lives in that workflow rather than in `mise.toml`, so nothing reports it; it moves only when a maintainer moves it. - **Scheduled scanning stops firing in a quiet repository.** In a public repository GitHub disables scheduled workflows after 60 days without repository activity. That is one repository-level clock rather than one per workflow, so **Vulnerability scan** and **Toolchain currency** stop together — read a gap in either one's weekly runs as a symptom, not as a clean result. Re-enabling one moves its notifications, per the bullet above. This describes a detector paired with a human response, not a project that is continuously free of known advisories. Go patch releases routinely fix reachable standard-library symbols, so the scan going red is expected periodically and is resolved by a maintainer bumping the pinned toolchain.