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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: "1.26.0"
go-version: "1.26.1"
cache: true

- name: Lint
Expand All @@ -48,7 +48,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: "1.26.0"
go-version: "1.26.1"
cache: true

- name: Install Task
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: "1.26.0"
go-version: "1.26.1"
cache: true

- name: Install go-licences
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ evals
.vscode
.idea/
*.debug
/lint/lint

# agents
agent.yaml
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1

ARG GO_VERSION="1.26.0"
ARG GO_VERSION="1.26.1"
ARG ALPINE_VERSION="3.22"
ARG XX_VERSION="1.9.0"

Expand Down
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ tasks:
desc: Run golangci-lint
cmds:
- golangci-lint run
- go run ./lint .
- go mod tidy --diff >/dev/null || (echo "go.mod/go.sum files are not tidy" && exit 1)
sources:
- "{{.GO_SOURCES}}"
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/docker/docker-agent

go 1.26.0
go 1.26.1

require (
charm.land/bubbles/v2 v2.0.0
Expand Down Expand Up @@ -128,6 +128,7 @@ require (
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgageot/rubocop-go v0.0.0-20260323134452-aecdd6345645
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgageot/rubocop-go v0.0.0-20260323134452-aecdd6345645 h1:7UgEWAo69Dgbtii1j1FLWE88+Rem9Qly4LLrrQhAN0s=
github.com/dgageot/rubocop-go v0.0.0-20260323134452-aecdd6345645/go.mod h1:r8YOJV5+/30NZ8HW/2NbWUObBGDXGvfHrjgury5YlFI=
github.com/dgageot/ultraviolet v0.0.0-20260313154905-9451997d56b6 h1:88fWkkjwzuI4tRTqadbJIbA9O+gO67oyu+2OpHHuuT8=
github.com/dgageot/ultraviolet v0.0.0-20260313154905-9451997d56b6/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
Expand Down
123 changes: 123 additions & 0 deletions lint/config_version_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package main

import (
"fmt"
"go/ast"
"go/token"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/dgageot/rubocop-go/cop"
)

// ConfigVersionImport enforces that config version packages (pkg/config/vN)
// only import their immediate predecessor (pkg/config/v{N-1}) and the shared
// types package (pkg/config/types). This preserves the strict migration chain:
// v0 → v1 → v2 → … → latest.
type ConfigVersionImport struct{}

func (*ConfigVersionImport) Name() string { return "Lint/ConfigVersionImport" }
func (*ConfigVersionImport) Description() string {
return "Config version packages must only import their immediate predecessor"
}
func (*ConfigVersionImport) Severity() cop.Severity { return cop.Error }

// configVersionRe matches "pkg/config/vN" at the end of an import path.
var configVersionRe = regexp.MustCompile(`pkg/config/v(\d+)$`)

// Check inspects import declarations in config version packages.
func (c *ConfigVersionImport) Check(fset *token.FileSet, file *ast.File) []cop.Offense {
if len(file.Imports) == 0 {
return nil
}

// Determine which config version package this file belongs to.
filename := fset.Position(file.Package).Filename
dirVersion, isVersioned := extractDirVersion(filename)
dirIsLatest := isLatestDir(filename)

if !isVersioned && !dirIsLatest {
return nil
}

var offenses []cop.Offense

for _, imp := range file.Imports {
importPath := strings.Trim(imp.Path.Value, `"`)

if !strings.Contains(importPath, "pkg/config/") {
continue
}

if strings.HasSuffix(importPath, "pkg/config/types") {
continue
}

if isVersioned {
offenses = append(offenses, c.checkVersionedImport(fset, imp, importPath, dirVersion)...)
} else if dirIsLatest {
offenses = append(offenses, c.checkLatestImport(fset, imp, importPath)...)
}
}

return offenses
}

func (c *ConfigVersionImport) checkVersionedImport(fset *token.FileSet, imp *ast.ImportSpec, importPath string, dirVersion int) []cop.Offense {
if strings.HasSuffix(importPath, "pkg/config/latest") {
return []cop.Offense{cop.NewOffense(c, fset, imp.Path.Pos(), imp.Path.End(),
fmt.Sprintf("config v%d must not import pkg/config/latest", dirVersion))}
}

m := configVersionRe.FindStringSubmatch(importPath)
if m == nil {
return nil
}

importedVersion, _ := strconv.Atoi(m[1])
expected := dirVersion - 1

if expected < 0 {
return []cop.Offense{cop.NewOffense(c, fset, imp.Path.Pos(), imp.Path.End(),
"config v0 must not import other config version packages")}
}

if importedVersion != expected {
return []cop.Offense{cop.NewOffense(c, fset, imp.Path.Pos(), imp.Path.End(),
fmt.Sprintf("config v%d must import v%d (its predecessor), not v%d", dirVersion, expected, importedVersion))}
}

return nil
}

func (c *ConfigVersionImport) checkLatestImport(fset *token.FileSet, imp *ast.ImportSpec, importPath string) []cop.Offense {
if configVersionRe.MatchString(importPath) {
return nil
}

return []cop.Offense{cop.NewOffense(c, fset, imp.Path.Pos(), imp.Path.End(),
"pkg/config/latest should only import config version or types packages, not "+importPath)}
}

func extractDirVersion(filename string) (int, bool) {
normalized := filepath.ToSlash(filename)

re := regexp.MustCompile(`/pkg/config/v(\d+)/`)
m := re.FindStringSubmatch(normalized)
if m == nil {
return 0, false
}

v, err := strconv.Atoi(m[1])
if err != nil {
return 0, false
}
return v, true
}

func isLatestDir(filename string) bool {
normalized := filepath.ToSlash(filename)
return strings.Contains(normalized, "/pkg/config/latest/")
}
37 changes: 37 additions & 0 deletions lint/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Package main runs project-specific linting cops using rubocop-go.
//
// Usage: go run ./lint ./...
package main

import (
"fmt"
"os"

"github.com/dgageot/rubocop-go/config"
"github.com/dgageot/rubocop-go/cop"
"github.com/dgageot/rubocop-go/runner"
)

func main() {
cop.Register(&ConfigVersionImport{})
cops := cop.All()
fmt.Printf("Inspecting Go files with %d cop(s)\n", len(cops))

cfg := config.DefaultConfig()
r := runner.New(cops, cfg, os.Stdout)

paths := os.Args[1:]
if len(paths) == 0 {
paths = []string{"."}
}

offenseCount, err := r.Run(paths)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if offenseCount > 0 {
os.Exit(1)
}
}
Loading