engineframework

package
v0.38.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 15, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

Engine Framework

The engineframework package provides a standardized framework for building MCP (Model Context Protocol) engines in Forge. It eliminates code duplication by abstracting common patterns for builders, test runners, and test environment subengines.

Table of Contents

Overview

The engine framework provides:

  • Builder Framework (builder.go) - For build engines that create artifacts
  • TestRunner Framework (testrunner.go) - For test execution engines
  • TestEnv Subengine Framework (testenvsubengine.go) - For test environment provisioning
  • Spec Utilities (spec.go) - Type-safe extraction from map[string]any specs
  • Version Utilities (version.go) - Git versioning for artifacts

Key Benefits:

  • Automatic MCP tool registration (build, buildBatch, run, create, delete)
  • Standardized input validation
  • Consistent error handling and response formatting
  • Reduced code duplication (typical savings: ~60% fewer lines)
  • Type-safe spec extraction with sensible defaults

Architecture

The framework follows a two-layer architecture:

┌──────────────────────────────────────┐
│   internal/cli.Bootstrap             │  ← Engine Lifecycle
│   - CLI flag parsing                 │
│   - --mcp mode detection             │
│   - Version info management          │
│   - main() orchestration             │
└──────────────────────────────────────┘
                  ↓
┌──────────────────────────────────────┐
│   pkg/engineframework                │  ← MCP Tool Registration
│   - RegisterBuilderTools()           │
│   - RegisterTestRunnerTools()        │
│   - RegisterTestEnvSubengineTools()  │
│   - Input validation                 │
│   - Error conversion                 │
│   - Response formatting              │
└──────────────────────────────────────┘
                  ↓
┌──────────────────────────────────────┐
│   Your Engine Implementation         │  ← Business Logic
│   - BuilderFunc / TestRunnerFunc     │
│   - CreateFunc / DeleteFunc          │
│   - Spec extraction                  │
│   - Resource management              │
└──────────────────────────────────────┘

CRITICAL: Use Both Layers

  • internal/cli.Bootstrap handles engine lifecycle (CLI parsing, --mcp mode)
  • pkg/engineframework handles MCP tool registration and validation
  • Never replace cli.Bootstrap - the framework extends it, not replaces it

Typical main.go structure:

package main

import (
    "github.com/alexandremahdhaoui/forge/internal/cli"
    "github.com/alexandremahdhaoui/forge/pkg/engineframework"
)

func main() {
    cli.Bootstrap(runMCPServer, &versionInfo)
}

func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-engine", v)

    config := engineframework.BuilderConfig{
        Name:      "my-engine",
        Version:   v,
        BuildFunc: myBuildFunc,
    }

    if err := engineframework.RegisterBuilderTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

When to Use Each Framework

Builder Framework

Use RegisterBuilderTools() when your engine:

  • Builds artifacts (binaries, containers, generated code)
  • Takes a BuildInput with Name, Engine, Spec
  • Returns an Artifact with Name, Type, Location, Version, Timestamp
  • Needs both build and buildBatch MCP tools

Examples: go-build, container-build, generic-builder, go-gen-openapi

TestRunner Framework

Use RegisterTestRunnerTools() when your engine:

  • Executes tests and collects results
  • Takes a RunInput with Stage, Name, Spec
  • Returns a TestReport with Status, TestStats, ErrorMessage
  • Needs a run MCP tool
  • Must distinguish between test failures (report with Status="failed") and execution errors

Examples: go-test, generic-test-runner

TestEnv Subengine Framework

Use RegisterTestEnvSubengineTools() when your engine:

  • Provisions test environment resources (clusters, registries, databases)
  • Takes CreateInput with TestID, Stage, TmpDir, Metadata, Spec
  • Takes DeleteInput with TestID, Metadata
  • Returns TestEnvArtifact with Files, Metadata, ManagedResources
  • Needs both create and delete MCP tools
  • Is called by the testenv orchestrator

Examples: testenv-kind, testenv-lcr, testenv-helm-install

Quick Start Guides

Creating a Builder

Step 1: Define your build function

func myBuildFunc(ctx context.Context, input mcptypes.BuildInput) (*forge.Artifact, error) {
    // Extract spec values
    outputDir := engineframework.ExtractStringWithDefault(input.Spec, "outputDir", "./build")

    // Perform build logic
    if err := runBuildCommand(input.Name, outputDir); err != nil {
        return nil, fmt.Errorf("build failed: %w", err)
    }

    // Return versioned artifact
    return engineframework.CreateVersionedArtifact(
        input.Name,
        "binary",
        filepath.Join(outputDir, input.Name),
    )
}

Step 2: Register with MCP server

func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-builder", v)

    config := engineframework.BuilderConfig{
        Name:      "my-builder",
        Version:   v,
        BuildFunc: myBuildFunc,
    }

    if err := engineframework.RegisterBuilderTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

Step 3: Keep cli.Bootstrap in main.go

func main() {
    cli.Bootstrap(runMCPServer, &versionInfo)
}

What you get automatically:

  • build tool for single builds
  • buildBatch tool for batch builds
  • Input validation (Name, Engine required)
  • Error conversion to MCP responses
  • Artifact formatting
Creating a Test Runner

Step 1: Define your test function

func myTestRunnerFunc(ctx context.Context, input mcptypes.RunInput) (*forge.TestReport, error) {
    // Extract spec values
    testPattern := engineframework.ExtractStringWithDefault(input.Spec, "pattern", "./...")

    // Run tests
    output, err := runTestCommand(input.Stage, testPattern)
    if err != nil {
        // Execution error - couldn't run tests
        return nil, fmt.Errorf("failed to execute tests: %w", err)
    }

    // Parse test results
    report := parseTestOutput(output)

    // CRITICAL: Return report even if tests failed
    // Framework will use ErrorResultWithArtifact for failed tests
    return report, nil
}

Step 2: Register with MCP server

func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-test-runner", v)

    config := engineframework.TestRunnerConfig{
        Name:        "my-test-runner",
        Version:     v,
        RunTestFunc: myTestRunnerFunc,
    }

    if err := engineframework.RegisterTestRunnerTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

Critical distinction:

  • Test failures are NOT errors - Return report with Status="failed"
  • Execution errors are errors - Return nil, error when you can't run tests

What you get automatically:

  • run tool for test execution
  • Input validation (Stage, Name required)
  • Report return even on test failure (uses ErrorResultWithArtifact)
  • Summary generation from TestStats
Creating a TestEnv Subengine
CreateInput Schema

