- 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
119 lines
2.8 KiB
Go
119 lines
2.8 KiB
Go
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
|
|
}
|