Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,19 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.25.x'
go-version: '1.27.x'
cache: false

- run: go test -race ./...
- run: go vet ./...

- name: Staticcheck pin and toolchain compatibility
run: go run ./cmd/check-staticcheck

- name: Static analysis
run: |
set -euo pipefail
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...

- run: go build ./cmd/agent-runtime

Expand All @@ -70,7 +73,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.25.x'
go-version: '1.27.x'
cache: false
- run: go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...

Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ belongs in a different repository.
```
go test -race ./...
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go run ./cmd/check-staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...
go build ./cmd/agent-runtime
go run ./cmd/check-fuzz
go run ./cmd/check-cold-compile
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ contract.

## [Unreleased]

- Raise Staticcheck to v0.8.1 and probe it against the local Go toolchain so the gate can analyze Go 1.27 export data.

## [0.1.4] - 2026-09-15

- Pin reusable workflows to current module mains.
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Development requires Go 1.25 or newer. Before submitting a pull request, run:
go test -race ./...
go run ./cmd/check-fuzz
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go run ./cmd/check-staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...
go build ./cmd/agent-runtime
go run ./cmd/check-fuzz
```
Expand Down
19 changes: 19 additions & 0 deletions cmd/check-staticcheck/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-only

package main

import (
"context"
"fmt"
"os"

"github.com/NDDev-OpenNetwork/agent-runtime/internal/staticcheckcompat"
)

func main() {
if err := staticcheckcompat.Verify(context.Background(), ".", os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "staticcheck compatibility failed:", err)
os.Exit(1)
}
fmt.Printf("staticcheck %s pin and toolchain probe succeeded\n", staticcheckcompat.Version)
}
7 changes: 4 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ and is not claimed by v0.

The vulnerability scanner and the linter are pinned at the point they are
invoked in `.github/workflows/ci.yml`, so there is one place to read and one
place to change. CI disables automatic toolchain
downloads with `GOTOOLCHAIN=local` and records both tool versions in the job
summary.
place to change. `cmd/check-staticcheck` refuses a drifted pin or a local Go
toolchain whose export data the pinned Staticcheck cannot decode. CI disables
automatic toolchain downloads with `GOTOOLCHAIN=local` and records both tool
versions in the job summary.

Closure is structurally distinct from an ordinary phase receipt. It records the
achieved outcome, cleanup, typed remaining debt/risks, and canonical next work.
Expand Down
132 changes: 132 additions & 0 deletions internal/staticcheckcompat/compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: AGPL-3.0-only

// Package staticcheckcompat pins Staticcheck and refuses a toolchain it cannot analyze.
package staticcheckcompat

import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)

const Version = "v0.8.1"

const Module = "honnef.co/go/tools/cmd/staticcheck"

var ContractFiles = []string{
"AGENTS.md",
"CONTRIBUTING.md",
".github/workflows/ci.yml",
}

type invocation struct {
path string
args []string
dir string
env []string
stdout io.Writer
stderr io.Writer
}

type commandRunner interface {
run(context.Context, invocation) error
}

type osCommandRunner struct{}

func (osCommandRunner) run(ctx context.Context, call invocation) error {
command := exec.CommandContext(ctx, call.path, call.args...)
command.Dir, command.Env = call.dir, call.env
command.Stdout, command.Stderr = call.stdout, call.stderr
return command.Run()
}

func VerifyPins(root string) error {
expected := Module + "@" + Version
for _, relative := range ContractFiles {
path := filepath.Join(root, relative)
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", relative, err)
}
text := string(data)
if !strings.Contains(text, expected) {
return fmt.Errorf("%s does not pin %s", relative, expected)
}
for _, line := range strings.Split(text, "\n") {
index := strings.Index(line, Module+"@")
if index < 0 {
continue
}
got := strings.Fields(line[index:])[0]
if got != expected {
return fmt.Errorf("%s pins %s, want %s", relative, got, expected)
}
}
}
return nil
}

func Verify(ctx context.Context, root string, stdout, stderr io.Writer) error {
return verify(ctx, root, stdout, stderr, osCommandRunner{})
}

func verify(ctx context.Context, root string, stdout, stderr io.Writer, runner commandRunner) error {
if err := VerifyPins(root); err != nil {
return err
}
env := localToolchain(os.Environ())
versionOut, versionErr := &bytes.Buffer{}, &bytes.Buffer{}
if err := runner.run(ctx, invocation{
path: "go",
args: []string{"run", Module + "@" + Version, "-debug.version"},
dir: root,
env: env,
stdout: io.MultiWriter(stdout, versionOut),
stderr: io.MultiWriter(stderr, versionErr),
}); err != nil {
return fmt.Errorf("staticcheck %s -debug.version: %w", Version, err)
}
if err := compatibilityError(versionOut.String() + versionErr.String()); err != nil {
return err
}
analyzeOut, analyzeErr := &bytes.Buffer{}, &bytes.Buffer{}
if err := runner.run(ctx, invocation{
path: "go",
args: []string{"run", Module + "@" + Version, "."},
dir: root,
env: env,
stdout: io.MultiWriter(stdout, analyzeOut),
stderr: io.MultiWriter(stderr, analyzeErr),
}); err != nil {
combined := analyzeOut.String() + analyzeErr.String()
if compat := compatibilityError(combined); compat != nil {
return compat
}
return fmt.Errorf("staticcheck %s compatibility probe: %w", Version, err)
}
return compatibilityError(analyzeOut.String() + analyzeErr.String())
}