The CreateInput struct provides all necessary context for creating a test environment resource:

Field Type Required Description
TestID string Yes Unique identifier for the test environment
Stage string Yes Test stage name
TmpDir string Yes Temporary directory for test artifacts
RootDir string No Project root directory for resolving relative paths
Metadata map[string]string No Metadata from previous subengines in the chain
Spec map[string]any No Subengine-specific configuration
Env map[string]string No Accumulated environment variables
EnvPropagation EnvPropagation No Environment variable propagation settings

RootDir Usage:

  • Used to resolve relative paths to absolute paths based on the project root
  • Populated by the testenv orchestrator via os.Getwd()
  • Should be used with filepath.Join() for portable path resolution
  • Example pattern for path resolution:
    if input.RootDir != "" && !filepath.IsAbs(relativePath) {
        absolutePath = filepath.Join(input.RootDir, relativePath)
    }
    
  • Always add fail-fast validation after resolution:
    if _, err := os.Stat(resolvedPath); os.IsNotExist(err) {
        return nil, fmt.Errorf("path not found: %s", resolvedPath)
    }
    

Step 1: Define create and delete functions

func myCreateFunc(ctx context.Context, input CreateInput) (*TestEnvArtifact, error) {
    // Extract spec values
    clusterVersion := engineframework.ExtractStringWithDefault(input.Spec, "version", "v1.27.0")

    // Create resource
    clusterName := fmt.Sprintf("myapp-%s", input.TestID)
    if err := createCluster(clusterName, clusterVersion); err != nil {
        return nil, fmt.Errorf("failed to create cluster: %w", err)
    }

    // Generate kubeconfig
    kubeconfigPath := filepath.Join(input.TmpDir, "kubeconfig")
    if err := writeKubeconfig(clusterName, kubeconfigPath); err != nil {
        return nil, fmt.Errorf("failed to write kubeconfig: %w", err)
    }

    // Return artifact
    return &TestEnvArtifact{
        TestID: input.TestID,
        Files: map[string]string{
            "my-engine.kubeconfig": "kubeconfig", // Relative to TmpDir
        },
        Metadata: map[string]string{
            "my-engine.clusterName": clusterName,
            "my-engine.version":     clusterVersion,
        },
        ManagedResources: []string{kubeconfigPath},
    }, nil
}

func myDeleteFunc(ctx context.Context, input DeleteInput) error {
    // Best-effort cleanup - don't fail if already gone
    clusterName := input.Metadata["my-engine.clusterName"]
    if clusterName == "" {
        clusterName = fmt.Sprintf("myapp-%s", input.TestID)
    }

    if err := deleteCluster(clusterName); err != nil {
        log.Printf("Warning: failed to delete cluster: %v", err)
        return nil // Don't fail on cleanup errors
    }

    return nil
}

Step 2: Register with MCP server

func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-testenv", v)

    config := engineframework.TestEnvSubengineConfig{
        Name:       "my-testenv",
        Version:    v,
        CreateFunc: myCreateFunc,
        DeleteFunc: myDeleteFunc,
    }

    if err := engineframework.RegisterTestEnvSubengineTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

