feat: extract coverage-gate action from Cue for reuse across projects
- Move coveragegate tool from cue/tools to vociferate/coverage-gate - Create composite action with JSON metrics output for CI - Update tool to export passes/total_coverage/packages_checked/packages_failed - Support per-package threshold policy via JSON configuration - Change module path to git.hrafn.xyz/aether/vociferate/coverage-gate
This commit is contained in:
118
coverage-gate/main.go
Normal file
118
coverage-gate/main.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type discoverPackagesFunc func(srcRoot string) ([]string, error)
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, discoverPackages))
|
||||
}
|
||||
|
||||
func run(args []string, stdout io.Writer, stderr io.Writer, discover discoverPackagesFunc) int {
|
||||
fs := flag.NewFlagSet("coveragegate", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
|
||||
profilePath := fs.String("profile", "../_build/coverage.out", "path to go coverprofile")
|
||||
policyPath := fs.String("policy", "../specs/003-testing-time/contracts/coverage-thresholds.json", "path to coverage policy json")
|
||||
srcRoot := fs.String("src-root", ".", "path to src workspace root")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
|
||||
policy, err := LoadPolicy(*policyPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "coverage gate: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
aggByPkg, err := ParseCoverProfile(*profilePath, policy)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "coverage gate: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
pkgs, err := discover(*srcRoot)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "coverage gate: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
results := EvaluateCoverage(pkgs, aggByPkg, policy)
|
||||
if len(results) == 0 {
|
||||
fmt.Fprintln(stderr, "coverage gate: no in-scope packages found")
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Fprintln(stdout, "Package coverage results:")
|
||||
fmt.Fprintln(stdout, "PACKAGE\tCOVERAGE\tTHRESHOLD\tSTATUS")
|
||||
|
||||
failed := false
|
||||
totalCoverage := 0.0
|
||||
for _, r := range results {
|
||||
status := "PASS"
|
||||
if !r.Pass {
|
||||
status = "FAIL"
|
||||
failed = true
|
||||
}
|
||||
fmt.Fprintf(stdout, "%s\t%.2f%%\t%.2f%%\t%s\n", r.Package, r.Percent, r.Threshold, status)
|
||||
totalCoverage += r.Percent
|
||||
}
|
||||
if len(results) > 0 {
|
||||
totalCoverage /= float64(len(results))
|
||||
}
|
||||
|
||||
packagesFailed := 0
|
||||
for _, r := range results {
|
||||
if !r.Pass {
|
||||
packagesFailed++
|
||||
}
|
||||
}
|
||||
|
||||
// Output JSON metrics for CI consumption
|
||||
metrics := map[string]interface{}{
|
||||
"passed": !failed,
|
||||
"total_coverage": fmt.Sprintf("%.2f", totalCoverage),
|
||||
"packages_checked": len(results),
|
||||
"packages_failed": packagesFailed,
|
||||
}
|
||||
metricsJSON, _ := json.Marshal(metrics)
|
||||
fmt.Fprintln(stdout, string(metricsJSON))
|
||||
|
||||
if failed {
|
||||
fmt.Fprintln(stderr, "coverage gate: one or more packages are below threshold")
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintln(stdout, "coverage gate: all in-scope packages meet threshold")
|
||||
return 0
|
||||
}
|
||||
|
||||
func discoverPackages(srcRoot string) ([]string, error) {
|
||||
cmd := exec.Command("go", "list", "./...")
|
||||
cmd.Dir = srcRoot
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("discover packages with go list: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
pkgs := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
pkgs = append(pkgs, line)
|
||||
}
|
||||
sort.Strings(pkgs)
|
||||
return pkgs, nil
|
||||
}
|
||||
Reference in New Issue
Block a user