Skip to content
Open
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
32 changes: 32 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Lint

on:
pull_request:
paths:
- 'services/api/**/*.go'
- 'services/api/go.mod'
- 'services/api/go.sum'
- 'services/api/.golangci.yml'
push:
branches:
- main
paths:
- 'services/api/**/*.go'

permissions:
contents: read

jobs:
golangci:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25'
cache-dependency-path: services/api/go.sum
- uses: golangci/golangci-lint-action@v9
with:
version: v2.12
working-directory: services/api
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.PHONY: lint lint-fix fmt

lint:
cd services/api && golangci-lint run ./...

lint-fix:
cd services/api && golangci-lint run --fix ./...

fmt:
cd services/api && golangci-lint fmt ./...
42 changes: 42 additions & 0 deletions services/api/.golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
version: "2"

run:
timeout: 5m
tests: true

linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- misspell
- unparam
- gocritic
- gosec
- bodyclose
- nilerr
settings:
errcheck:
check-blank: true
gosec:
excludes:
- G124
- G304
exclusions:
rules:
- path: '_test\.go'
linters:
- gosec
- unparam
- unused

formatters:
enable:
- gofmt
- goimports

issues:
max-issues-per-linter: 0
max-same-issues: 0
2 changes: 1 addition & 1 deletion services/api/cmd/agentforge-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func run() error {
if err != nil {
return err
}
defer database.Close()
defer database.Close() //nolint:errcheck // deferred close

migrationsDir, err := resolveMigrationsDir()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/agents/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func (r *Repository) list(ctx context.Context, db queryer, ownerUserID string) (
if err != nil {
return nil, err
}
defer rows.Close()
defer rows.Close() //nolint:errcheck // deferred close

var agents []Agent
for rows.Next() {
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/auth/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func (r *Repository) FindUserByEmail(ctx context.Context, email string) (User, e
if err != nil {
return User{}, err
}
defer rows.Close()
defer rows.Close() //nolint:errcheck // deferred close

var matches []User
for rows.Next() {
Expand Down
16 changes: 8 additions & 8 deletions services/api/internal/channels/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ var (
)

type Channel struct {
ID string `json:"id"`
AgentID string `json:"agentId"`
ChannelType Type `json:"channelType"`
Status Status `json:"status"`
ID string `json:"id"`
AgentID string `json:"agentId"`
ChannelType Type `json:"channelType"`
Status Status `json:"status"`
ExternalAccountID string `json:"externalAccountId"`
LastErrorCode string `json:"lastErrorCode"`
LastErrorMessage string `json:"lastErrorMessage"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
LastErrorCode string `json:"lastErrorCode"`
LastErrorMessage string `json:"lastErrorMessage"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

type PairingSession struct {
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/channels/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (r *Repository) ListPairingSessions(ctx context.Context, channelID string)
if err != nil {
return nil, err
}
defer rows.Close()
defer rows.Close() //nolint:errcheck // deferred close
var sessions []PairingSession
for rows.Next() {
var session PairingSession
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func readDotEnv(path string) (map[string]string, error) {
}
return nil, err
}
defer file.Close()
defer file.Close() //nolint:errcheck // deferred close

values := map[string]string{}
scanner := bufio.NewScanner(file)
Expand Down
6 changes: 3 additions & 3 deletions services/api/internal/db/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestOpenAppliesSQLitePragmas(t *testing.T) {
if err != nil {
t.Fatalf("Open() error = %v", err)
}
defer database.Close()
defer database.Close() //nolint:errcheck // deferred close

var journalMode string
if err := database.QueryRowContext(ctx, "PRAGMA journal_mode;").Scan(&journalMode); err != nil {
Expand Down Expand Up @@ -48,7 +48,7 @@ func TestMigrateIsIdempotentAndEnforcesForeignKeys(t *testing.T) {
if err != nil {
t.Fatalf("Open() error = %v", err)
}
defer database.Close()
defer database.Close() //nolint:errcheck // deferred close

migrationsDir := filepath.Join("..", "..", "migrations")
if err := Migrate(ctx, database, migrationsDir); err != nil {
Expand All @@ -73,7 +73,7 @@ func TestMigrateEnforcesSpecStatusConstraints(t *testing.T) {
if err != nil {
t.Fatalf("Open() error = %v", err)
}
defer database.Close()
defer database.Close() //nolint:errcheck // deferred close

if err := Migrate(ctx, database, filepath.Join("..", "..", "migrations")); err != nil {
t.Fatalf("Migrate() error = %v", err)
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/db/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func applyMigration(ctx context.Context, database *sql.DB, migrationsDir, file s
if err != nil {
return err
}
defer tx.Rollback()
defer tx.Rollback() //nolint:errcheck // rollback on error path

if _, err := tx.ExecContext(ctx, string(sqlBytes)); err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions services/api/internal/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
)

// errorWithStack 包装错误并携带堆栈信息
//
//nolint:errname // internal helper type
type errorWithStack struct {
Err error
Stack []byte
Expand Down
1 change: 1 addition & 0 deletions services/api/internal/http/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func (e *requestError) Unwrap() error {

// errorWithStack 包装错误并携带堆栈信息
type errorWithStack struct {
//nolint:errname // internal helper type
Err error
Stack []byte
}
Expand Down
5 changes: 2 additions & 3 deletions services/api/internal/http/template_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func (h *TemplateHandlers) AddSkill(c *gin.Context) {
writeErrorWithMsg(c, http.StatusBadRequest, "invalid_request", "missing or invalid 'file' field: "+err.Error())
return
}
defer file.Close()
defer file.Close() //nolint:errcheck // deferred close
archive, err := io.ReadAll(file)
if err != nil {
writeErrorWithMsg(c, http.StatusBadRequest, "invalid_request", "failed to read file: "+err.Error())
Expand Down Expand Up @@ -401,7 +401,6 @@ func requireAdminUser(c *gin.Context) (auth.User, bool) {
return user, true
}


func readMultipartSkillArchive(header *multipart.FileHeader) (templates.SkillArchive, error) {
if header == nil {
return templates.SkillArchive{}, fmt.Errorf("%w: missing file header", templates.ErrInvalidInput)
Expand All @@ -410,7 +409,7 @@ func readMultipartSkillArchive(header *multipart.FileHeader) (templates.SkillArc
if err != nil {
return templates.SkillArchive{}, fmt.Errorf("%w: failed to open skill file: %v", templates.ErrInvalidInput, err)
}
defer file.Close()
defer file.Close() //nolint:errcheck // deferred close
content, err := io.ReadAll(file)
if err != nil {
return templates.SkillArchive{}, fmt.Errorf("%w: failed to read skill file: %v", templates.ErrInvalidInput, err)
Expand Down
10 changes: 5 additions & 5 deletions services/api/internal/http/weixin_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@ type channelDTO struct {
}

type pairingSessionDTO struct {
ID string `json:"id"`
Status channels.PairingStatus `json:"status"`
QRPayload string `json:"qrPayload,omitempty"`
ID string `json:"id"`
Status channels.PairingStatus `json:"status"`
QRPayload string `json:"qrPayload,omitempty"`
// QRPayloadURL is the scannable liteapp URL (e.g.
// https://liteapp.weixin.qq.com/q/...). The frontend must encode it
// into a QR image client-side; it is plain text, not image data.
QRPayloadURL string `json:"qrPayloadUrl,omitempty"`
ExpiresAt string `json:"expiresAt"`
QRPayloadURL string `json:"qrPayloadUrl,omitempty"`
ExpiresAt string `json:"expiresAt"`
}

func newChannelDTO(channel channels.Channel) channelDTO {
Expand Down
8 changes: 4 additions & 4 deletions services/api/internal/jobs/channel_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ func (r *ChannelRepository) CreateOrReuseConnectJob(ctx context.Context, channel
job, err := r.getActiveJobByChannel(ctx, channelID)
if errors.Is(err, ErrNotFound) {
job, err = r.CreateQueued(ctx, ChannelJob{
AgentChannelID: channelID,
AgentChannelID: channelID,
PairingSessionID: &session.ID,
Type: TypeConnectWeixin,
Type: TypeConnectWeixin,
})
return session, job, true, err
}
Expand All @@ -174,9 +174,9 @@ func (r *ChannelRepository) CreateOrReuseConnectJob(ctx context.Context, channel
return channels.PairingSession{}, ChannelJob{}, false, err
}
job, err := r.CreateQueued(ctx, ChannelJob{
AgentChannelID: channelID,
AgentChannelID: channelID,
PairingSessionID: &session.ID,
Type: TypeConnectWeixin,
Type: TypeConnectWeixin,
})
return session, job, true, err
}
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/jobs/runtime_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (r *RuntimeRepository) ListByAgent(ctx context.Context, agentID string) ([]
if err != nil {
return nil, err
}
defer rows.Close()
defer rows.Close() //nolint:errcheck // deferred close

var runtimeJobs []RuntimeJob
for rows.Next() {
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/jobs/runtime_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func (w *RuntimeWorker) processProvisionAgent(ctx context.Context, job runtimeJo
CPUs: w.hermesCPUs,
}); err != nil {
slog.Error("ECI/container start failed", "error", err, "agentID", agent.ID)
return w.failProvision(ctx, job, agent, runtime.ErrCodeContainerStartFailed, err.Error())
return w.failProvision(ctx, job, agent, runtime.ErrCodeContainerStartFailed, err.Error())
}

if err := w.transitionAgent(ctx, agent.ID, agent.Status, agentStatusRunning, runtimeID, "", ""); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/jobs/runtime_worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ func listRuntimeEvents(t *testing.T, database *sql.DB, agentID string) []string
if err != nil {
t.Fatalf("query runtime events: %v", err)
}
defer rows.Close()
defer rows.Close() //nolint:errcheck // deferred close
var events []string
for rows.Next() {
var eventType string
Expand Down
2 changes: 1 addition & 1 deletion services/api/internal/runtime/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type ContainerStatus struct {
}

type dockerRunner struct {
dockerBin string
dockerBin string
agentsVolume string // Docker named volume for /data/agents
}

Expand Down
12 changes: 6 additions & 6 deletions services/api/internal/runtime/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestDockerRunnerEnsureRunningBuildsExpectedCommand(t *testing.T) {
t.Fatalf("write stub docker: %v", err)
}

runner := NewDockerRunner(stubPath)
runner := NewDockerRunner(stubPath, "")
homePath := filepath.Join(workdir, "relative", "..", "hermes-home")
spec := ContainerSpec{
AgentID: "agent-internal-id",
Expand Down Expand Up @@ -71,7 +71,7 @@ func TestDockerRunnerInspectParsesRunningState(t *testing.T) {
t.Fatalf("write stub docker: %v", err)
}

runner := NewDockerRunner(stubPath)
runner := NewDockerRunner(stubPath, "")
status, err := runner.Inspect(ctx, "agentforge-hermes-agent-1")
if err != nil {
t.Fatalf("Inspect returned error: %v", err)
Expand Down Expand Up @@ -100,7 +100,7 @@ func TestDockerRunnerEnsureRunningStartsExistingStoppedContainer(t *testing.T) {
t.Fatalf("write stub docker: %v", err)
}

runner := NewDockerRunner(stubPath)
runner := NewDockerRunner(stubPath, "")
if err := runner.EnsureRunning(ctx, ContainerSpec{
AgentID: "agent-internal-id",
HermesHome: filepath.Join(workdir, "hermes-home"),
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestDockerRunnerEnsureRunningNoopsWhenContainerAlreadyRunning(t *testing.T)
t.Fatalf("write stub docker: %v", err)
}

runner := NewDockerRunner(stubPath)
runner := NewDockerRunner(stubPath, "")
if err := runner.EnsureRunning(ctx, ContainerSpec{
AgentID: "agent-internal-id",
HermesHome: filepath.Join(workdir, "hermes-home"),
Expand Down Expand Up @@ -179,7 +179,7 @@ func TestDockerRunnerRemoveReturnsNotFoundWhenContainerMissing(t *testing.T) {
if err := os.WriteFile(stubPath, []byte(script), 0o755); err != nil {
t.Fatalf("write stub docker: %v", err)
}
runner := NewDockerRunner(stubPath)
runner := NewDockerRunner(stubPath, "")

err := runner.Remove(ctx, "agentforge-hermes-missing")
if !errors.Is(err, ErrContainerNotFound) {
Expand All @@ -195,7 +195,7 @@ func TestDockerRunnerRemoveWrapsOtherErrors(t *testing.T) {
if err := os.WriteFile(stubPath, []byte(script), 0o755); err != nil {
t.Fatalf("write stub docker: %v", err)
}
runner := NewDockerRunner(stubPath)
runner := NewDockerRunner(stubPath, "")

err := runner.Remove(ctx, "agentforge-hermes-x")
if err == nil {
Expand Down
Loading
Loading