Important patterns:

  • Use input.TmpDir for file storage
  • Return relative paths in Files map
  • Store metadata for downstream consumers and cleanup
  • Delete should be best-effort (don't fail if resource is gone)

What you get automatically:

  • create tool for resource provisioning
  • delete tool for cleanup
  • Input validation (TestID, Stage, TmpDir for create; TestID for delete)
  • Artifact serialization to map[string]interface{}

Migration Guide

Before: Manual MCP Registration
// Old mcp.go - ~165 lines
func runMCPServer() error {
    server := mcpserver.New("my-builder", "1.0.0")

    mcpserver.RegisterTool(server, &mcp.Tool{
        Name:        "build",
        Description: "Build an artifact",
    }, handleBuildTool)

    mcpserver.RegisterTool(server, &mcp.Tool{
        Name:        "buildBatch",
        Description: "Build multiple artifacts",
    }, handleBuildBatchTool)

    return server.RunDefault()
}

func handleBuildTool(ctx context.Context, req *mcp.CallToolRequest, input BuildInput) (*mcp.CallToolResult, any, error) {
    // 30+ lines of validation, error handling, response formatting
    if input.Name == "" {
        return mcputil.ErrorResult("Build failed: name is required"), nil, nil
    }
    // ... more validation ...

    artifact, err := build(input)
    if err != nil {
        return mcputil.ErrorResult(fmt.Sprintf("Build failed: %v", err)), nil, nil
    }

    result, returnedArtifact := mcputil.SuccessResultWithArtifact("Build succeeded", artifact)
    return result, returnedArtifact, nil
}

func handleBuildBatchTool(ctx context.Context, req *mcp.CallToolRequest, input BatchBuildInput) (*mcp.CallToolResult, any, error) {
    // 40+ lines of batch handling, error collection, response formatting
    // ...
}

func build(input BuildInput) (*Artifact, error) {
    // Actual build logic
}
After: Using Framework
// New mcp.go - ~40 lines
func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-builder", v)

    config := engineframework.BuilderConfig{
        Name:      "my-builder",
        Version:   v,
        BuildFunc: build,
    }

    if err := engineframework.RegisterBuilderTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

func build(ctx context.Context, input mcptypes.BuildInput) (*forge.Artifact, error) {
    // Extract spec values
    outputDir := engineframework.ExtractStringWithDefault(input.Spec, "outputDir", "./build")

    // Actual build logic
    // ...

    return engineframework.CreateVersionedArtifact(name, "binary", path)
}

Migration checklist:

  1. ✅ Keep cli.Bootstrap() in main.go (DO NOT CHANGE)
  2. ✅ Create BuilderFunc/TestRunnerFunc/CreateFunc+DeleteFunc
  3. ✅ Move validation logic to framework (automatic)
  4. ✅ Use spec extraction utilities for configuration
  5. ✅ Use version utilities for artifacts
  6. ✅ Remove manual tool registration code
  7. ✅ Remove manual validation code
  8. ✅ Remove manual error conversion code
  9. ✅ Test with existing integration tests

Detailed Documentation

Function Type Approach

The framework uses function types instead of interface embedding:

type BuilderFunc func(ctx context.Context, input mcptypes.BuildInput) (*forge.Artifact, error)

Why not interfaces?

// ❌ Interface approach - more complex
type Builder interface {
    Build(ctx context.Context, input BuildInput) (*Artifact, error)
}

// Requires struct with methods
type MyBuilder struct{}
func (b *MyBuilder) Build(ctx context.Context, input BuildInput) (*Artifact, error) { ... }

// ✅ Function type approach - simpler
func myBuildFunc(ctx context.Context, input BuildInput) (*Artifact, error) { ... }

Benefits of function types:

  • Simpler - just write a function, no struct needed
  • More idiomatic Go for handler-style code
  • Compiles cleanly without ceremony
  • Matches existing MCP handler patterns
Spec Extraction Utilities

The framework provides type-safe extraction from map[string]any specs:

// Extract with type conversion
value, ok := engineframework.ExtractString(spec, "key")
sliceVal, ok := engineframework.ExtractStringSlice(spec, "tags")
mapVal, ok := engineframework.ExtractStringMap(spec, "labels")
boolVal, ok := engineframework.ExtractBool(spec, "enabled")
intVal, ok := engineframework.ExtractInt(spec, "timeout")

// Extract with defaults
value := engineframework.ExtractStringWithDefault(spec, "key", "default")
timeout := engineframework.ExtractIntWithDefault(spec, "timeout", 30)

// Require values (returns error if missing/wrong type)
value, err := engineframework.RequireString(spec, "key")
tags, err := engineframework.RequireStringSlice(spec, "tags")

Handles JSON unmarshal edge cases:

  • []any with string elements → []string
  • map[string]any with string values → map[string]string
  • float64 (JSON number) → int (if no decimal part)
Git Versioning Utilities
// Get current git commit SHA
version, err := engineframework.GetGitVersion()
// Returns: "abc123..." or "unknown" on error

// Create versioned artifact (uses git SHA)
artifact, err := engineframework.CreateVersionedArtifact("my-app", "binary", "./build/bin/my-app")
// artifact.Version = "abc123..."
// artifact.Timestamp = "2024-01-15T10:30:00Z"

// Create artifact without version (for generated code)
artifact := engineframework.CreateArtifact("openapi-client", "generated", "./pkg/generated")
// artifact.Version = ""
// artifact.Timestamp = "2024-01-15T10:30:00Z"

// Create artifact with custom version
artifact := engineframework.CreateCustomArtifact("my-app", "container", "localhost:5000/my-app:v1.2.3", "v1.2.3")
// artifact.Version = "v1.2.3"
// artifact.Timestamp = "2024-01-15T10:30:00Z"

All timestamps are RFC3339 in UTC.

Troubleshooting

Problem: "unknown tool buildBatch" error

Cause: MCP tool not registered

Solution: Use RegisterBuilderTools() instead of manually registering only build tool

// ❌ Wrong - missing buildBatch
mcpserver.RegisterTool(server, &mcp.Tool{Name: "build"}, handleBuild)

// ✅ Correct - registers both build and buildBatch
engineframework.RegisterBuilderTools(server, config)
Problem: Tests fail but report is nil

Cause: Returning error instead of report for test failures

Solution: Return report with Status="failed", only return error for execution failures

// ❌ Wrong - returns error for test failures
if testsFailed {
    return nil, errors.New("tests failed")
}

// ✅ Correct - returns report with Status="failed"
if testsFailed {
    return &forge.TestReport{
        Status: "failed",
        TestStats: forge.TestStats{...},
    }, nil
}
Problem: Spec extraction returns wrong type

Cause: JSON unmarshal converts types

Solution: Use framework extraction utilities that handle JSON edge cases

// ❌ Wrong - doesn't handle []any with string elements
tags := spec["tags"].([]string) // Panics!

// ✅ Correct - handles JSON unmarshal edge cases
tags, ok := engineframework.ExtractStringSlice(spec, "tags")
Problem: cli.Bootstrap conflicts with framework

Cause: Misunderstanding architecture

Solution: Use BOTH - cli.Bootstrap for lifecycle, framework for MCP registration

// ✅ Correct - use both layers
func main() {
    cli.Bootstrap(runMCPServer, &versionInfo) // Lifecycle
}

func runMCPServer() error {
    server := mcpserver.New("my-engine", v)
    engineframework.RegisterBuilderTools(server, config) // MCP registration
    return server.RunDefault()
}
Problem: Missing artifact version

Cause: Using CreateArtifact() for built binaries

Solution: Use CreateVersionedArtifact() for built artifacts

// ❌ Wrong - no version for built binary
artifact := engineframework.CreateArtifact(name, "binary", path)

// ✅ Correct - includes git commit SHA as version
artifact, err := engineframework.CreateVersionedArtifact(name, "binary", path)

Documentation

Overview

Package engineframework provides MCP tool registration utilities for forge engines.

This package EXTENDS existing infrastructure:

  • Engines use pkg/enginecli.Bootstrap for lifecycle management (main, version, --mcp flags)
  • Engines use engineframework for MCP tool registration (build, run, create, delete tools)

DO NOT use this package to replace cli.Bootstrap. Use it to simplify MCP tool registration.

Architecture

Forge engines have a two-layer architecture:

  1. Lifecycle Layer (pkg/enginecli.Bootstrap): - Handles main() function - Parses command-line flags (version, help, --mcp) - Manages MCP server lifecycle - Provides version information

  2. MCP Registration Layer (pkg/engineframework): - Registers MCP tools (build, run, create, delete) - Handles batch operations automatically - Provides input validation and result formatting - Eliminates duplicate MCP handler code

Framework Types

The package provides three specialized frameworks:

  1. Builder Framework (builder.go): - For engines that build artifacts (binaries, containers, generated code) - Registers "build" and "buildBatch" MCP tools - Used by: go-build, container-build, generic-builder, go-gen-openapi, go-gen-mocks, go-format, go-lint

  2. TestRunner Framework (testrunner.go): - For engines that execute tests and generate reports - Registers "run" MCP tool - Used by: go-test, go-lint-tags, generic-test-runner, forge-e2e

  3. TestEnv Subengine Framework (testenvsubengine.go): - For engines that create and delete test environments - Registers "create" and "delete" MCP tools - Used by: testenv-kind, testenv-lcr, testenv-helm-install - NOTE: testenv orchestrator does NOT use this (different pattern)

Function Type Approach

This framework uses function types (not interface embedding):

type BuilderFunc func(ctx context.Context, input mcptypes.BuildInput) (*forge.Artifact, error)
type TestRunnerFunc func(ctx context.Context, input mcptypes.RunInput) (*forge.TestReport, error)
type CreateFunc func(ctx context.Context, input CreateInput) (*TestEnvArtifact, error)

This approach:

  • Compiles cleanly in Go (no impossible interface constraints)
  • Matches existing engine handler patterns (standalone functions)
  • Simplifies implementation (no struct requirements)
  • Maintains type safety

Usage Examples

Builder Framework Example

Using cli.Bootstrap + BuilderFramework together:

package main

import (
    "context"
    "fmt"
    "github.com/alexandremahdhaoui/forge/pkg/enginecli"
    "github.com/alexandremahdhaoui/forge/pkg/mcpserver"
    "github.com/alexandremahdhaoui/forge/pkg/engineframework"
    "github.com/alexandremahdhaoui/forge/pkg/forge"
    "github.com/alexandremahdhaoui/forge/pkg/mcptypes"
)

var versionInfo cli.VersionInfo

func main() {
    // Use cli.Bootstrap for lifecycle (main, version, --mcp)
    cli.Bootstrap(runMCPServer, &versionInfo)
}

func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-builder", v)

    // Use engineframework for MCP registration
    config := engineframework.BuilderConfig{
        Name:      "my-builder",
        Version:   v,
        BuildFunc: buildFunc,
    }

    if err := engineframework.RegisterBuilderTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

func buildFunc(ctx context.Context, input mcptypes.BuildInput) (*forge.Artifact, error) {
    // Extract spec configuration
    outputDir := engineframework.ExtractStringWithDefault(input.Spec, "outputDir", "./build")

    // Perform build
    if err := runBuild(input.Name, outputDir); err != nil {
        return nil, fmt.Errorf("build failed: %w", err)
    }

    // Return versioned artifact
    return engineframework.CreateVersionedArtifact(input.Name, "binary", outputDir+"/"+input.Name)
}

TestRunner Framework Example

For test runners that execute tests and return reports:

func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-test-runner", v)

    config := engineframework.TestRunnerConfig{
        Name:        "my-test-runner",
        Version:     v,
        RunTestFunc: runTests,
    }

    if err := engineframework.RegisterTestRunnerTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

func runTests(ctx context.Context, input mcptypes.RunInput) (*forge.TestReport, error) {
    // Extract spec configuration
    testPattern := engineframework.ExtractStringWithDefault(input.Spec, "pattern", "./...")

    // Run tests
    output, err := executeTests(input.Stage, testPattern)
    if err != nil {
        // Execution error - couldn't run tests
        return nil, fmt.Errorf("failed to execute tests: %w", err)
    }

    // Parse test results
    report := parseTestOutput(output)

    // CRITICAL: Return report even if tests failed
    // Framework will use ErrorResultWithArtifact for failed tests
    return report, nil
}

TestEnv Subengine Framework Example

For test environment provisioners (clusters, registries, databases):

func runMCPServer() error {
    v, _, _ := versionInfo.Get()
    server := mcpserver.New("my-testenv", v)

    config := engineframework.TestEnvSubengineConfig{
        Name:       "my-testenv",
        Version:    v,
        CreateFunc: createResource,
        DeleteFunc: deleteResource,
    }

    if err := engineframework.RegisterTestEnvSubengineTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

func createResource(ctx context.Context, input engineframework.CreateInput) (*engineframework.TestEnvArtifact, error) {
    // Extract spec configuration
    version := engineframework.ExtractStringWithDefault(input.Spec, "version", "latest")

    // Create resource
    resourceName := fmt.Sprintf("myapp-%s", input.TestID)
    if err := provisionResource(resourceName, version); err != nil {
        return nil, fmt.Errorf("failed to create resource: %w", err)
    }

    // Return artifact with files, metadata, and managed resources
    return &engineframework.TestEnvArtifact{
        TestID: input.TestID,
        Files: map[string]string{
            "my-testenv.config": "config.yaml",
        },
        Metadata: map[string]string{
            "my-testenv.resourceName": resourceName,
            "my-testenv.version":      version,
        },
        ManagedResources: []string{input.TmpDir + "/config.yaml"},
    }, nil
}

func deleteResource(ctx context.Context, input engineframework.DeleteInput) error {
    // Best-effort cleanup
    resourceName := input.Metadata["my-testenv.resourceName"]
    if err := cleanupResource(resourceName); err != nil {
        log.Printf("Warning: failed to cleanup: %v", err)
        return nil // Don't fail on cleanup errors
    }
    return nil
}

Utilities

The package also provides common utilities:

  • Spec Extraction (spec.go): Extract typed values from map[string]any Spec fields
  • Git Versioning (version.go): Standardize artifact versioning with git commit hashes

See individual framework files (builder.go, testrunner.go, testenvsubengine.go) for detailed usage.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CallDetector added in v0.16.0

func CallDetector(ctx context.Context, cmd string, args []string, toolName string, input any) ([]forge.ArtifactDependency, error)

CallDetector calls a detector MCP server and returns dependencies. It spawns the detector as a subprocess, connects via MCP, calls the specified tool, and converts the response to []forge.ArtifactDependency.

Parameters:

  • ctx: context for the operation
  • cmd: command to execute (e.g., "go")
  • args: arguments for the command (e.g., ["run", "github.com/.../cmd/detector@v0.9.0"])
  • toolName: name of the MCP tool to call (e.g., "detectDependencies")
  • input: input parameters for the tool (will be serialized to JSON)

Returns:

  • []forge.ArtifactDependency: list of detected dependencies
  • error: if connection fails, tool call fails, or response parsing fails

func CreateArtifact

func CreateArtifact(name, artifactType, location string) *forge.Artifact

CreateArtifact creates an artifact with current timestamp but NO version field. Use this for artifacts that don't have git versioning (e.g., generated code, test reports).

Parameters:

  • name: Artifact name (from BuildInput.Name)
  • artifactType: Type of artifact (e.g., "generated", "test-report")
  • location: Location of the artifact (path or directory)

Returns:

  • *forge.Artifact with Name, Type, Location, and Timestamp set
  • Version field is empty (generated artifacts don't have versions)

Example:

artifact := CreateArtifact("openapi-client", "generated", "./pkg/generated")
// artifact.Version = "" (empty - generated code has no version)
// artifact.Timestamp = "2025-01-15T10:30:00Z" (current time)

func CreateCustomArtifact

func CreateCustomArtifact(name, artifactType, location, version string) *forge.Artifact

CreateCustomArtifact creates an artifact with a custom version string and current timestamp. Use this when you need a specific version that's not from git (e.g., semantic version, build number).

Parameters:

  • name: Artifact name (from BuildInput.Name)
  • artifactType: Type of artifact (e.g., "binary", "container")
  • location: Location of the artifact (path or registry URL)
  • version: Custom version string (e.g., "v1.2.3", "build-123")

Returns:

  • *forge.Artifact with Name, Type, Location, Version (custom), and Timestamp set

Example:

artifact := CreateCustomArtifact("my-app", "container", "localhost:5000/my-app:v1.2.3", "v1.2.3")
// artifact.Version = "v1.2.3" (custom version)
// artifact.Timestamp = "2025-01-15T10:30:00Z" (current time)

func CreateVersionedArtifact

func CreateVersionedArtifact(name, artifactType, location string) (*forge.Artifact, error)

CreateVersionedArtifact creates an artifact with git version and current timestamp. The version field is populated with the current git commit hash. The timestamp field is set to the current time in RFC3339 format.

Parameters:

  • name: Artifact name (from BuildInput.Name)
  • artifactType: Type of artifact (e.g., "binary", "container", "generated")
  • location: Location of the artifact (path or registry URL)

Returns:

  • *forge.Artifact with Name, Type, Location, Version (git SHA), and Timestamp set
  • error if git version cannot be determined

Example:

artifact, err := CreateVersionedArtifact("my-app", "binary", "./build/bin/my-app")
if err != nil {
    return nil, fmt.Errorf("failed to create artifact: %w", err)
}
// artifact.Version = "a1b2c3d4..." (git commit SHA)
// artifact.Timestamp = "2025-01-15T10:30:00Z" (current time)

func ExtractBool

func ExtractBool(spec map[string]any, key string) (bool, bool)

ExtractBool safely extracts a bool value from a spec map. Returns the bool value and true if the key exists and is a bool. Returns false and false if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"enabled": true, "name": "foo"}
enabled, ok := ExtractBool(spec, "enabled")  // true, true
missing, ok := ExtractBool(spec, "missing")  // false, false
wrong, ok := ExtractBool(spec, "name")  // false, false (wrong type)

func ExtractBoolWithDefault

func ExtractBoolWithDefault(spec map[string]any, key string, defaultValue bool) bool

ExtractBoolWithDefault safely extracts a bool value from a spec map with a default value. Returns the bool value if the key exists and is a bool. Returns the default value if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"enabled": true}
enabled := ExtractBoolWithDefault(spec, "enabled", false)  // true
missing := ExtractBoolWithDefault(spec, "missing", false)  // false

func ExtractInt

func ExtractInt(spec map[string]any, key string) (int, bool)

ExtractInt safely extracts an int value from a spec map. Returns the int value and true if the key exists and is an int, int64, or float64 that represents an integer. Returns 0 and false if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"count": 42, "rate": 3.14, "name": "foo"}
count, ok := ExtractInt(spec, "count")  // 42, true
missing, ok := ExtractInt(spec, "missing")  // 0, false
wrong, ok := ExtractInt(spec, "name")  // 0, false (wrong type)