func localToolchain(env []string) []string {
result := make([]string, 0, len(env)+1)
for _, entry := range env {
if strings.HasPrefix(entry, "GOTOOLCHAIN=") {
continue
}
result = append(result, entry)
}
return append(result, "GOTOOLCHAIN=local")
}

func compatibilityError(output string) error {
if strings.Contains(output, "export data version") {
return fmt.Errorf("staticcheck %s cannot decode this Go toolchain's export data; the pinned analyzer is older than the local compiler", Version)
}
return nil
}
122 changes: 122 additions & 0 deletions internal/staticcheckcompat/compat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: AGPL-3.0-only

package staticcheckcompat

import (
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
)

type runnerFunc func(context.Context, invocation) error

func (fn runnerFunc) run(ctx context.Context, call invocation) error { return fn(ctx, call) }

func TestVerifyPinsMatchesRepositoryContracts(t *testing.T) {
t.Parallel()
root := filepath.Join("..", "..")
if err := VerifyPins(root); err != nil {
t.Fatal(err)
}
}

func TestVerifyPinsFailsClosedOnMissingOrDriftedPin(t *testing.T) {
t.Parallel()
root := t.TempDir()
writeContract(t, root, "AGENTS.md", "go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...\n")
writeContract(t, root, "CONTRIBUTING.md", "go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...\n")
if err := os.MkdirAll(filepath.Join(root, ".github", "workflows"), 0o755); err != nil {
t.Fatal(err)
}
if err := VerifyPins(root); err == nil {
t.Fatal("missing workflow pin accepted")
}
writeContract(t, root, ".github/workflows/ci.yml", "go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...\n")
if err := VerifyPins(root); err == nil {
t.Fatal("drifted workflow pin accepted")
}
writeContract(t, root, ".github/workflows/ci.yml", "go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...\n")
if err := VerifyPins(root); err != nil {
t.Fatal(err)
}
}

func TestVerifyProbesPinnedStaticcheckWithLocalToolchain(t *testing.T) {
t.Parallel()
root := contractRoot(t)
var calls []invocation
runner := runnerFunc(func(_ context.Context, call invocation) error {
calls = append(calls, call)
return nil
})
var stdout, stderr bytes.Buffer
if err := verify(context.Background(), root, &stdout, &stderr, runner); err != nil {
t.Fatal(err)
}
if len(calls) != 2 {
t.Fatalf("calls=%d", len(calls))
}
if calls[0].path != "go" || strings.Join(calls[0].args, " ") != "run honnef.co/go/tools/cmd/staticcheck@v0.8.1 -debug.version" {
t.Fatalf("version call=%#v", calls[0])
}
if calls[1].path != "go" || strings.Join(calls[1].args, " ") != "run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ." {
t.Fatalf("probe call=%#v", calls[1])
}
for index, call := range calls {
if call.dir != root || call.stdout == nil || call.stderr == nil || count(call.env, "GOTOOLCHAIN=local") != 1 {
t.Fatalf("call %d lost directory, output, or toolchain boundary", index)
}
}
}

func TestVerifyFailsClosedOnExportDataIncompatibility(t *testing.T) {
t.Parallel()
root := contractRoot(t)
err := verify(context.Background(), root, &bytes.Buffer{}, &bytes.Buffer{}, runnerFunc(func(_ context.Context, call invocation) error {
if strings.Contains(strings.Join(call.args, " "), "-debug.version") {
return nil
}
_, _ = io.WriteString(call.stderr, "cannot decode \"internal/byteorder\", export data version 4 is greater than maximum supported version 2\n")
return errors.New("exit status 1")
}))
if err == nil || !strings.Contains(err.Error(), "cannot decode this Go toolchain's export data") {
t.Fatalf("incompatibility not reported: %v", err)
}
}

func writeContract(t *testing.T, root, relative, body string) {
t.Helper()
path := filepath.Join(root, relative)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}

func contractRoot(t *testing.T) string {
t.Helper()
root := t.TempDir()
writeContract(t, root, "AGENTS.md", "go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...\n")
writeContract(t, root, "CONTRIBUTING.md", "go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...\n")
writeContract(t, root, ".github/workflows/ci.yml", "go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...\n")
return root
}

func count(values []string, want string) int {
found := 0
for _, value := range values {
if value == want {
found++
}
}
return found
}