diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml index 743ca3b..cab6b5d 100644 --- a/.github/workflows/ci-cd.yaml +++ b/.github/workflows/ci-cd.yaml @@ -34,11 +34,12 @@ jobs: with: version: latest args: --timeout=5m + install-mode: goinstall - name: Security Scan (gosec) - uses: securego/gosec@master - with: - args: ./... + run: | + go install github.com/securego/gosec/v2/cmd/gosec@latest + gosec ./... - name: Dependency Check (govulncheck) run: | diff --git a/Dockerfile b/Dockerfile index a19c37b..c3654a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM golang:alpine AS builder +FROM golang:1.26.1-alpine AS builder WORKDIR /app # Download dependencies @@ -15,8 +15,8 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o gitpoll ./cmd/gitpoll # Final stage FROM alpine:latest -# Install necessary runtime dependencies (git, bash, openssh) -RUN apk add --no-cache git bash openssh +# Install necessary runtime dependencies (bash, openssh) +RUN apk add --no-cache bash openssh WORKDIR /app diff --git a/go.mod b/go.mod index 38dc531..2316440 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module repo-gitpoll -go 1.25.7 +go 1.26.1 require ( github.com/charmbracelet/bubbles v1.0.0 diff --git a/internal/poller/poller.go b/internal/poller/poller.go index b25e5c8..58d5ac8 100644 --- a/internal/poller/poller.go +++ b/internal/poller/poller.go @@ -3,9 +3,15 @@ package poller import ( "context" "math/rand" - "strings" + "os" + "path/filepath" "time" - "os/exec" + + gogit "github.com/go-git/go-git/v5" + gitconfig "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/go-git/go-git/v5/storage/memory" "repo-gitpoll/internal/config" "repo-gitpoll/internal/events" @@ -16,20 +22,58 @@ type GitClient interface { LsRemote(ctx context.Context, repoURL, branch string) (string, error) } -// defaultGitClient implements GitClient using os/exec +// defaultGitClient implements GitClient using go-git type defaultGitClient struct{} func (c *defaultGitClient) LsRemote(ctx context.Context, repoURL, branch string) (string, error) { - // #nosec G204 - command relies on variables but is explicitly internal to the background worker configuration - cmd := exec.CommandContext(ctx, "git", "ls-remote", repoURL, branch) - out, err := cmd.Output() + rem := gogit.NewRemote(memory.NewStorage(), &gitconfig.RemoteConfig{ + Name: "origin", + URLs: []string{repoURL}, + }) + + listOpts := &gogit.ListOptions{} + + if ep, err := transport.NewEndpoint(repoURL); err == nil && ep.Protocol == "ssh" { + user := ep.User + if user == "" { + user = "git" + } + + // Try ssh-agent first + auth, authErr := ssh.DefaultAuthBuilder(user) + + // Fallback to local keys if agent is not available + if authErr != nil { + if homeDir, err := os.UserHomeDir(); err == nil { + keys := []string{"id_ed25519", "id_rsa", "id_ecdsa", "id_dsa"} + for _, key := range keys { + keyPath := filepath.Join(homeDir, ".ssh", key) + if _, err := os.Stat(keyPath); err == nil { + if pkAuth, err := ssh.NewPublicKeysFromFile(user, keyPath, ""); err == nil { + auth = pkAuth + break + } + } + } + } + } + + if auth != nil { + listOpts.Auth = auth + } + } + + refs, err := rem.ListContext(ctx, listOpts) if err != nil { return "", err } - parts := strings.Fields(string(out)) - if len(parts) > 0 { - return parts[0], nil + + for _, ref := range refs { + if ref.Name().Short() == branch { + return ref.Hash().String(), nil + } } + return "", nil }