func ExtractIntWithDefault

func ExtractIntWithDefault(spec map[string]any, key string, defaultValue int) int

ExtractIntWithDefault safely extracts an int value from a spec map with a default value. Returns the int value if the key exists and is a valid integer. Returns the default value if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"count": 42}
count := ExtractIntWithDefault(spec, "count", 10)  // 42
missing := ExtractIntWithDefault(spec, "missing", 10)  // 10

func ExtractMap

func ExtractMap(spec map[string]any, key string) (map[string]any, bool)

ExtractMap safely extracts a map[string]any value from a spec map. Returns the map and true if the key exists and is a map[string]any. Returns nil and false if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"config": map[string]any{"timeout": 30}}
config, ok := ExtractMap(spec, "config")  // {"timeout": 30}, true
missing, ok := ExtractMap(spec, "missing")  // nil, false

func ExtractMapWithDefault

func ExtractMapWithDefault(spec map[string]any, key string, defaultValue map[string]any) map[string]any

ExtractMapWithDefault safely extracts a map[string]any value from a spec map with a default value. Returns the map if the key exists and is a valid map[string]any. Returns the default value if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"config": map[string]any{"timeout": 30}}
config := ExtractMapWithDefault(spec, "config", map[string]any{"default": true})  // {"timeout": 30}
missing := ExtractMapWithDefault(spec, "missing", map[string]any{"default": true})  // {"default": true}

