From d42c54ca95829ac5726fb71e4e28d78226887f81 Mon Sep 17 00:00:00 2001 From: midagedev Date: Wed, 30 Sep 2026 05:14:55 +0900 Subject: [PATCH] ci: main runs to a verdict, and staticcheck reads the filter every other tier reads (GDK-2003, GDK-2044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things, all on the billable half. cancel-in-progress applied to every ref, so a push that overtook another left a main commit with no verdict at all — "a push is not the end, CI green is the end" broken at the source. Only PR heads cancel now; main and workflow_dispatch run to a conclusion. The staticcheck job was the last inline input filter in this file, and its regex carried ^desktop/, so one line of desktop/README.md woke the 335 s GOOS matrix. It reads tools/ci-filter.sh now — but under its own subject, not `go`: tools/staticcheck.sh analyses the desktop module too, and the go table carves desktop/ out, so reusing it would have skipped a desktop/main.go push and hidden a Go change from a gate. That inverse is the row the test pins hardest. The pinned binary is a pure function of (version, OS, arch, Go toolchain) and is cached whole, with no restore-keys: a prefix hit could restore a binary built by another toolchain, and the safe failure here is a miss, which is today's behaviour. The mobile subject's web rows had rotted the other way (GDK-2044). They named lib/i18n/ and lib/terminal/ while the phone imports 24 modules out of web/src/lib — view-config, issue-group, keyboard, adf, person-match and the rest — so a push touching only web/src/lib/view-config.ts skipped the Mobile job over a file the phone reads. That is the one direction a filter must never fail in: CI green over a broken phone. The row is the directory now, and ci-filter-test.sh derives the phone's import set from its own source and fails on anything the table misses, so narrowing it later is red rather than silent. An enumeration is what rotted the first time. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 126 ++++++++++++++++++++++----------------- tools/ci-filter-test.sh | 88 ++++++++++++++++++++++++++- tools/ci-filter.sh | 68 +++++++++++++++++++-- tools/staticcheck.sh | 5 +- 4 files changed, 222 insertions(+), 65 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5aa6da5a6..0d9515165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,12 @@ permissions: concurrency: group: ci-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + # A cancelled run keeps neither its compute nor a verdict, and every main + # commit must carry a green one — so only PR heads cancel (only their + # latest head ever matters). workflow_dispatch evaluates false here + # (event_name != 'pull_request'), on purpose: a dispatched run is an + # explicit request that cancelling would discard. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: build: @@ -154,8 +159,9 @@ jobs: # GDK-1912: the census measured 31 of 127 commits in one cycle # touching only CHANGELOG/CLAUDE.md/docs — each re-billing this tier - # (~1,150 s of race compute). Same shape as the staticcheck job's - # gofilter and the same fail-open contract, one generalization: + # (~1,150 s of race compute). Same shape and fail-open contract as + # the staticcheck job's original inline gofilter (since GDK-2003 a + # ci-filter.sh subject too), one generalization: # tools/ci-filter.sh owns the input table so every filtered job reads # the same one (subject `go`: **/*.go, module files, internal/ # testdata + embeds, tools/, .github/workflows/). The step's log line @@ -234,7 +240,7 @@ jobs: # GDK-1912: subject `mobile` — mobile/, the two web/src slices it # imports, the root lockfile (Playwright), the brand mark, the # pairing vectors, and the Go module gate-serve.sh builds for - # `gadak demo`. Fail-open like the gofilter; the log line names the + # `gadak demo`. Fail-open by the ci-filter.sh contract; the log line names the # verdict. - name: Did this change touch the mobile inputs? id: cifilter @@ -312,7 +318,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/ms-playwright - key: playwright-chromium-${{ steps.pwver.outputs.version }} + key: playwright-chromium-${{ steps.pwver.outputs.version }}-${{ runner.os }}-${{ runner.arch }} - name: Install Playwright Chromium if: steps.cifilter.outputs.run == 'true' @@ -369,6 +375,11 @@ jobs: name: Staticcheck runs-on: ubuntu-latest timeout-minutes: 15 + env: + # Single owner of the pin: the cache key below and the install + # command both read it, so a bump cannot leave a stale-keyed binary + # behind (GDK-2003). + STATICCHECK_VERSION: v0.7.0 steps: - uses: actions/checkout@v4 @@ -376,71 +387,74 @@ jobs: # GDK-1702: this job billed ~5 min of setup+install per push while # the census measured most pushes touching no Go at all. It is a gate # (the cross-platform list is empty since 90e27bdf), so the skip must - # never hide a Go change — hence the fail-open rules below. GitHub Actions has no - # per-job paths filter (`on:` filters are workflow-level and would - # skip every job), so the first step diffs the change and every step - # below is gated on its output. - # - # It fails OPEN, and that is the contract: a forced push whose before - # is unreachable, a first push (all-zero before), workflow_dispatch, - # an empty event base, or any diff error runs the job. A gate skipped - # by mistake is the failure mode that matters; a gate run for nothing - # is five minutes. - # - # The path list is the Go surface plus the job's own inputs: the - # script it runs, and this workflow (a change here can rewrite the - # gate itself). `git diff A B` compares trees without needing shared - # history, so two depth-1 objects are enough. - - name: Did this change touch Go? - id: gofilter + # never hide a Go change — the fail-open contract (unreachable or + # all-zero before, unknown event, failed diff → run) is owned by + # tools/ci-filter.sh, same as every other filtered tier. The subject + # is its own, not `go`: staticcheck analyses the desktop module too + # (`run_module desktop desktop ./...`), so this table has no + # desktop/ carve-out — and its path list is the script's actual + # inputs, which the old inline regex here was not (`^desktop/` woke + # the 335 s GOOS matrix for a README line; GDK-2003 removed the last + # inline filter copy in this file). + - name: Did this change touch the staticcheck inputs? + id: cifilter env: EVENT_NAME: ${{ github.event_name }} BEFORE: ${{ github.event.before }} BASE_SHA: ${{ github.event.pull_request.base.sha }} SHA: ${{ github.sha }} - run: | - run_job() { echo "go=true" >> "$GITHUB_OUTPUT"; echo "staticcheck: $1"; exit 0; } - skip_job() { echo "go=false" >> "$GITHUB_OUTPUT"; echo "staticcheck: $1"; exit 0; } - case "$EVENT_NAME" in - pull_request) base="$BASE_SHA" ;; - push) base="$BEFORE" ;; - *) run_job "event '$EVENT_NAME' has no before — running (fail open)" ;; - esac - if [ -z "$base" ] || printf '%s' "$base" | grep -Eq '^0+$'; then - run_job "no usable base sha (first push or forced push) — running (fail open)" - fi - if ! git fetch --no-tags --depth=1 origin "$base" 2>/dev/null; then - run_job "base $base not fetchable — running (fail open)" - fi - if ! changed="$(git diff --name-only "$base" "$SHA")"; then - run_job "git diff failed — running (fail open)" - fi - n=$(printf '%s\n' "$changed" | grep -c . || true) - if printf '%s\n' "$changed" | grep -Eq '\.go$|^go\.mod$|^go\.sum$|^desktop/|^tools/staticcheck\.sh$|^\.github/workflows/ci\.yml$'; then - run_job "Go-touching change in ${n} changed path(s)" - fi - skip_job "no Go-touching change in ${n} changed path(s)" + run: bash tools/ci-filter.sh staticcheck - name: Set up Go - if: steps.gofilter.outputs.go == 'true' + if: steps.cifilter.outputs.run == 'true' uses: actions/setup-go@v5 with: go-version-file: go.mod cache: true + # The exact toolchain the analysis will run under — setup-go resolved + # it from go.mod, the single owner of the Go version. GOVERSION (not + # a go.mod hash, which also rotates on every dependency bump) is what + # makes a hit trustworthy: a staticcheck binary built by a different + # Go can analyse differently. + - name: Go toolchain stamp + id: gover + if: steps.cifilter.outputs.run == 'true' + run: echo "version=$(go env GOVERSION)" >> "$GITHUB_OUTPUT" + + # The pinned binary is a pure function of (staticcheck version, OS, + # arch, Go toolchain) — nothing repo-specific enters it — so it is + # cached whole (GDK-2003; 25–35 s of `go install` per Go-touching + # run otherwise). No restore-keys on purpose: a prefix hit could + # restore a binary built by a different toolchain or version and + # quietly change what the gate analyses. A miss is today's behaviour + # — install — so the safe failure is a miss. + - name: staticcheck binary cache + if: steps.cifilter.outputs.run == 'true' + uses: actions/cache@v4 + with: + path: ~/go/bin/staticcheck + key: staticcheck-bin-${{ env.STATICCHECK_VERSION }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.gover.outputs.version }} + # What decides whether a finding fails the run or is a build-tag # artefact is the script's classifier, not staticcheck. It runs over # fixtures, needs no toolchain, and takes a second. - name: staticcheck.sh self-test - if: steps.gofilter.outputs.go == 'true' + if: steps.cifilter.outputs.run == 'true' run: bash tools/staticcheck.sh --self-test # Pinned on purpose: staticcheck gains checks between releases, and - # @latest would turn a new check into a CI change nobody made. + # @latest would turn a new check into a CI change nobody made. On a + # cache hit the binary is already in ~/go/bin and the existence check + # is the skip; $GITHUB_PATH is added on both paths (GDK-2003). - name: Install staticcheck - if: steps.gofilter.outputs.go == 'true' + if: steps.cifilter.outputs.run == 'true' run: | - go install honnef.co/go/tools/cmd/staticcheck@v0.7.0 + if [ ! -x "$(go env GOPATH)/bin/staticcheck" ]; then + go install "honnef.co/go/tools/cmd/staticcheck@${STATICCHECK_VERSION}" + else + echo "staticcheck ${STATICCHECK_VERSION} restored from cache — install skipped" + fi echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" # A gate since the cross-platform list reached zero (GDK-1463 triage, @@ -452,7 +466,7 @@ jobs: # second copy of the mirror-flaky apt step in this file for a job that # cannot fail. The root module gets all three either way. - name: staticcheck over the GOOS matrix - if: steps.gofilter.outputs.go == 'true' + if: steps.cifilter.outputs.run == 'true' run: bash tools/staticcheck.sh e2e: @@ -484,7 +498,7 @@ jobs: # GDK-1912: subject `e2e` — the whole web surface plus the Go module, # because e2e/serve.sh (the webServer playwright waits on) builds the # gadak binary before serving the committed fixtures. Fail-open like - # the gofilter; the log line names the verdict. + # ci-filter.sh's fail-open contract; the log line names the verdict. - name: Did this change touch the e2e inputs? id: cifilter env: @@ -528,7 +542,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/ms-playwright - key: playwright-chromium-${{ steps.pwver.outputs.version }} + key: playwright-chromium-${{ steps.pwver.outputs.version }}-${{ runner.os }}-${{ runner.arch }} # --with-deps is two unlike jobs: apt (root, dpkg lock, not safely # killable) and a browser download (no lock, retryable). Wrapping the @@ -699,7 +713,7 @@ jobs: - uses: actions/checkout@v4 # GDK-1912: subject `desktop` — desktop/ plus the root module and web - # build every pack script compiles. Fail-open like the gofilter. + # build every pack script compiles. Fail-open by ci-filter.sh's contract. - name: Did this change touch the desktop inputs? id: cifilter env: @@ -780,7 +794,7 @@ jobs: # GDK-1912: subject `desktop` — the Linux pack compiles the root # module, the desktop module and the web build, so the table is the - # union. Fail-open like the gofilter. + # union. Fail-open by ci-filter.sh's contract. - name: Did this change touch the desktop inputs? id: cifilter env: @@ -1142,8 +1156,8 @@ jobs: - uses: actions/checkout@v4 # GDK-1912: subject `desktop` — this job builds the root module - # (cmd/gadak), the desktop module and the web build. Fail-open like - # the gofilter. + # (cmd/gadak), the desktop module and the web build. Fail-open by + # ci-filter.sh's contract. - name: Did this change touch the desktop inputs? id: cifilter env: diff --git a/tools/ci-filter-test.sh b/tools/ci-filter-test.sh index 19de6db5f..300f3b289 100644 --- a/tools/ci-filter-test.sh +++ b/tools/ci-filter-test.sh @@ -145,7 +145,7 @@ docs/project/STATE_OF_PLAY.md specs/000-product/data-model.md artifacts/notes.txt contrib/README.md" -for s in go e2e mobile desktop; do +for s in go e2e mobile desktop staticcheck; do expect_skip "docs-only / $s" "$s" "$DOCS_ONLY" done # docs/media is mobile's ONE docs input (the brand-icon check reads it) — @@ -157,7 +157,7 @@ echo "== case 2: one .go change runs every tier that compiles Go" # ci.yml, not taste: the race tier compiles the two packages from the root # module; e2e/serve.sh builds the binary it serves; gate-serve.sh runs # `gadak demo`; every desktop pack script builds ./cmd/gadak. -for s in go e2e mobile desktop; do +for s in go e2e mobile desktop staticcheck; do expect_run "one .go / $s" "$s" "internal/server/sync.go" done expect_run "cmd .go / go" go "cmd/gadak/main.go" @@ -228,6 +228,42 @@ expect_skip "a stray file under examples/ / go" go "examples/notes.txt" expect_skip "a stray file under examples/ / e2e" e2e "examples/notes.txt" expect_skip "a stray file under examples/ / mobile" mobile "examples/notes.txt" +echo +echo "== case 3d: subject staticcheck — both modules, no desktop carve-out (GDK-2003)" +# The defect this subject closes: the staticcheck job's inline gofilter +# regex carried `^desktop/`, so desktop/README.md alone woke the 335 s GOOS +# matrix. The inverse trap is worse and is the reason this is a NEW subject +# rather than subject `go`: the go table carves desktop/ out (no +# root-module tier compiles it), but tools/staticcheck.sh DOES analyse the +# desktop module (`run_module desktop desktop ./...`) — reusing `go` would +# skip a desktop/main.go-only push and hide a Go change from a gate. The +# run/skip pair below pins both directions; desktop/main.go is the row that +# catches the reuse-go trap. +expect_skip "desktop/README.md does not touch staticcheck" staticcheck "desktop/README.md" +expect_run "desktop module .go → staticcheck (the reuse-go trap)" staticcheck "desktop/main.go" +expect_run "run 35025858519 / staticcheck" staticcheck "$RUN_35025858519" +expect_run "desktop module graph → staticcheck" staticcheck "desktop/go.mod" +expect_run "desktop module sums → staticcheck" staticcheck "desktop/go.sum" +expect_skip "pack scripts are not analysis inputs" staticcheck "desktop/build-linux.sh" +expect_skip "the syso pair is a link input, not an analysis one" staticcheck "desktop/windows-app.manifest" +expect_run "root module graph → staticcheck" staticcheck "go.mod" +expect_run "root .go → staticcheck" staticcheck "internal/statuscat/category.go" +# internal/ stays a whole-directory row for one measured reason: the +# //go:embed files there are load-bearing. `go list -e` on a package whose +# embed target is deleted reports "pattern data.json: no matching files +# found" with Incomplete=true — the load itself fails, which +# staticcheck.sh's classifier fails the run on. Content is never analysed; +# existence is. (testdata/ is inert for staticcheck but shares the prefix.) +expect_run "embed catalogs are load-bearing" staticcheck "internal/config/tokencheck/catalog.json" +expect_run "test-file embeds load too" staticcheck "internal/config/tokencheck/testdata/token-vectors.json" +# The analysis owner and the skip owner, each its own row (not dir:tools/: +# doc-checks.sh edits must not wake this job). +expect_run "the analysis script → staticcheck" staticcheck "tools/staticcheck.sh" +expect_run "the filter owns this skip → staticcheck" staticcheck "tools/ci-filter.sh" +expect_run "the workflow → staticcheck" staticcheck ".github/workflows/ci.yml" +expect_skip "web does not touch staticcheck" staticcheck "web/src/routes/+page.svelte" +expect_skip "docs-only / staticcheck (single file)" staticcheck "CHANGELOG.md" + echo echo "== case 4: fail-open — no diff obtainable means run, always" : > "$WORK/empty.txt" @@ -237,6 +273,7 @@ export FAKE_GIT_DIFF="$WORK/chg.txt" # 4a. workflow_dispatch / schedule: no before at all. CI_ENV=(EVENT_NAME=workflow_dispatch SHA=deadbeef FAKE_GIT_MODE=ok) ci_mode "workflow_dispatch → run" run go +ci_mode "workflow_dispatch → run (staticcheck, same ladder)" run staticcheck # 4b. push with no before (first push) and all-zero before (forced/tag push). CI_ENV=(EVENT_NAME=push BEFORE= SHA=deadbeef FAKE_GIT_MODE=ok) @@ -291,9 +328,11 @@ expect_run "a workflow rewrite re-runs every tier" go ".github/workflows/ci.yml" expect_run " (same, e2e)" e2e ".github/workflows/ci.yml" expect_run " (same, mobile)" mobile ".github/workflows/ci.yml" expect_run " (same, desktop)" desktop ".github/workflows/ci.yml" +expect_run " (same, staticcheck)" staticcheck ".github/workflows/ci.yml" # The filter itself lives under tools/: editing it re-runs every tier, so a # broken filter never ships hidden behind its own verdict. expect_run "filter self-edit → go" go "tools/ci-filter.sh" +expect_run "filter self-edit → staticcheck" staticcheck "tools/ci-filter.sh" echo echo "== stdout contract: verdict line first, reason second, GITHUB_OUTPUT written" @@ -321,7 +360,52 @@ echo "== FAIL-first: the checks go red on bad answers" expect_skip "webmap.ts is not web/" e2e "webmap.ts" expect_skip "a nested go.mod that is not the root one" go "docs/go.mod" expect_skip "a .go suffix inside a filename, not at the end" go "proto.go.txt" +expect_skip " (same, staticcheck)" staticcheck "proto.go.txt" expect_skip "gomodulate is not go.mod" desktop "gomodulate.sh" +expect_skip "desktop/gomod.sh is not desktop/go.mod" staticcheck "desktop/gomod.sh" +expect_skip "docs/go.mod is not the root go.mod" staticcheck "docs/go.mod" + +echo +echo "== the mobile table covers everything the phone imports out of web/ (GDK-2044)" +# Derived, never enumerated. The mobile subject's web rows rotted once: the +# table named lib/i18n/ and lib/terminal/ while mobile/src imported 24 +# modules out of web/src/lib, so a push touching only +# web/src/lib/view-config.ts skipped the Mobile job and CI went green over +# a phone that reads that file. An enumeration is what rotted, so this +# reads the imports off the phone's own source and asks the filter itself. +# Only real import/@import statements — a path a comment merely names is +# not an input (mobile/src cites several web components in prose). +mobile_web_imports() { + grep -rhE "^[[:space:]]*(import|@import|} from|export .* from)[^\n]*web/src/" mobile/src mobile/e2e 2>/dev/null \ + | grep -oE "web/src/[A-Za-z0-9/_.-]+" \ + | sed "s#/*\$##" | sort -u +} +imported="$(mobile_web_imports)" +n_imports=$(printf '%s\n' "$imported" | grep -c . || true) +if [[ "$n_imports" -lt 8 ]]; then + fail "only $n_imports web/ imports found in mobile/src (floor 8) — the scan rotted, fix the scan and not the floor" +fi +uncovered="" +while IFS= read -r f; do + [[ -z "$f" ]] && continue + # Module specifiers drop the extension; resolve to whatever is on disk. + if [[ ! -f "$f" ]]; then + if [[ -f "$f.ts" ]]; then f="$f.ts" + elif [[ -f "$f.svelte" ]]; then f="$f.svelte" + elif [[ -f "$f/index.ts" ]]; then f="$f/index.ts" + else continue + fi + fi + if [[ "$(decide mobile "$f")" != run=true ]]; then + uncovered="$uncovered $f" + fi +done <<<"$imported" +if [[ -n "$uncovered" ]]; then + fail "mobile/src imports these out of web/, and the mobile subject skips them — + a push touching only one would leave the Mobile job unrun:$uncovered" +else + echo "- all $n_imports web/ paths the phone imports run the mobile subject" +fi echo if [[ "$FAILURES" -eq 0 ]]; then diff --git a/tools/ci-filter.sh b/tools/ci-filter.sh index 5cec6e58e..74081e74d 100644 --- a/tools/ci-filter.sh +++ b/tools/ci-filter.sh @@ -52,8 +52,20 @@ # of examples/ (dashboards/, compose/, plugins/ are read by demo # recordings and Go tests outside this filtered tier). # -# mobile mobile/ (own src, e2e, package-lock.json, tauri) plus the two -# web/src slices it imports (lib/i18n/, lib/terminal/); the root +# mobile mobile/ (own src, e2e, package-lock.json, tauri) plus the web +# slices it imports — web/src/lib/ WHOLE and web/src/app.css +# (GDK-2044). It was two slices (lib/i18n/, lib/terminal/) until a +# count showed the phone importing 24 modules out of that +# directory — adf, api, issue-group, view-config, keyboard, +# person-match and the rest — so a push touching only +# web/src/lib/view-config.ts skipped the Mobile job while the +# phone read that file. That is the one direction this filter +# must never fail in: CI green over a broken phone, the inverse +# of the local-green/CI-red class. The directory row is +# deliberate over a 24-line enumeration, which is what rotted the +# first time; ci-filter-test.sh derives the import set from +# mobile/src and fails if any of it falls outside this table, so +# narrowing the row later is red rather than silent. The root # package.json/package-lock.json (Playwright lives at the root); # docs/media/logo.png (check-brand-icons.sh diffs the phone icons # against the mark); internal/pairing/testdata/ (offer-vectors.json @@ -75,6 +87,37 @@ # .github/workflows/. No desktop/ carve-out here: this subject # IS the one that builds it. # +# staticcheck tools/staticcheck.sh analyses BOTH modules over the GOOS +# matrix (`run_module root . ./cmd/... ./internal/... ./tools/...` +# + `run_module desktop desktop ./...`, GOOS darwin/linux/windows): +# **/*.go with NO desktop/ carve-out — unlike subject `go`, the +# desktop module IS an analysis input here, so desktop/main.go +# must run this subject (the run 35025858519 push that subject +# `go` rightly skips must run this one); go.mod, go.sum, +# desktop/go.mod, desktop/go.sum (the two module graphs staticcheck +# resolves). internal/ stays a whole-directory row for one +# MEASURED reason, not the race tier's: the //go:embed files +# there are load-bearing — `go list -e` on a package whose embed +# target is deleted answers `pattern data.json: no matching +# files found`, Incomplete=true (verified with a scratch module), +# and staticcheck.sh's classifier fails the run on a load error. +# Content is never analysed (staticcheck type-checks, never +# executes); existence is. The measured set: catalog.json, +# dim-catalog.json (internal/config/tokencheck/), and +# testdata/token-vectors.json — tokencheck_test.go embeds it and +# staticcheck loads _test.go files; other internal/ testdata is +# inert but shares the prefix. NOT web/, package.json, .nvmrc +# (no Node anywhere in this job), NOT examples/, contrib/ or the +# root package embed.go (outside the ./cmd/... ./internal/... +# ./tools/... patterns — a stray suffix:.go match on those four +# files is cost, never a wrong skip). tools/staticcheck.sh (the +# analysis owner) and tools/ci-filter.sh (the skip owner — the +# same pairing as subject go's race-partition.sh row: a change +# here can rewrite the gate itself) as exact rows, NOT dir:tools/ +# — doc-checks.sh and the e2e/race partitioners are not this +# job's inputs and must not wake it. .github/workflows/ (the job +# and the pinned staticcheck version live there). +# # Visibility: a filter that never says anything is its own defect, so every # decision — run or skip — prints one line naming the subject, the verdict, # and why (how many paths changed, which pattern matched). Those lines are @@ -99,7 +142,7 @@ SELF="ci-filter" usage() { cat >&2 <<'EOF' usage: - tools/ci-filter.sh subject: go | e2e | mobile | desktop + tools/ci-filter.sh subject: go | e2e | mobile | desktop | staticcheck tools/ci-filter.sh --files decide from a path list ('-' = stdin) EOF exit 2 @@ -155,8 +198,8 @@ EOF cat <<'EOF' not:desktop/ dir:mobile/ -dir:web/src/lib/i18n/ -dir:web/src/lib/terminal/ +dir:web/src/lib/ +exact:web/src/app.css exact:docs/media/logo.png dir:internal/pairing/testdata/ exact:examples/demo.db @@ -182,10 +225,23 @@ exact:go.sum exact:package.json exact:package-lock.json exact:.nvmrc +EOF + ;; + staticcheck) + cat <<'EOF' +suffix:.go +exact:go.mod +exact:go.sum +exact:desktop/go.mod +exact:desktop/go.sum +dir:internal/ +exact:tools/staticcheck.sh +exact:tools/ci-filter.sh +dir:.github/workflows/ EOF ;; *) - echo "$SELF: unknown subject '$1' (go | e2e | mobile | desktop)" >&2 + echo "$SELF: unknown subject '$1' (go | e2e | mobile | desktop | staticcheck)" >&2 exit 2 ;; esac diff --git a/tools/staticcheck.sh b/tools/staticcheck.sh index 41ec96a04..734b2213d 100755 --- a/tools/staticcheck.sh +++ b/tools/staticcheck.sh @@ -318,7 +318,10 @@ fi # --- real run -------------------------------------------------------------- if ! command -v staticcheck >/dev/null 2>&1; then echo "staticcheck.sh: staticcheck is not on PATH." >&2 - echo " go install honnef.co/go/tools/cmd/staticcheck@latest" >&2 + # The version CI pins (.github/workflows/ci.yml, STATICCHECK_VERSION). + # @latest would install a build with checks this tree has never been + # measured against, so a local run could disagree with the gate. + echo " go install honnef.co/go/tools/cmd/staticcheck@v0.7.0" >&2 exit 2 fi