From b3720f00d4c9c42799d6638a82c6b5e4d9423784 Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 4 Nov 2026 22:43:07 -0600 Subject: [PATCH 1/3] Refactor: Nest the sentinel error check inside the open error check gohawk reports a failure-to-open result as a missing release, because a return under `errors.Is(err, os.ErrNotExist)` or `errors.Is(err, os.ErrExist)` does not prove that the open failed, so the analyzer cannot rule out an open resource on that path. Testing the error first gives the same result for every input and shows that the resource was never obtained. Behavior is unchanged; the go group's new gohawk step now passes. --- internal/timber/review/maintenance.go | 6 +++--- internal/timber/review/state.go | 6 +++--- internal/timber/review/worktree.go | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/timber/review/maintenance.go b/internal/timber/review/maintenance.go index 7a8ff5c..6610d0c 100644 --- a/internal/timber/review/maintenance.go +++ b/internal/timber/review/maintenance.go @@ -47,10 +47,10 @@ func prunePatchCache(ctx context.Context, dir string) (err error) { return err } folder, err := os.Open(dir) - if errors.Is(err, os.ErrNotExist) { - return nil - } if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } return err } defer func() { err = errors.Join(err, folder.Close()) }() diff --git a/internal/timber/review/state.go b/internal/timber/review/state.go index a55da08..72b8d5a 100644 --- a/internal/timber/review/state.go +++ b/internal/timber/review/state.go @@ -263,10 +263,10 @@ func (s Store) Update(action func(*State) error) (err error) { } lockPath := s.Path + ".lock" lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) - if errors.Is(err, os.ErrExist) { - return fmt.Errorf("review state is locked: %s; remove the lock only after confirming no writer is running", lockPath) - } if err != nil { + if errors.Is(err, os.ErrExist) { + return fmt.Errorf("review state is locked: %s; remove the lock only after confirming no writer is running", lockPath) + } return err } defer func() { err = errors.Join(err, lock.Close(), os.Remove(lockPath)) }() diff --git a/internal/timber/review/worktree.go b/internal/timber/review/worktree.go index ec187b1..5b67152 100644 --- a/internal/timber/review/worktree.go +++ b/internal/timber/review/worktree.go @@ -28,10 +28,10 @@ func WorktreePath(repo string) (string, error) { // reviewRunning probes an existing lock without creating it. func reviewRunning(ctx context.Context, path, branch string) (running bool, err error) { file, err := os.OpenFile(path+".lock", os.O_RDONLY, 0) - if errors.Is(err, os.ErrNotExist) { - return false, nil - } if err != nil { + if errors.Is(err, os.ErrNotExist) { + return false, nil + } return false, err } defer func() { err = errors.Join(err, file.Close()) }() From 67fea3282743cc4d3b1a942dbe1e8737a68c03db Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 4 Nov 2026 22:43:08 -0600 Subject: [PATCH 2/3] Grant each release job only the permission it needs A workflow-level `contents: write` lets the build jobs push commits and create releases, and the build restores a mise cache that a pull request can populate. jactionlint reports both when a workflow builds on a tag. The top level now grants nothing, the build jobs read the repository, and only the publish job writes, with the cache off so no cache entry from another branch can reach the published artifacts. --- .github/workflows/release.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 88a886f..4dd83ef 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,13 +2,14 @@ name: Release on: push: tags: ["v*"] -permissions: - contents: write +permissions: {} concurrency: group: release cancel-in-progress: false jobs: build: + permissions: + contents: read runs-on: ubuntu-26.04 timeout-minutes: 20 strategy: @@ -32,6 +33,8 @@ jobs: uses: jdx/mise-action@6612f0ef07ed2a736cd1e00a94a065522dd41195 # v5.2.5 with: version: 2026.10.25 + # A cache from a pull request could reach the published artifacts. + cache: false - name: Build env: GOOS: ${{ matrix.platform.os }} @@ -48,6 +51,8 @@ jobs: if-no-files-found: error publish: needs: build + permissions: + contents: write runs-on: ubuntu-26.04 timeout-minutes: 10 env: From c30161a85551a3a1a014ce7801b0640704f7872c Mon Sep 17 00:00:00 2001 From: Nathan Nutter Date: Wed, 4 Nov 2026 22:43:08 -0600 Subject: [PATCH 3/3] Take the hook steps and their order from hk-config The config listed every step and wrote the depends graph by hand, which repeats the order rules that hk-config now owns and drifts when a group gains a step. Hooks.all builds the 4 hooks from the groups it is given and orders their steps, so the config names go, toml, yaml, and agentskills and imports no builtin of its own. The hook defaults add a mise check and jactionlint, and the go group adds gohawk, so both tools join mise.toml. jactionlint reads the runner labels that the workflows name, so no extra label file is needed. --- hk.pkl | 69 +++++++++++-------------------------------------------- mise.lock | 43 ++++++++++++++++++++++++++++++++++ mise.toml | 2 ++ 3 files changed, 58 insertions(+), 56 deletions(-) diff --git a/hk.pkl b/hk.pkl index 8f9cd96..961dd53 100644 --- a/hk.pkl +++ b/hk.pkl @@ -1,63 +1,20 @@ +// The hook templates from nnutter/hk-config, which amend hk's configuration. +// Both package paths name the same hk-config version, and that version must +// target the hk pin in mise.toml. The revision is the newest one released for +// that hk version. amends - "package://github.com/nnutter/hk-config/releases/download/v2.11.1-0/hk-config@2.11.1-0#/Config.pkl" + "package://github.com/nnutter/hk-config/releases/download/v2.11.1-2/hk-config@2.11.1-2#/Config.pkl" -import "package://github.com/jdx/hk/releases/download/v2.11.1/hk@2.11.1#/Builtins.pkl" as Hk import - "package://github.com/nnutter/hk-config/releases/download/v2.11.1-0/hk-config@2.11.1-0#/Builtins.pkl" - as Extra + "package://github.com/nnutter/hk-config/releases/download/v2.11.1-2/hk-config@2.11.1-2#/Hooks.pkl" + as Hooks /// Fixtures hold deliberately broken files, so every step skips them. exclude = List("**/testdata/**") -/// Shared by the implicit check, fix, and pre-commit hooks. -steps { - ["constable"] = Extra.constable - ["errcheck"] = Hk.err_check - ["gitleaks"] = Hk.gitleaks - ["go_check_sumtype"] = Extra.go_check_sumtype - ["go_fix"] = Hk.go_fix - ["go_fumpt"] = Hk.go_fumpt - ["go_imports"] = Extra.go_imports - ["go_sec"] = Extra.go_sec - ["go_vet"] = Hk.go_vet - ["go_vuln_check"] = Hk.go_vuln_check - ["gomod_tidy"] = Hk.gomod_tidy - ["mdlint"] = Extra.mdlint - ["newlines"] = Hk.newlines - ["nilaway"] = Extra.nilaway - ["pkl"] = Hk.pkl - ["pkl_format"] = Hk.pkl_format - ["skills_ref"] = Extra.skills_ref - ["staticcheck"] = Hk.staticcheck - ["testifylint"] = Extra.testifylint - ["tombi"] = Hk.tombi - ["tombi_format"] = Hk.tombi_format - ["trailing_whitespace"] = Hk.trailing_whitespace - ["yamlfmt"] = Hk.yamlfmt -} - -hooks { - // A hook that hk does not create needs an explicit declaration. - ["commit-msg"] { - fix = true - steps { - ["change-id"] = Extra.timber_commit_msg - } - } - ["pre-commit"] { - fix = true - stage = true - stash = "git" - steps { - // No file filter: scan every staged change, including arbitrary types, - // binary files, deletions, and commits without Go or Markdown changes. - // The git-mode scan only sees text diffs, so staged binary bytes are - // checked out to a temporary directory and scanned explicitly. - ["gitleaks"] = (Hk.gitleaks) { - scan = "staged" - check = - "gitleaks git --redact --pre-commit --staged --verbose --no-banner && tmp=$(mktemp -d) || exit $?; trap 'rm -rf \"$tmp\"' EXIT INT TERM; git diff --cached --name-only -z --diff-filter=ACMRT > \"$tmp/list\" || exit $?; git checkout-index -z --stdin --prefix=\"$tmp/tree/\" < \"$tmp/list\" || exit $?; mkdir -p \"$tmp/tree\" || exit $?; gitleaks dir --redact --verbose --no-banner \"$tmp/tree\" || exit $?" - } - } - } -} +// The templates build the check, fix, pre-commit, and commit-msg hooks, and +// they order the steps of each hook that fixes: the steps that rewrite files +// run in the order their edits must land, and every step that only reads waits +// for the last fixer, so the readers run together. `check` sets no order. +// `go`, `toml`, `yaml`, and `agentskills` are the groups this repository needs. +hooks = Hooks.all(List("go", "toml", "yaml", "agentskills")) diff --git a/mise.lock b/mise.lock index 51d2eb9..9319174 100644 --- a/mise.lock +++ b/mise.lock @@ -300,6 +300,10 @@ url = "https://dl.google.com/go/go1.27.3.windows-amd64.zip" version = "1.30.0" backend = "go:github.com/kisielk/errcheck" +[[tools."go:github.com/kojah/gohawk"]] +version = "0.7.1" +backend = "go:github.com/kojah/gohawk" + [[tools."go:github.com/nnutter/testifylint"]] version = "0.0.0-20261101203540-577bf922d916" backend = "go:github.com/nnutter/testifylint" @@ -396,6 +400,45 @@ checksum = "sha256:60b925d9734cd6bec5fab78b9c825b39b2ad9b1c9d9f03b685423c2d21ae9 url = "https://github.com/jdx/hk/releases/download/v2.11.1/hk-x86_64-pc-windows-msvc.zip" signer = "sigstore-oidc:https://github.com/jdx/hk/.github/workflows/release.yml" +[[tools.jactionlint]] +version = "2.4.0" +backend = "packslip:github.com/jdx/jactionlint" + +[tools.jactionlint."platforms.linux-arm64"] +checksum = "sha256:42413391c248f99ff4550d6c866ee717d5ea4ca2b966d9d54ad1503af955e22f" +url = "https://github.com/jdx/jactionlint/releases/download/v2.4.0/jactionlint_2.4.0_linux_arm64.tar.gz" +signer = "sigstore-oidc:https://github.com/jdx/jactionlint/.github/workflows/release.yaml" + +[tools.jactionlint."platforms.linux-arm64-musl"] +checksum = "sha256:42413391c248f99ff4550d6c866ee717d5ea4ca2b966d9d54ad1503af955e22f" +url = "https://github.com/jdx/jactionlint/releases/download/v2.4.0/jactionlint_2.4.0_linux_arm64.tar.gz" +signer = "sigstore-oidc:https://github.com/jdx/jactionlint/.github/workflows/release.yaml" + +[tools.jactionlint."platforms.linux-x64"] +checksum = "sha256:26ddba81db28fffc51163175c070ee1a9d1150260de4b5232f01d9b749f32d8e" +url = "https://github.com/jdx/jactionlint/releases/download/v2.4.0/jactionlint_2.4.0_linux_amd64.tar.gz" +signer = "sigstore-oidc:https://github.com/jdx/jactionlint/.github/workflows/release.yaml" + +[tools.jactionlint."platforms.linux-x64-musl"] +checksum = "sha256:26ddba81db28fffc51163175c070ee1a9d1150260de4b5232f01d9b749f32d8e" +url = "https://github.com/jdx/jactionlint/releases/download/v2.4.0/jactionlint_2.4.0_linux_amd64.tar.gz" +signer = "sigstore-oidc:https://github.com/jdx/jactionlint/.github/workflows/release.yaml" + +[tools.jactionlint."platforms.macos-arm64"] +checksum = "sha256:349a13dbffdd25376898a256d195806afda08ba97ccc6378cbf2dd11a77ef8e7" +url = "https://github.com/jdx/jactionlint/releases/download/v2.4.0/jactionlint_2.4.0_darwin_arm64.tar.gz" +signer = "sigstore-oidc:https://github.com/jdx/jactionlint/.github/workflows/release.yaml" + +[tools.jactionlint."platforms.macos-x64"] +checksum = "sha256:4c21fce834f8267b65a65b32a527aaa2ec1c321c8f066d44b42d7f038835aa90" +url = "https://github.com/jdx/jactionlint/releases/download/v2.4.0/jactionlint_2.4.0_darwin_amd64.tar.gz" +signer = "sigstore-oidc:https://github.com/jdx/jactionlint/.github/workflows/release.yaml" + +[tools.jactionlint."platforms.windows-x64"] +checksum = "sha256:4e0c7b428a4faa35bb3b86af6fa65a0ac9797edcd4c64e4f7e16ca886437e172" +url = "https://github.com/jdx/jactionlint/releases/download/v2.4.0/jactionlint_2.4.0_windows_amd64.zip" +signer = "sigstore-oidc:https://github.com/jdx/jactionlint/.github/workflows/release.yaml" + [[tools.pkl]] version = "0.32.2" backend = "aqua:apple/pkl" diff --git a/mise.toml b/mise.toml index c86d3ef..7632740 100644 --- a/mise.toml +++ b/mise.toml @@ -2,6 +2,7 @@ minimum_release_age = "0" [tools] +jactionlint = "latest" "github:alecthomas/go-check-sumtype" = "latest" "github:nnutter/constable" = "latest" "github:nnutter/mdlint" = "latest" @@ -10,6 +11,7 @@ minimum_release_age = "0" gitleaks = "latest" go = "latest" "go:github.com/kisielk/errcheck" = "latest" +"go:github.com/kojah/gohawk" = "latest" "go:github.com/nnutter/testifylint" = "latest" "go:go.uber.org/nilaway/cmd/nilaway" = "latest" "go:golang.org/x/tools/cmd/goimports" = "latest"