Ryan Malloy 2f82e8d2e0 Implement comprehensive Docker development environment with major performance optimizations
* Docker Infrastructure:
  - Multi-stage Dockerfile.dev with optimized Go proxy configuration
  - Complete compose.dev.yml with service orchestration
  - Fixed critical GOPROXY setting achieving 42x performance improvement
  - Migrated from Poetry to uv for faster Python package management

* Build System Enhancements:
  - Enhanced Mage build system with caching and parallelization
  - Added incremental build capabilities with SHA256 checksums
  - Implemented parallel task execution with dependency resolution
  - Added comprehensive test orchestration targets

* Testing Infrastructure:
  - Complete API testing suite with OpenAPI validation
  - Performance testing with multi-worker simulation
  - Integration testing for end-to-end workflows
  - Database testing with migration validation
  - Docker-based test environments

* Documentation:
  - Comprehensive Docker development guides
  - Performance optimization case study
  - Build system architecture documentation
  - Test infrastructure usage guides

* Performance Results:
  - Build time reduced from 60+ min failures to 9.5 min success
  - Go module downloads: 42x faster (84.2s vs 60+ min timeouts)
  - Success rate: 0% → 100%
  - Developer onboarding: days → 10 minutes

Fixes critical Docker build failures and establishes production-ready
containerized development environment with comprehensive testing.
2025-09-09 12:11:08 -06:00

450 lines
11 KiB
Go