func ExtractString

func ExtractString(spec map[string]any, key string) (string, bool)

ExtractString safely extracts a string value from a spec map. Returns the string value and true if the key exists and is a string. Returns empty string and false if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"name": "my-app", "count": 42}
name, ok := ExtractString(spec, "name")  // "my-app", true
missing, ok := ExtractString(spec, "missing")  // "", false
wrong, ok := ExtractString(spec, "count")  // "", false (wrong type)

func ExtractStringMap

func ExtractStringMap(spec map[string]any, key string) (map[string]string, bool)

ExtractStringMap safely extracts a map[string]string value from a spec map. Returns the map and true if the key exists and is a map[string]string or map[string]any with string values. Returns nil and false if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"labels": map[string]string{"app": "foo"}}
labels, ok := ExtractStringMap(spec, "labels")  // {"app": "foo"}, true
missing, ok := ExtractStringMap(spec, "missing")  // nil, false

func ExtractStringMapWithDefault

func ExtractStringMapWithDefault(spec map[string]any, key string, defaultValue map[string]string) map[string]string

ExtractStringMapWithDefault safely extracts a map[string]string value from a spec map with a default value. Returns the map if the key exists and is a valid map[string]string. Returns the default value if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"labels": map[string]string{"app": "foo"}}
labels := ExtractStringMapWithDefault(spec, "labels", map[string]string{"default": "value"})  // {"app": "foo"}
missing := ExtractStringMapWithDefault(spec, "missing", map[string]string{"default": "value"})  // {"default": "value"}

func ExtractStringSlice

func ExtractStringSlice(spec map[string]any, key string) ([]string, bool)

ExtractStringSlice safely extracts a []string value from a spec map. Returns the slice and true if the key exists and is a []string or []any containing only strings. Returns nil and false if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"tags": []string{"a", "b"}, "numbers": []int{1, 2}}
tags, ok := ExtractStringSlice(spec, "tags")  // ["a", "b"], true
missing, ok := ExtractStringSlice(spec, "missing")  // nil, false
wrong, ok := ExtractStringSlice(spec, "numbers")  // nil, false

func ExtractStringSliceWithDefault

func ExtractStringSliceWithDefault(spec map[string]any, key string, defaultValue []string) []string

