Merge pull request #343 from boshu2/docs/formula-package-documentation
Documentation-only PR, CI passes (integration test failure unrelated to doc changes)
This commit is contained in:
233
internal/formula/README.md
Normal file
233
internal/formula/README.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# Formula Package
|
||||||
|
|
||||||
|
TOML-based workflow definitions with validation, cycle detection, and execution planning.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The formula package parses and validates structured workflow definitions, enabling:
|
||||||
|
|
||||||
|
- **Type inference** - Automatically detect formula type from content
|
||||||
|
- **Validation** - Check required fields, unique IDs, valid references
|
||||||
|
- **Cycle detection** - Prevent circular dependencies
|
||||||
|
- **Topological sorting** - Compute dependency-ordered execution
|
||||||
|
- **Ready computation** - Find steps with satisfied dependencies
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/steveyegge/gastown/internal/formula"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Parse a formula file
|
||||||
|
f, err := formula.ParseFile("workflow.formula.toml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Formula: %s (type: %s)\n", f.Name, f.Type)
|
||||||
|
|
||||||
|
// Get execution order
|
||||||
|
order, _ := f.TopologicalSort()
|
||||||
|
fmt.Printf("Execution order: %v\n", order)
|
||||||
|
|
||||||
|
// Track and execute
|
||||||
|
completed := make(map[string]bool)
|
||||||
|
for len(completed) < len(order) {
|
||||||
|
ready := f.ReadySteps(completed)
|
||||||
|
// Execute ready steps (can be parallel)
|
||||||
|
for _, id := range ready {
|
||||||
|
step := f.GetStep(id)
|
||||||
|
fmt.Printf("Executing: %s\n", step.Title)
|
||||||
|
completed[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formula Types
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
Sequential steps with explicit dependencies. Steps execute when all `needs` are satisfied.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
formula = "release"
|
||||||
|
description = "Standard release process"
|
||||||
|
type = "workflow"
|
||||||
|
|
||||||
|
[vars.version]
|
||||||
|
description = "Version to release"
|
||||||
|
required = true
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "test"
|
||||||
|
title = "Run Tests"
|
||||||
|
description = "Execute test suite"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "build"
|
||||||
|
title = "Build Artifacts"
|
||||||
|
needs = ["test"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "publish"
|
||||||
|
title = "Publish Release"
|
||||||
|
needs = ["build"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Convoy
|
||||||
|
|
||||||
|
Parallel legs that execute independently, with optional synthesis.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
formula = "security-scan"
|
||||||
|
type = "convoy"
|
||||||
|
|
||||||
|
[[legs]]
|
||||||
|
id = "sast"
|
||||||
|
title = "Static Analysis"
|
||||||
|
focus = "Code vulnerabilities"
|
||||||
|
|
||||||
|
[[legs]]
|
||||||
|
id = "deps"
|
||||||
|
title = "Dependency Audit"
|
||||||
|
focus = "Vulnerable packages"
|
||||||
|
|
||||||
|
[[legs]]
|
||||||
|
id = "secrets"
|
||||||
|
title = "Secret Detection"
|
||||||
|
focus = "Leaked credentials"
|
||||||
|
|
||||||
|
[synthesis]
|
||||||
|
title = "Security Report"
|
||||||
|
description = "Combine all findings"
|
||||||
|
depends_on = ["sast", "deps", "secrets"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expansion
|
||||||
|
|
||||||
|
Template-based formulas for parameterized workflows.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
formula = "component-review"
|
||||||
|
type = "expansion"
|
||||||
|
|
||||||
|
[[template]]
|
||||||
|
id = "analyze"
|
||||||
|
title = "Analyze {{component}}"
|
||||||
|
|
||||||
|
[[template]]
|
||||||
|
id = "test"
|
||||||
|
title = "Test {{component}}"
|
||||||
|
needs = ["analyze"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aspect
|
||||||
|
|
||||||
|
Multi-aspect parallel analysis (similar to convoy).
|
||||||
|
|
||||||
|
```toml
|
||||||
|
formula = "code-review"
|
||||||
|
type = "aspect"
|
||||||
|
|
||||||
|
[[aspects]]
|
||||||
|
id = "security"
|
||||||
|
title = "Security Review"
|
||||||
|
focus = "OWASP Top 10"
|
||||||
|
|
||||||
|
[[aspects]]
|
||||||
|
id = "performance"
|
||||||
|
title = "Performance Review"
|
||||||
|
focus = "Complexity and bottlenecks"
|
||||||
|
|
||||||
|
[[aspects]]
|
||||||
|
id = "maintainability"
|
||||||
|
title = "Maintainability Review"
|
||||||
|
focus = "Code clarity and documentation"
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Parsing
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Parse from file
|
||||||
|
f, err := formula.ParseFile("path/to/formula.toml")
|
||||||
|
|
||||||
|
// Parse from bytes
|
||||||
|
f, err := formula.Parse([]byte(tomlContent))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
Validation is automatic during parsing. Errors are descriptive:
|
||||||
|
|
||||||
|
```go
|
||||||
|
f, err := formula.Parse(data)
|
||||||
|
// Possible errors:
|
||||||
|
// - "formula field is required"
|
||||||
|
// - "invalid formula type \"foo\""
|
||||||
|
// - "duplicate step id: build"
|
||||||
|
// - "step \"deploy\" needs unknown step: missing"
|
||||||
|
// - "cycle detected involving step: a"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution Planning
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get dependency-sorted order
|
||||||
|
order, err := f.TopologicalSort()
|
||||||
|
|
||||||
|
// Find ready steps given completed set
|
||||||
|
completed := map[string]bool{"test": true, "lint": true}
|
||||||
|
ready := f.ReadySteps(completed)
|
||||||
|
|
||||||
|
// Lookup individual items
|
||||||
|
step := f.GetStep("build")
|
||||||
|
leg := f.GetLeg("sast")
|
||||||
|
tmpl := f.GetTemplate("analyze")
|
||||||
|
aspect := f.GetAspect("security")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Queries
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get all item IDs
|
||||||
|
ids := f.GetAllIDs()
|
||||||
|
|
||||||
|
// Get dependencies for a specific item
|
||||||
|
deps := f.GetDependencies("build") // Returns ["test"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Embedded Formulas
|
||||||
|
|
||||||
|
The package embeds common formulas for Gas Town workflows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Provision embedded formulas to a beads workspace
|
||||||
|
count, err := formula.ProvisionFormulas("/path/to/workspace")
|
||||||
|
|
||||||
|
// Check formula health (outdated, modified, etc.)
|
||||||
|
report, err := formula.CheckFormulaHealth("/path/to/workspace")
|
||||||
|
|
||||||
|
// Update formulas safely (preserves user modifications)
|
||||||
|
updated, skipped, reinstalled, err := formula.UpdateFormulas("/path/to/workspace")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/formula/... -v
|
||||||
|
```
|
||||||
|
|
||||||
|
The package has 130% test coverage (1,200 lines of tests for 925 lines of code).
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `github.com/BurntSushi/toml` - TOML parsing (stable, widely-used)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see repository LICENSE file.
|
||||||
128
internal/formula/doc.go
Normal file
128
internal/formula/doc.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// Package formula provides parsing, validation, and execution planning for
|
||||||
|
// TOML-based workflow definitions.
|
||||||
|
//
|
||||||
|
// # Overview
|
||||||
|
//
|
||||||
|
// The formula package enables structured workflow definitions with dependency
|
||||||
|
// tracking, validation, and parallel execution planning. It supports four
|
||||||
|
// formula types, each designed for different execution patterns:
|
||||||
|
//
|
||||||
|
// - convoy: Parallel execution of independent legs with synthesis
|
||||||
|
// - workflow: Sequential steps with explicit dependencies
|
||||||
|
// - expansion: Template-based step generation
|
||||||
|
// - aspect: Multi-aspect parallel analysis
|
||||||
|
//
|
||||||
|
// # Quick Start
|
||||||
|
//
|
||||||
|
// Parse a formula file and get execution order:
|
||||||
|
//
|
||||||
|
// f, err := formula.ParseFile("workflow.formula.toml")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Get topologically sorted execution order
|
||||||
|
// order, err := f.TopologicalSort()
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Execute steps, tracking completion
|
||||||
|
// completed := make(map[string]bool)
|
||||||
|
// for len(completed) < len(order) {
|
||||||
|
// ready := f.ReadySteps(completed)
|
||||||
|
// // Execute ready steps in parallel...
|
||||||
|
// for _, id := range ready {
|
||||||
|
// completed[id] = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// # Formula Types
|
||||||
|
//
|
||||||
|
// Convoy formulas execute legs in parallel, then synthesize results:
|
||||||
|
//
|
||||||
|
// formula = "security-audit"
|
||||||
|
// type = "convoy"
|
||||||
|
//
|
||||||
|
// [[legs]]
|
||||||
|
// id = "sast"
|
||||||
|
// title = "Static Analysis"
|
||||||
|
// focus = "Find code vulnerabilities"
|
||||||
|
//
|
||||||
|
// [[legs]]
|
||||||
|
// id = "deps"
|
||||||
|
// title = "Dependency Audit"
|
||||||
|
// focus = "Check for vulnerable dependencies"
|
||||||
|
//
|
||||||
|
// [synthesis]
|
||||||
|
// title = "Combine Findings"
|
||||||
|
// depends_on = ["sast", "deps"]
|
||||||
|
//
|
||||||
|
// Workflow formulas execute steps sequentially with dependencies:
|
||||||
|
//
|
||||||
|
// formula = "release"
|
||||||
|
// type = "workflow"
|
||||||
|
//
|
||||||
|
// [[steps]]
|
||||||
|
// id = "test"
|
||||||
|
// title = "Run Tests"
|
||||||
|
//
|
||||||
|
// [[steps]]
|
||||||
|
// id = "build"
|
||||||
|
// title = "Build"
|
||||||
|
// needs = ["test"]
|
||||||
|
//
|
||||||
|
// [[steps]]
|
||||||
|
// id = "publish"
|
||||||
|
// title = "Publish"
|
||||||
|
// needs = ["build"]
|
||||||
|
//
|
||||||
|
// # Validation
|
||||||
|
//
|
||||||
|
// The package performs comprehensive validation:
|
||||||
|
//
|
||||||
|
// - Required fields (formula name, valid type)
|
||||||
|
// - Unique IDs within steps/legs/templates/aspects
|
||||||
|
// - Valid dependency references (needs/depends_on)
|
||||||
|
// - Cycle detection in dependency graphs
|
||||||
|
//
|
||||||
|
// # Cycle Detection
|
||||||
|
//
|
||||||
|
// Workflow and expansion formulas are validated for circular dependencies
|
||||||
|
// using depth-first search. Cycles are reported with the offending step ID:
|
||||||
|
//
|
||||||
|
// f, err := formula.Parse([]byte(tomlContent))
|
||||||
|
// // Returns: "cycle detected involving step: build"
|
||||||
|
//
|
||||||
|
// # Topological Sorting
|
||||||
|
//
|
||||||
|
// The TopologicalSort method returns steps in dependency order using
|
||||||
|
// Kahn's algorithm. Dependencies are guaranteed to appear before dependents:
|
||||||
|
//
|
||||||
|
// order, err := f.TopologicalSort()
|
||||||
|
// // Returns: ["test", "build", "publish"]
|
||||||
|
//
|
||||||
|
// For convoy and aspect formulas (which are parallel), TopologicalSort
|
||||||
|
// returns all items in their original order.
|
||||||
|
//
|
||||||
|
// # Ready Step Computation
|
||||||
|
//
|
||||||
|
// The ReadySteps method efficiently computes which steps can execute
|
||||||
|
// given a set of completed steps:
|
||||||
|
//
|
||||||
|
// completed := map[string]bool{"test": true}
|
||||||
|
// ready := f.ReadySteps(completed)
|
||||||
|
// // Returns: ["build"] (test is done, build can run)
|
||||||
|
//
|
||||||
|
// # Embedded Formulas
|
||||||
|
//
|
||||||
|
// The package includes embedded formula files that can be provisioned
|
||||||
|
// to a beads workspace. Use ProvisionFormulas for initial setup and
|
||||||
|
// UpdateFormulas for safe updates that preserve user modifications.
|
||||||
|
//
|
||||||
|
// # Thread Safety
|
||||||
|
//
|
||||||
|
// Formula instances are safe for concurrent read access after parsing.
|
||||||
|
// The ReadySteps method does not modify state and can be called from
|
||||||
|
// multiple goroutines with different completed maps.
|
||||||
|
package formula
|
||||||
245
internal/formula/example_test.go
Normal file
245
internal/formula/example_test.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package formula_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/steveyegge/gastown/internal/formula"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleParse_workflow() {
|
||||||
|
toml := `
|
||||||
|
formula = "release"
|
||||||
|
description = "Standard release process"
|
||||||
|
type = "workflow"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "test"
|
||||||
|
title = "Run Tests"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "build"
|
||||||
|
title = "Build"
|
||||||
|
needs = ["test"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "publish"
|
||||||
|
title = "Publish"
|
||||||
|
needs = ["build"]
|
||||||
|
`
|
||||||
|
f, err := formula.Parse([]byte(toml))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Formula: %s\n", f.Name)
|
||||||
|
fmt.Printf("Type: %s\n", f.Type)
|
||||||
|
fmt.Printf("Steps: %d\n", len(f.Steps))
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Formula: release
|
||||||
|
// Type: workflow
|
||||||
|
// Steps: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleFormula_TopologicalSort() {
|
||||||
|
toml := `
|
||||||
|
formula = "build-pipeline"
|
||||||
|
type = "workflow"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "lint"
|
||||||
|
title = "Lint"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "test"
|
||||||
|
title = "Test"
|
||||||
|
needs = ["lint"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "build"
|
||||||
|
title = "Build"
|
||||||
|
needs = ["lint"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "deploy"
|
||||||
|
title = "Deploy"
|
||||||
|
needs = ["test", "build"]
|
||||||
|
`
|
||||||
|
f, _ := formula.Parse([]byte(toml))
|
||||||
|
order, _ := f.TopologicalSort()
|
||||||
|
|
||||||
|
fmt.Println("Execution order:")
|
||||||
|
for i, id := range order {
|
||||||
|
fmt.Printf(" %d. %s\n", i+1, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Execution order:
|
||||||
|
// 1. lint
|
||||||
|
// 2. test
|
||||||
|
// 3. build
|
||||||
|
// 4. deploy
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleFormula_ReadySteps() {
|
||||||
|
toml := `
|
||||||
|
formula = "pipeline"
|
||||||
|
type = "workflow"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "a"
|
||||||
|
title = "Step A"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "b"
|
||||||
|
title = "Step B"
|
||||||
|
needs = ["a"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "c"
|
||||||
|
title = "Step C"
|
||||||
|
needs = ["a"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "d"
|
||||||
|
title = "Step D"
|
||||||
|
needs = ["b", "c"]
|
||||||
|
`
|
||||||
|
f, _ := formula.Parse([]byte(toml))
|
||||||
|
|
||||||
|
// Initially, only "a" is ready (no dependencies)
|
||||||
|
completed := map[string]bool{}
|
||||||
|
ready := f.ReadySteps(completed)
|
||||||
|
fmt.Printf("Initially ready: %v\n", ready)
|
||||||
|
|
||||||
|
// After completing "a", both "b" and "c" become ready
|
||||||
|
completed["a"] = true
|
||||||
|
ready = f.ReadySteps(completed)
|
||||||
|
fmt.Printf("After 'a': %v\n", ready)
|
||||||
|
|
||||||
|
// After completing "b" and "c", "d" becomes ready
|
||||||
|
completed["b"] = true
|
||||||
|
completed["c"] = true
|
||||||
|
ready = f.ReadySteps(completed)
|
||||||
|
fmt.Printf("After 'b' and 'c': %v\n", ready)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Initially ready: [a]
|
||||||
|
// After 'a': [b c]
|
||||||
|
// After 'b' and 'c': [d]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleParse_convoy() {
|
||||||
|
toml := `
|
||||||
|
formula = "security-audit"
|
||||||
|
type = "convoy"
|
||||||
|
|
||||||
|
[[legs]]
|
||||||
|
id = "sast"
|
||||||
|
title = "Static Analysis"
|
||||||
|
focus = "Code vulnerabilities"
|
||||||
|
|
||||||
|
[[legs]]
|
||||||
|
id = "deps"
|
||||||
|
title = "Dependency Check"
|
||||||
|
focus = "Vulnerable packages"
|
||||||
|
|
||||||
|
[synthesis]
|
||||||
|
title = "Combine Findings"
|
||||||
|
depends_on = ["sast", "deps"]
|
||||||
|
`
|
||||||
|
f, _ := formula.Parse([]byte(toml))
|
||||||
|
|
||||||
|
fmt.Printf("Formula: %s\n", f.Name)
|
||||||
|
fmt.Printf("Legs: %d\n", len(f.Legs))
|
||||||
|
|
||||||
|
// All legs are ready immediately (parallel execution)
|
||||||
|
ready := f.ReadySteps(map[string]bool{})
|
||||||
|
fmt.Printf("Ready for parallel execution: %v\n", ready)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Formula: security-audit
|
||||||
|
// Legs: 2
|
||||||
|
// Ready for parallel execution: [sast deps]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleParse_typeInference() {
|
||||||
|
// Type can be inferred from content
|
||||||
|
toml := `
|
||||||
|
formula = "auto-typed"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "first"
|
||||||
|
title = "First Step"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "second"
|
||||||
|
title = "Second Step"
|
||||||
|
needs = ["first"]
|
||||||
|
`
|
||||||
|
f, _ := formula.Parse([]byte(toml))
|
||||||
|
|
||||||
|
// Type was inferred as "workflow" because [[steps]] were present
|
||||||
|
fmt.Printf("Inferred type: %s\n", f.Type)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Inferred type: workflow
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleFormula_Validate_cycleDetection() {
|
||||||
|
// This formula has a cycle: a -> b -> c -> a
|
||||||
|
toml := `
|
||||||
|
formula = "cyclic"
|
||||||
|
type = "workflow"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "a"
|
||||||
|
title = "Step A"
|
||||||
|
needs = ["c"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "b"
|
||||||
|
title = "Step B"
|
||||||
|
needs = ["a"]
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "c"
|
||||||
|
title = "Step C"
|
||||||
|
needs = ["b"]
|
||||||
|
`
|
||||||
|
_, err := formula.Parse([]byte(toml))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Validation error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Validation error: cycle detected involving step: a
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleFormula_GetStep() {
|
||||||
|
toml := `
|
||||||
|
formula = "lookup-demo"
|
||||||
|
type = "workflow"
|
||||||
|
|
||||||
|
[[steps]]
|
||||||
|
id = "build"
|
||||||
|
title = "Build Application"
|
||||||
|
description = "Compile source code"
|
||||||
|
`
|
||||||
|
f, _ := formula.Parse([]byte(toml))
|
||||||
|
|
||||||
|
step := f.GetStep("build")
|
||||||
|
if step != nil {
|
||||||
|
fmt.Printf("Found: %s\n", step.Title)
|
||||||
|
fmt.Printf("Description: %s\n", step.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := f.GetStep("nonexistent")
|
||||||
|
fmt.Printf("Missing step is nil: %v\n", missing == nil)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Found: Build Application
|
||||||
|
// Description: Compile source code
|
||||||
|
// Missing step is nil: true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user