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
8 changes: 8 additions & 0 deletions dagger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "ant-cli",
"engineVersion": "v0.20.6",
"sdk": {
"source": "go"
},
"source": "dagger"
}
4 changes: 4 additions & 0 deletions dagger/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/dagger.gen.go linguist-generated
/internal/dagger/** linguist-generated
/internal/querybuilder/** linguist-generated
/internal/telemetry/** linguist-generated
5 changes: 5 additions & 0 deletions dagger/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/dagger.gen.go
/internal/dagger
/internal/querybuilder
/internal/telemetry
/.env
400 changes: 400 additions & 0 deletions dagger/MIGRATION.md

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions dagger/factors_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"context"

"dagger/ant-cli/internal/dagger"
)

// BuildFactor compiles the project
type BuildFactor struct {
config *FactorConfig
source *dagger.Directory
}

func (f *BuildFactor) Name() string { return "build" }
func (f *BuildFactor) Dependencies() []string { return []string{"cache-warmup"} }

func (f *BuildFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
return dag.Container().
From("golang:1.25-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go").
WithEnvVariable("CGO_ENABLED", "0").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-cache")).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-cache")).
WithEnvVariable("GOCACHE", "/go/build-cache").
WithExec([]string{"go", "build", "-a", "-o", "/output/", "-ldflags=-s -w", "./..."}).
Directory("/output").
Sync(ctx)
}

// TestFactor runs the test suite
type TestFactor struct {
config *FactorConfig
source *dagger.Directory
}

func (f *TestFactor) Name() string { return "test" }
func (f *TestFactor) Dependencies() []string { return []string{"cache-warmup"} }

func (f *TestFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
return dag.Container().
From("golang:1.25-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git", "lsof"}).
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-cache")).
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-cache")).
WithExec([]string{"mkdir", "-p", "/output"}).
WithExec([]string{"sh", "-c", "go test -v -count=1 -p=4 -coverprofile=/output/coverage.out ./... > /output/test.log 2>&1; echo $? > /output/exit-code.txt; exit 0"}).
WithExec([]string{"sh", "-c", "go tool cover -html=/output/coverage.out -o /output/coverage.html 2>/dev/null || true"}).
Directory("/output").
Sync(ctx)
}

// LintFactor runs golangci-lint
type LintFactor struct {
config *FactorConfig
source *dagger.Directory
}

func (f *LintFactor) Name() string { return "lint" }
func (f *LintFactor) Dependencies() []string { return []string{"cache-warmup"} }

func (f *LintFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
return dag.Container().
From("golang:1.25-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-cache")).
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-cache")).
WithMountedCache("/root/.cache/golangci-lint", dag.CacheVolume("golangci-lint-cache")).
WithExec([]string{"go", "install", "github.com/golangci/golangci-lint/cmd/golangci-lint@latest"}).
WithExec([]string{"mkdir", "-p", "/output"}).
WithExec([]string{"golangci-lint", "run", "./...", "--out-format=json", "--issues-exit-code=0"}).
WithNewFile("/output/lint-report.json", "", dagger.ContainerWithNewFileOpts{}).
Directory("/output").
Sync(ctx)
}
35 changes: 35 additions & 0 deletions dagger/factors_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"context"

"dagger/ant-cli/internal/dagger"
)

// CacheWarmupFactor pre-warms all caches before pipeline execution
// +cache="session"
type CacheWarmupFactor struct {
config *FactorConfig
source *dagger.Directory
}

func (f *CacheWarmupFactor) Name() string { return "cache-warmup" }
func (f *CacheWarmupFactor) Dependencies() []string { return nil }

func (f *CacheWarmupFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
return dag.Container().
From("golang:1.25-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go").
WithEnvVariable("CGO_ENABLED", "0").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-cache")).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-cache")).
WithEnvVariable("GOCACHE", "/go/build-cache").
WithExec([]string{"go", "mod", "download"}).
WithExec([]string{"go", "build", "-o", "/dev/null", "./..."}).
Directory("/src").
Sync(ctx)
}
46 changes: 46 additions & 0 deletions dagger/factors_catalog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"context"
"fmt"
"strings"

"dagger/ant-cli/internal/dagger"
)

// ImageCatalogEntry represents a container image used by factors
type ImageCatalogEntry struct {
Name string
Version string
Source string
}

// ImageCatalog generates a catalog of all container images used by factors
func (m *AntCli) ImageCatalog(ctx context.Context) (*dagger.File, error) {
images := []ImageCatalogEntry{
{Name: "golang", Version: "1.23-alpine", Source: "BuildFactor, TestFactor, LintFactor, StaticAnalysisFactor, VulnScanFactor, SBOMFactor, SLSAProvenanceFactor, LicenseCheckFactor, PrivateRepoAccessFactor, CacheWarmupFactor"},
{Name: "zricethezav/gitleaks", Version: "latest", Source: "SecretScanningFactor"},
{Name: "instrumenta/conftest", Version: "latest", Source: "PolicyCheckFactor"},
{Name: "goreleaser/goreleaser", Version: "v2.15.2", Source: "GoReleaserFactor"},
{Name: "alpine", Version: "3.19", Source: "ReleaseVerificationFactor"},
{Name: "ghcr.io/sigstore/cosign/cosign", Version: "v2.2.3", Source: "CosignSign"},
}

var catalog strings.Builder
catalog.WriteString("# Dagger Container Image Catalog\n\n")
catalog.WriteString("## Images\n\n")

for _, img := range images {
catalog.WriteString(fmt.Sprintf("- **%s:%s** - Used by: %s\n", img.Name, img.Version, img.Source))
}

catalog.WriteString("\n## Version Policy\n\n")
catalog.WriteString("Versions must be pinned. Current exceptions to fix:\n")
catalog.WriteString("- zricethezav/gitleaks:latest\n")
catalog.WriteString("- instrumenta/conftest:latest\n")

return dag.Directory().
WithNewFile("/catalog/images.md", catalog.String(), dagger.DirectoryWithNewFileOpts{}).
File("/catalog/images.md").
Sync(ctx)
}
166 changes: 166 additions & 0 deletions dagger/factors_cicd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package main

import (
"context"
"fmt"

"dagger/ant-cli/internal/dagger"
)

// GoReleaserFactor runs GoReleaser
type GoReleaserFactor struct {
config *FactorConfig
source *dagger.Directory
mode string // "snapshot" or "release"
}

func (f *GoReleaserFactor) Name() string { return "goreleaser" }
func (f *GoReleaserFactor) Dependencies() []string { return []string{"build"} }

func (f *GoReleaserFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
args := "release --clean"
if f.mode == "snapshot" {
args = "release --snapshot --clean --skip=publish"
}

return dag.Container().
From("goreleaser/goreleaser:v2.15.2").
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-cache")).
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-cache")).
WithMountedCache("/root/.cache/goreleaser", dag.CacheVolume("goreleaser-cache")).
WithExec([]string{"/goreleaser", args}).
Directory("/dist").
Sync(ctx)
}

// CrossPlatformBuildFactor builds for all target platforms in parallel
type CrossPlatformBuildFactor struct {
config *FactorConfig
source *dagger.Directory
}

func (f *CrossPlatformBuildFactor) Name() string { return "cross-platform-build" }
func (f *CrossPlatformBuildFactor) Dependencies() []string { return []string{"cache-warmup"} }

func (f *CrossPlatformBuildFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
output := dag.Directory()
platforms := []struct{ os, arch string }{
{"linux", "amd64"},
{"linux", "arm64"},
{"darwin", "amd64"},
{"darwin", "arm64"},
{"windows", "amd64"},
{"windows", "arm64"},
}

type buildResult struct {
platform string
binary *dagger.File
err error
}
results := make(chan buildResult, len(platforms))

for _, plat := range platforms {
go func(p struct{ os, arch string }) {
binary, err := dag.Container().
From("golang:1.25-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go").
WithEnvVariable("CGO_ENABLED", "0").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-cache")).
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-cache")).
WithEnvVariable("GOOS", p.os).
WithEnvVariable("GOARCH", p.arch).
WithExec([]string{
"go", "build",
"-o", fmt.Sprintf("/output/anthropic-cli-%s-%s", p.os, p.arch),
"-ldflags=-s -w -X main.Version=$(git describe --tags --always --dirty)",
"./cmd/ant",
}).
File(fmt.Sprintf("/output/anthropic-cli-%s-%s", p.os, p.arch)).
Sync(ctx)
results <- buildResult{
platform: fmt.Sprintf("%s-%s", p.os, p.arch),
binary: binary,
err: err,
}
}(plat)
}

for i := 0; i < len(platforms); i++ {
result := <-results
if result.err != nil {
return nil, fmt.Errorf("build for %s failed: %w", result.platform, result.err)
}
output = output.WithFile(result.platform, result.binary)
}

return output.Sync(ctx)
}

// ReleaseVerificationFactor verifies tag is on main's first-parent history
type ReleaseVerificationFactor struct {
config *FactorConfig
source *dagger.Directory
tag string
sha string
}

func (f *ReleaseVerificationFactor) Name() string { return "release-verification" }
func (f *ReleaseVerificationFactor) Dependencies() []string { return nil }

func (f *ReleaseVerificationFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
return dag.Container().
From("alpine:3.19").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithExec([]string{"git", "fetch", "origin", "main"}).
WithExec([]string{"sh", "-c", fmt.Sprintf(
"if ! git rev-list --first-parent origin/main | grep -qxF '%s'; then echo 'FAIL: %s not on main first-parent' > /output/verification.txt; exit 1; fi",
f.sha, f.sha,
)}).
WithNewFile("/output/verification.txt", "PASS: Tag is on main's first-parent history", dagger.ContainerWithNewFileOpts{}).
Directory("/output").
Sync(ctx)
}

// PrivateRepoAccessFactor configures access to private Go modules
type PrivateRepoAccessFactor struct {
config *FactorConfig
source *dagger.Directory
accessToken string
stainlessKey string
}

func (f *PrivateRepoAccessFactor) Name() string { return "private-repo-access" }
func (f *PrivateRepoAccessFactor) Dependencies() []string { return nil }

func (f *PrivateRepoAccessFactor) Execute(ctx context.Context, state *FactorState) (*dagger.Directory, error) {
container := dag.Container().
From("golang:1.25-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
WithMountedDirectory("/src", f.source).
WithWorkdir("/src").
WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-cache")).
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-cache"))

if f.accessToken != "" {
container = container.WithExec([]string{"sh", "-c", fmt.Sprintf(
"git config --global url.\"https://x-access-token:%s@github.com/stainless-sdks/anthropic-go\".insteadOf \"https://github.com/stainless-sdks/anthropic-go\"",
f.accessToken,
)})
}

return container.
WithNewFile("/output/git-config.txt", "Private repo access configured", dagger.ContainerWithNewFileOpts{}).
Directory("/output").
Sync(ctx)
}
Loading