ExtractStringSliceWithDefault safely extracts a []string value from a spec map with a default value. Returns the slice if the key exists and is a valid []string. Returns the default value if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"tags": []string{"a", "b"}}
tags := ExtractStringSliceWithDefault(spec, "tags", []string{"default"})  // ["a", "b"]
missing := ExtractStringSliceWithDefault(spec, "missing", []string{"default"})  // ["default"]

func ExtractStringWithDefault

func ExtractStringWithDefault(spec map[string]any, key, defaultValue string) string

ExtractStringWithDefault safely extracts a string value from a spec map with a default value. Returns the string value if the key exists and is a string. Returns the default value if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"name": "my-app"}
name := ExtractStringWithDefault(spec, "name", "default")  // "my-app"
missing := ExtractStringWithDefault(spec, "missing", "default")  // "default"

func FindDetector deprecated added in v0.16.0

func FindDetector(name string) (string, error)

FindDetector locates a dependency detector binary by name. It searches in the following order:

  1. PATH environment variable
  2. ./build/bin directory (common for forge self-build)

Returns the absolute path to the binary or an error if not found.

Deprecated: FindDetector only works when CWD is the forge repository. Use ResolveDetector() + CallDetector() instead, which works from any directory by using `go run` with versioned module paths.

func GetGitVersion

func GetGitVersion() (string, error)

GetGitVersion returns the current git commit hash. Returns the commit SHA and nil error on success. Returns "unknown" and an error if git operations fail.

Example:

version, err := GetGitVersion()
if err != nil {
    log.Printf("Warning: could not get git version: %v", err)
    version = "unknown"
}
fmt.Printf("Building version: %s\n", version)

func RegisterBuilderTools

func RegisterBuilderTools(server *mcpserver.Server, config BuilderConfig) error

RegisterBuilderTools registers build and buildBatch tools with the MCP server.

This function automatically:

  • Registers "build" tool that calls the BuildFunc
  • Registers "buildBatch" tool that handles multiple builds in parallel
  • Validates required input fields (Name, Engine)
  • Converts BuilderFunc errors to MCP error responses
  • Formats successful results with artifact information
  • Uses mcputil.HandleBatchBuild for batch processing

Parameters:

  • server: The MCP server instance
  • config: Builder configuration with Name, Version, and BuildFunc

Returns:

  • nil on success
  • error if tool registration fails (e.g., duplicate tool names)

Example:

func runMCPServer() error {
    server := mcpserver.New("my-builder", "1.0.0")

    config := BuilderConfig{
        Name:      "my-builder",
        Version:   "1.0.0",
        BuildFunc: myBuildFunc,
    }

    if err := RegisterBuilderTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

func RegisterTestEnvSubengineTools

func RegisterTestEnvSubengineTools(server *mcpserver.Server, config TestEnvSubengineConfig) error

RegisterTestEnvSubengineTools registers create and delete tools with the MCP server.

This function automatically:

  • Registers "create" tool that calls the CreateFunc
  • Registers "delete" tool that calls the DeleteFunc
  • Validates required input fields (TestID, Stage, TmpDir for create; TestID for delete)
  • Converts function errors to MCP error responses
  • Returns TestEnvArtifact on successful create
  • Uses SuccessResultWithArtifact for create operations
  • Uses SuccessResult for delete operations

Parameters:

  • server: The MCP server instance
  • config: TestEnvSubengine configuration with Name, Version, CreateFunc, and DeleteFunc

Returns:

  • nil on success
  • error if tool registration fails (e.g., duplicate tool names)

Example:

func runMCPServer() error {
    server := mcpserver.New("testenv-kind", "1.0.0")

    config := TestEnvSubengineConfig{
        Name:       "testenv-kind",
        Version:    "1.0.0",
        CreateFunc: myCreateFunc,
        DeleteFunc: myDeleteFunc,
    }

    if err := RegisterTestEnvSubengineTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

func RegisterTestRunnerTools

func RegisterTestRunnerTools(server *mcpserver.Server, config TestRunnerConfig) error

RegisterTestRunnerTools registers the run tool with the MCP server.

This function automatically:

  • Registers "run" tool that calls the RunTestFunc
  • Validates required input fields (Stage, Runner)
  • Converts TestRunnerFunc errors to MCP error responses
  • Returns TestReport as artifact even when tests fail
  • Uses ErrorResultWithArtifact for failed tests (report still returned)
  • Uses SuccessResultWithArtifact for passed tests

Parameters:

  • server: The MCP server instance
  • config: TestRunner configuration with Name, Version, and RunTestFunc

Returns:

  • nil on success
  • error if tool registration fails (e.g., duplicate tool names)

Example:

func runMCPServer() error {
    server := mcpserver.New("my-test-runner", "1.0.0")

    config := TestRunnerConfig{
        Name:        "my-test-runner",
        Version:     "1.0.0",
        RunTestFunc: myTestRunnerFunc,
    }

    if err := RegisterTestRunnerTools(server, config); err != nil {
        return err
    }

    return server.RunDefault()
}

func RequireBool

func RequireBool(spec map[string]any, key string) (bool, error)

RequireBool extracts a required bool value from a spec map. Returns the bool value and nil error if the key exists and is a bool. Returns an error if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"enabled": true}
enabled, err := RequireBool(spec, "enabled")  // true, nil
missing, err := RequireBool(spec, "missing")  // false, error

func RequireInt

func RequireInt(spec map[string]any, key string) (int, error)

RequireInt extracts a required int value from a spec map. Returns the int value and nil error if the key exists and is a valid integer. Returns an error if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"count": 42}
count, err := RequireInt(spec, "count")  // 42, nil
missing, err := RequireInt(spec, "missing")  // 0, error

func RequireMap

func RequireMap(spec map[string]any, key string) (map[string]any, error)

RequireMap extracts a required map[string]any value from a spec map. Returns the map and nil error if the key exists and is a valid map[string]any. Returns an error if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"config": map[string]any{"timeout": 30}}
config, err := RequireMap(spec, "config")  // {"timeout": 30}, nil
missing, err := RequireMap(spec, "missing")  // nil, error

func RequireString

func RequireString(spec map[string]any, key string) (string, error)

RequireString extracts a required string value from a spec map. Returns the string value and nil error if the key exists and is a string. Returns an error if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"name": "my-app"}
name, err := RequireString(spec, "name")  // "my-app", nil
missing, err := RequireString(spec, "missing")  // "", error

func RequireStringMap