//go:build mage
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
)
// BuildCache manages incremental build artifacts and dependency tracking
type BuildCache struct {
cacheDir string
metadataDir string
}
// BuildMetadata tracks build dependencies and outputs
type BuildMetadata struct {
Target string `json:"target"`
Sources []SourceFile `json:"sources"`
Dependencies []string `json:"dependencies"`
Outputs []string `json:"outputs"`
Environment map[string]string `json:"environment"`
Timestamp time.Time `json:"timestamp"`
Checksum string `json:"checksum"`
}
// SourceFile represents a source file with its modification time and hash
type SourceFile struct {
Path string `json:"path"`
ModTime time.Time `json:"mod_time"`
Size int64 `json:"size"`
Checksum string `json:"checksum"`
}
const (
buildCacheDir = ".build-cache"
metadataExt = ".meta.json"
)
// NewBuildCache creates a new build cache instance
func NewBuildCache() *BuildCache {
cacheDir := filepath.Join(buildCacheDir, "artifacts")
metadataDir := filepath.Join(buildCacheDir, "metadata")
// Ensure cache directories exist
os.MkdirAll(cacheDir, 0755)
os.MkdirAll(metadataDir, 0755)
return &BuildCache{
cacheDir: cacheDir,
metadataDir: metadataDir,
}
}
// NeedsBuild checks if a target needs to be rebuilt based on source changes
func (bc *BuildCache) NeedsBuild(target string, sources []string, dependencies []string, outputs []string) (bool, error) {
if mg.Verbose() {
fmt.Printf("Cache: Checking if %s needs build\n", target)
}
// Check if any output files are missing
for _, output := range outputs {
if _, err := os.Stat(output); os.IsNotExist(err) {
if mg.Verbose() {
fmt.Printf("Cache: Output %s missing, needs build\n", output)
}
return true, nil
}
}
// Load existing metadata
metadata, err := bc.loadMetadata(target)
if err != nil {
if mg.Verbose() {
fmt.Printf("Cache: No metadata for %s, needs build\n", target)
}
return true, nil // No cached data, needs build
}
// Check if dependencies have changed
if !stringSlicesEqual(metadata.Dependencies, dependencies) {
if mg.Verbose() {
fmt.Printf("Cache: Dependencies changed for %s, needs build\n", target)
}
return true, nil
}
// Check if any source files have changed
currentSources, err := bc.analyzeSourceFiles(sources)
if err != nil {
return true, err
}
if bc.sourcesChanged(metadata.Sources, currentSources) {
if mg.Verbose() {
fmt.Printf("Cache: Sources changed for %s, needs build\n", target)
}
return true, nil
}
// Check if environment has changed for critical variables
criticalEnvVars := []string{"CGO_ENABLED", "GOOS", "GOARCH", "LDFLAGS"}
for _, envVar := range criticalEnvVars {
currentValue := os.Getenv(envVar)
cachedValue, exists := metadata.Environment[envVar]
if !exists || cachedValue != currentValue {
if mg.Verbose() {
fmt.Printf("Cache: Environment variable %s changed for %s, needs build\n", envVar, target)
}
return true, nil
}
}
if mg.Verbose() {
fmt.Printf("Cache: %s is up to date\n", target)
}
return false, nil
}
// RecordBuild records successful build metadata
func (bc *BuildCache) RecordBuild(target string, sources []string, dependencies []string, outputs []string) error {
if mg.Verbose() {
fmt.Printf("Cache: Recording build metadata for %s\n", target)
}
currentSources, err := bc.analyzeSourceFiles(sources)
if err != nil {
return err
}
// Create environment snapshot
environment := make(map[string]string)
criticalEnvVars := []string{"CGO_ENABLED", "GOOS", "GOARCH", "LDFLAGS"}
for _, envVar := range criticalEnvVars {
environment[envVar] = os.Getenv(envVar)
}
// Calculate overall checksum
checksum := bc.calculateBuildChecksum(currentSources, dependencies, environment)
metadata := BuildMetadata{
Target: target,
Sources: currentSources,
Dependencies: dependencies,
Outputs: outputs,
Environment: environment,
Timestamp: time.Now(),
Checksum: checksum,
}
return bc.saveMetadata(target, &metadata)
}
// CopyToCache copies build artifacts to cache
func (bc *BuildCache) CopyToCache(target string, files []string) error {
if mg.Verbose() {
fmt.Printf("Cache: Copying artifacts for %s to cache\n", target)
}
targetDir := filepath.Join(bc.cacheDir, target)
if err := os.MkdirAll(targetDir, 0755); err != nil {
return err
}
for _, file := range files {
if _, err := os.Stat(file); os.IsNotExist(err) {
continue // Skip missing files
}
dest := filepath.Join(targetDir, filepath.Base(file))
if err := copyFile(file, dest); err != nil {
return fmt.Errorf("failed to copy %s to cache: %w", file, err)
}
}
return nil
}
// RestoreFromCache restores build artifacts from cache
func (bc *BuildCache) RestoreFromCache(target string, files []string) error {
if mg.Verbose() {
fmt.Printf("Cache: Restoring artifacts for %s from cache\n", target)
}
targetDir := filepath.Join(bc.cacheDir, target)
for _, file := range files {
source := filepath.Join(targetDir, filepath.Base(file))
if _, err := os.Stat(source); os.IsNotExist(err) {
continue // Skip missing cached files
}
if err := copyFile(source, file); err != nil {
return fmt.Errorf("failed to restore %s from cache: %w", file, err)
}
}
return nil
}
// CleanCache removes all cached artifacts and metadata
func (bc *BuildCache) CleanCache() error {
fmt.Println("Cache: Cleaning build cache")
return sh.Rm(buildCacheDir)
}
// CacheStats returns statistics about the build cache
func (bc *BuildCache) CacheStats() (map[string]interface{}, error) {
stats := make(map[string]interface{})
// Count cached targets
metadataFiles, err := filepath.Glob(filepath.Join(bc.metadataDir, "*"+metadataExt))
if err != nil {
return nil, err
}
stats["targets_cached"] = len(metadataFiles)
// Calculate cache size
var totalSize int64
err = filepath.Walk(buildCacheDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
totalSize += info.Size()
}
return nil
})
if err != nil {
return nil, err
}
stats["cache_size_bytes"] = totalSize
stats["cache_size_mb"] = totalSize / (1024 * 1024)
return stats, nil
}
// analyzeSourceFiles calculates checksums and metadata for source files
func (bc *BuildCache) analyzeSourceFiles(sources []string) ([]SourceFile, error) {
var result []SourceFile
for _, source := range sources {
// Handle glob patterns
matches, err := filepath.Glob(source)
if err != nil {
return nil, err
}
if len(matches) == 0 {
// Not a glob, treat as literal path
matches = []string{source}
}
for _, match := range matches {
info, err := os.Stat(match)
if os.IsNotExist(err) {
continue // Skip missing files
} else if err != nil {
return nil, err
}
if info.IsDir() {
// For directories, walk and include all relevant files
err = filepath.Walk(match, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// Only include source code files
if bc.isSourceFile(path) {
checksum, err := bc.calculateFileChecksum(path)
if err != nil {
return err
}
result = append(result, SourceFile{
Path: path,
ModTime: info.ModTime(),
Size: info.Size(),
Checksum: checksum,
})
}
return nil
})
if err != nil {
return nil, err
}
} else {
checksum, err := bc.calculateFileChecksum(match)
if err != nil {
return nil, err
}
result = append(result, SourceFile{
Path: match,
ModTime: info.ModTime(),
Size: info.Size(),
Checksum: checksum,
})
}
}
}
return result, nil
}
// isSourceFile determines if a file is a source code file we should track
func (bc *BuildCache) isSourceFile(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
return ext == ".go" || ext == ".js" || ext == ".ts" || ext == ".vue" ||
ext == ".py" || ext == ".yaml" || ext == ".yml" ||
filepath.Base(path) == "go.mod" || filepath.Base(path) == "go.sum" ||
filepath.Base(path) == "package.json" || filepath.Base(path) == "yarn.lock"
}
// calculateFileChecksum calculates SHA256 checksum of a file
func (bc *BuildCache) calculateFileChecksum(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// calculateBuildChecksum creates a composite checksum for the entire build
func (bc *BuildCache) calculateBuildChecksum(sources []SourceFile, dependencies []string, environment map[string]string) string {
hash := sha256.New()
// Add source file checksums
for _, source := range sources {
hash.Write([]byte(source.Path + source.Checksum))
}
// Add dependencies
for _, dep := range dependencies {
hash.Write([]byte(dep))
}
// Add environment variables
for key, value := range environment {
hash.Write([]byte(key + "=" + value))
}
return hex.EncodeToString(hash.Sum(nil))
}
// sourcesChanged checks if source files have changed compared to cached data
func (bc *BuildCache) sourcesChanged(cached []SourceFile, current []SourceFile) bool {
if len(cached) != len(current) {
return true
}
// Create lookup maps
cachedMap := make(map[string]SourceFile)
for _, file := range cached {
cachedMap[file.Path] = file
}
for _, currentFile := range current {
cachedFile, exists := cachedMap[currentFile.Path]
if !exists {
return true // New file
}
if cachedFile.Checksum != currentFile.Checksum {
return true // File changed
}
}
return false
}
// loadMetadata loads build metadata from disk
func (bc *BuildCache) loadMetadata(target string) (*BuildMetadata, error) {
metaPath := filepath.Join(bc.metadataDir, target+metadataExt)
data, err := os.ReadFile(metaPath)
if err != nil {
return nil, err
}
var metadata BuildMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
return nil, err
}
return &metadata, nil
}
// saveMetadata saves build metadata to disk
func (bc *BuildCache) saveMetadata(target string, metadata *BuildMetadata) error {
metaPath := filepath.Join(bc.metadataDir, target+metadataExt)
data, err := json.MarshalIndent(metadata, "", " ")
if err != nil {
return err
}
return os.WriteFile(metaPath, data, 0644)
}
// copyFile copies a file from src to dst
func copyFile(src, dst string) error {
source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()
// Ensure destination directory exists
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()
_, err = io.Copy(destination, source)
return err
}
// stringSlicesEqual compares two string slices for equality
func stringSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}