func RequireStringMap(spec map[string]any, key string) (map[string]string, error)

RequireStringMap extracts a required map[string]string value from a spec map. Returns the map and nil error if the key exists and is a valid map[string]string. Returns an error if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"labels": map[string]string{"app": "foo"}}
labels, err := RequireStringMap(spec, "labels")  // {"app": "foo"}, nil
missing, err := RequireStringMap(spec, "missing")  // nil, error

func RequireStringSlice

func RequireStringSlice(spec map[string]any, key string) ([]string, error)

RequireStringSlice extracts a required []string value from a spec map. Returns the slice and nil error if the key exists and is a valid []string. Returns an error if the key doesn't exist or has the wrong type.

Example:

spec := map[string]any{"tags": []string{"a", "b"}}
tags, err := RequireStringSlice(spec, "tags")  // ["a", "b"], nil
missing, err := RequireStringSlice(spec, "missing")  // nil, error

func ResolveDetector added in v0.24.0

func ResolveDetector(detectorURI, forgeVersion string) (cmd string, args []string, err error)

ResolveDetector parses a detector URI and returns the command and args to execute it. Detectors only support go:// URIs.

Parameters:

  • detectorURI: URI of the detector (e.g., "go://go-dependency-detector")
  • forgeVersion: Version of forge to use (e.g., "v0.9.0")

Returns:

  • cmd: The command to execute (always "go")
  • args: Arguments for the command (e.g., ["run", "github.com/.../cmd/detector@v0.9.0"])
  • err: Error if the URI is invalid or resolution fails

Example usage:

cmd, args, err := ResolveDetector("go://go-dependency-detector", "v0.9.0")
// cmd = "go"
// args = ["run", "github.com/alexandremahdhaoui/forge/cmd/go-dependency-detector@v0.9.0"]

Types

type BuilderConfig

type BuilderConfig struct {
	Name      string      // Engine name (e.g., "go-build")
	Version   string      // Engine version
	BuildFunc BuilderFunc // Build implementation
}

BuilderConfig configures builder tool registration.

Fields:

  • Name: Engine name (e.g., "go-build", "container-build")
  • Version: Engine version string (e.g., "1.0.0" or git commit hash)
  • BuildFunc: The build implementation function

Example:

config := BuilderConfig{
    Name:      "my-builder",
    Version:   "1.0.0",
    BuildFunc: myBuildFunc,
}

type BuilderFunc

type BuilderFunc func(ctx context.Context, input mcptypes.BuildInput) (*forge.Artifact, error)

BuilderFunc is the signature for build operations.

Implementations must:

  • Validate input fields (required fields should be checked, use RequireString etc. from spec.go)
  • Execute the build operation (compile code, generate files, etc.)
  • Return Artifact on success (with Name, Type, Location, Version if applicable, Timestamp)
  • Return error on failure (business logic errors, not MCP errors)

The framework handles:

  • MCP tool registration
  • Batch operation support
  • Result formatting
  • Error conversion to MCP responses

Example:

func myBuildFunc(ctx context.Context, input mcptypes.BuildInput) (*forge.Artifact, error) {
    // Extract and validate spec fields
    sourceFile, err := RequireString(input.Spec, "sourceFile")
    if err != nil {
        return nil, err
    }

    // Execute build logic
    if err := compileSources(sourceFile); err != nil {
        return nil, fmt.Errorf("compilation failed: %w", err)
    }

    // Return artifact
    return CreateArtifact(input.Name, "binary", "./build/bin/"+input.Name), nil
}

type CreateFunc

type CreateFunc func(ctx context.Context, input CreateInput) (*TestEnvArtifact, error)

CreateFunc is the signature for testenv subengine create operations.

Implementations must:

  • Validate input fields (testID, stage, tmpDir are required)
  • Create the test environment resource (cluster, registry, etc.)
  • Return TestEnvArtifact on success with files, metadata, and managedResources
  • Return error on failure

The framework handles:

  • MCP tool registration
  • Result formatting
  • Error conversion to MCP responses
  • Artifact serialization

Example:

func myCreateFunc(ctx context.Context, input CreateInput) (*TestEnvArtifact, error) {
    // Create resource (e.g., kind cluster)
    clusterName := fmt.Sprintf("myapp-%s", input.TestID)
    if err := createCluster(clusterName); err != nil {
        return nil, fmt.Errorf("failed to create cluster: %w", err)
    }

    // Return artifact
    return &TestEnvArtifact{
        TestID: input.TestID,
        Files: map[string]string{
            "kubeconfig": "kubeconfig",
        },
        Metadata: map[string]string{
            "clusterName": clusterName,
        },
        ManagedResources: []string{"/path/to/kubeconfig"},
    }, nil
}

type CreateInput

type CreateInput struct {
	TestID         string                `json:"testID" jsonschema:"Unique identifier for this test environment instance"`
	Stage          string                `json:"stage" jsonschema:"Test stage name from forge.yaml test[].name"`
	TmpDir         string                `json:"tmpDir" jsonschema:"Temporary directory allocated for this test environment"`
	RootDir        string                `json:"rootDir,omitempty" jsonschema:"Project root directory for path resolution"`
	Metadata       map[string]string     `json:"metadata" jsonschema:"Metadata from previous testenv subengines in the chain"`
	Spec           map[string]any        `json:"spec,omitempty" jsonschema:"Engine-specific configuration from forge.yaml testenv[].spec"`
	Env            map[string]string     `json:"env,omitempty" jsonschema:"Accumulated environment variables from previous subengines in the chain"`
	EnvPropagation *forge.EnvPropagation `json:"envPropagation,omitempty" jsonschema:"Configuration for filtering environment variable propagation"`
}

CreateInput represents the input for testenv subengine create operations.

This is the standard input format for all testenv subengines (e.g., testenv-kind, testenv-lcr). The testenv orchestrator calls each subengine's "create" tool with this input.

Fields:

  • TestID: Unique identifier for this test environment instance (required)
  • Stage: Test stage name from forge.yaml (required)
  • TmpDir: Temporary directory allocated for this test environment (required)
  • RootDir: Project root directory for path resolution (optional)
  • Metadata: Metadata from previous subengines in the chain (optional)
  • Spec: Optional spec for configuration override from forge.yaml
  • Env: Accumulated environment variables from previous sub-engines (optional)
  • EnvPropagation: Optional EnvPropagation configuration from spec (optional)

Example:

input := CreateInput{
    TestID:   "test-abc123",
    Stage:    "integration",
    TmpDir:   "/tmp/forge-test-abc123",
    RootDir:  "/home/user/project",
    Metadata: map[string]string{"testenv-lcr.registryFQDN": "testenv-lcr.testenv-lcr.svc.cluster.local:31906"},
    Spec:     map[string]any{"kindVersion": "v1.27.0"},
    Env:      map[string]string{"TESTENV_LCR_FQDN": "testenv-lcr.testenv-lcr.svc.cluster.local:31906"},
}

type DeleteFunc

type DeleteFunc func(ctx context.Context, input DeleteInput) error

DeleteFunc is the signature for testenv subengine delete operations.

Implementations must:

  • Validate input fields (testID is required)
  • Delete the test environment resource (cluster, registry, etc.)
  • Return error on failure (or nil for best-effort cleanup)

The framework handles:

  • MCP tool registration
  • Result formatting
  • Error conversion to MCP responses

IMPORTANT: Delete operations should be best-effort. If resources are already gone, don't return an error. Only return errors for actual failures that need attention.

Example:

func myDeleteFunc(ctx context.Context, input DeleteInput) error {
    // Reconstruct cluster name from testID
    clusterName := input.Metadata["clusterName"]
    if clusterName == "" {
        clusterName = fmt.Sprintf("myapp-%s", input.TestID)
    }

    // Delete cluster (best-effort)
    if err := deleteCluster(clusterName); err != nil {
        log.Printf("Warning: failed to delete cluster: %v", err)
        return nil // Don't fail on cleanup errors
    }

    return nil
}

type DeleteInput

type DeleteInput struct {
	TestID   string            `json:"testID" jsonschema:"Unique identifier of the test environment instance to delete"`
	Metadata map[string]string `json:"metadata" jsonschema:"Metadata from the test environment used for resource cleanup"`
}

DeleteInput represents the input for testenv subengine delete operations.

This is the standard input format for all testenv subengines. The testenv orchestrator calls each subengine's "delete" tool with this input.

Fields:

  • TestID: Unique identifier for the test environment instance to delete (required)
  • Metadata: Metadata from the test environment (optional, useful for cleanup)

Example:

input := DeleteInput{
    TestID:   "test-abc123",
    Metadata: map[string]string{"testenv-kind.clusterName": "myapp-test-abc123"},
}

type TestEnvArtifact

type TestEnvArtifact struct {
	TestID           string            `json:"testID"`           // Test environment ID
	Files            map[string]string `json:"files"`            // Map of logical names to relative file paths
	Metadata         map[string]string `json:"metadata"`         // Metadata for downstream consumers
	ManagedResources []string          `json:"managedResources"` // Resources to clean up
	Env              map[string]string `json:"env,omitempty"`    // Environment variables exported by this sub-engine
}

TestEnvArtifact represents the artifact returned by testenv subengine create operations.

This is the standard artifact format for all testenv subengines. The artifact is passed to the test runner and returned to the caller.

Fields:

  • TestID: Test environment ID
  • Files: Map of logical names to relative file paths (relative to TmpDir)
  • Metadata: Key-value metadata for downstream consumers
  • ManagedResources: List of resources to clean up (file paths, cluster names, etc.)
  • Env: Environment variables exported by this sub-engine (optional)

Example:

artifact := TestEnvArtifact{
    TestID: "test-abc123",
    Files: map[string]string{
        "testenv-kind.kubeconfig": "kubeconfig",
    },
    Metadata: map[string]string{
        "testenv-kind.clusterName":    "myapp-test-abc123",
        "testenv-kind.kubeconfigPath": "/tmp/forge-test-abc123/kubeconfig",
    },
    ManagedResources: []string{"/tmp/forge-test-abc123/kubeconfig"},
    Env: map[string]string{
        "KUBECONFIG": "/tmp/forge-test-abc123/kubeconfig",
    },
}

type TestEnvSubengineConfig

type TestEnvSubengineConfig struct {
	Name       string     // Engine name (e.g., "testenv-kind")
	Version    string     // Engine version
	CreateFunc CreateFunc // Create operation implementation
	DeleteFunc DeleteFunc // Delete operation implementation
}

TestEnvSubengineConfig configures testenv subengine tool registration.

Fields:

  • Name: Engine name (e.g., "testenv-kind", "testenv-lcr")
  • Version: Engine version string (e.g., "1.0.0" or git commit hash)
  • CreateFunc: The create operation implementation function
  • DeleteFunc: The delete operation implementation function

Example:

config := TestEnvSubengineConfig{
    Name:       "testenv-kind",
    Version:    "1.0.0",
    CreateFunc: myCreateFunc,
    DeleteFunc: myDeleteFunc,
}

type TestRunnerConfig

type TestRunnerConfig struct {
	Name        string         // Engine name (e.g., "go-test")
	Version     string         // Engine version
	RunTestFunc TestRunnerFunc // Test execution implementation
}

TestRunnerConfig configures test runner tool registration.

Fields:

  • Name: Engine name (e.g., "go-test", "generic-test-runner")
  • Version: Engine version string (e.g., "1.0.0" or git commit hash)
  • RunTestFunc: The test execution implementation function

Example:

config := TestRunnerConfig{
    Name:        "my-test-runner",
    Version:     "1.0.0",
    RunTestFunc: myTestRunnerFunc,
}

type TestRunnerFunc

type TestRunnerFunc func(ctx context.Context, input mcptypes.RunInput) (*forge.TestReport, error)

TestRunnerFunc is the signature for test execution.

Implementations must:

  • Validate input fields (required fields should be checked)
  • Execute tests (run test commands, collect results)
  • Return TestReport on success (with Status "passed" or "failed")
  • Return TestReport even on test failure (Status field indicates pass/fail)
  • Return error only for execution failures (not test failures)

The framework handles:

  • MCP tool registration
  • Result formatting
  • Error conversion to MCP responses
  • Report return even on test failure

IMPORTANT: Test failures are NOT errors. Return a TestReport with Status="failed". Only return error for execution failures (can't run tests, can't parse results, etc.).

Example:

func myTestRunnerFunc(ctx context.Context, input mcptypes.RunInput) (*forge.TestReport, error) {
    // Execute tests
    output, err := runTests(input.Stage)
    if err != nil {
        // Execution error - couldn't run tests
        return nil, fmt.Errorf("failed to execute tests: %w", err)
    }

    // Parse results
    report := parseTestOutput(output)

    // Return report (even if tests failed)
    // Framework will use ErrorResultWithArtifact for failed tests
    return report, nil
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL