Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func Run(args []string, stdout, stderr io.Writer) int {

rootCmd.AddCommand(scanCommand(stdout, stderr, &exitCode))
rootCmd.AddCommand(textCommand(stdout, stderr, &exitCode))
rootCmd.AddCommand(findConfigCommand(stdout, stderr, &exitCode))
rootCmd.AddCommand(versionCommand(stdout, &exitCode))

rootCmd.SetArgs(args)
Expand Down
42 changes: 42 additions & 0 deletions cmd/find_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cmd

import (
"encoding/json"
"fmt"
"io"

"github.com/chaoss/disclosure/workflow"
"github.com/spf13/cobra"
)

func findConfigCommand(stdout, stderr io.Writer, exitCode *int) *cobra.Command {
cmd := &cobra.Command{
Use: "find-config [repo-path]",
Short: "Detect configured AI settings from GitHub Actions workflows",
Args: cobra.MaximumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
repoPath := "."
if len(args) > 0 {
repoPath = args[0]
}

config, err := workflow.DetectConfigs(repoPath)
if err != nil {
fmt.Fprintf(stderr, "error reading workflows: %v\n", err)
*exitCode = ExitError
return err
}

enc := json.NewEncoder(stdout)
enc.SetIndent("", " ")
if err := enc.Encode(config); err != nil {
fmt.Fprintf(stderr, "error formatting json: %v\n", err)
*exitCode = ExitError
return err
}

return nil
},
}
return cmd
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ require (
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
103 changes: 103 additions & 0 deletions workflow/workflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package workflow

import (
"fmt"
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)

type Workflow struct {
Jobs map[string]Job `yaml:"jobs"`
}

type Job struct {
Steps []Step `yaml:"steps"`
}

type Step struct {
Uses string `yaml:"uses"`
With map[string]interface{} `yaml:"with"`
}

// ActionConfig represents the extracted configuration for a single use of the action.
type ActionConfig struct {
Label string `json:"label"`
MinConfidence string `json:"min_confidence"`
ScanPRBody string `json:"scan_pr_body"`
}

// Config represents the extracted AI configurations from workflow files.
type Config struct {
Configs []ActionConfig `json:"configs"`
}

func getString(m map[string]interface{}, key, def string) string {
if v, ok := m[key]; ok {
return fmt.Sprintf("%v", v)
}
return def
}

// DetectConfigs scans the .github/workflows directory for the chaoss/disclosure action
// and returns all configured instances. Default values are populated for missing inputs.
func DetectConfigs(repoPath string) (*Config, error) {
workflowsDir := filepath.Join(repoPath, ".github", "workflows")
entries, err := os.ReadDir(workflowsDir)
if err != nil {
if os.IsNotExist(err) {
return &Config{Configs: []ActionConfig{}}, nil
}
return nil, err
}

seenConfigs := make(map[ActionConfig]bool)

for _, entry := range entries {
if entry.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(entry.Name()))
if ext != ".yml" && ext != ".yaml" {
continue
}

path := filepath.Join(workflowsDir, entry.Name())
data, err := os.ReadFile(path)
if err != nil {
continue
}

var wf Workflow
if err := yaml.Unmarshal(data, &wf); err != nil {
continue
}

for _, job := range wf.Jobs {
for _, step := range job.Steps {
uses := strings.TrimSpace(step.Uses)
if strings.HasPrefix(uses, "chaoss/disclosure@") || uses == "chaoss/disclosure" {
ac := ActionConfig{
Label: getString(step.With, "label", "ai-detected"),
MinConfidence: getString(step.With, "min-confidence", "low"),
ScanPRBody: getString(step.With, "scan-pr-body", "true"),
}
seenConfigs[ac] = true
}
}
}
}

var configs []ActionConfig
for c := range seenConfigs {
configs = append(configs, c)
}

if configs == nil {
configs = []ActionConfig{}
}

return &Config{Configs: configs}, nil
}
99 changes: 99 additions & 0 deletions workflow/workflow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package workflow

import (
"os"
"path/filepath"
"testing"
)

func TestDetectConfigs(t *testing.T) {
tempDir := t.TempDir()
workflowsDir := filepath.Join(tempDir, ".github", "workflows")
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatal(err)
}

yamlContent := `
jobs:
test:
steps:
- uses: actions/checkout@v4
- uses: chaoss/disclosure@main
with:
label: custom-ai-label
min-confidence: medium
scan-pr-body: "false"
`
if err := os.WriteFile(filepath.Join(workflowsDir, "test.yml"), []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}

config, err := DetectConfigs(tempDir)
if err != nil {
t.Fatal(err)
}

if len(config.Configs) != 1 {
t.Fatalf("expected 1 config, got %d", len(config.Configs))
}

ac := config.Configs[0]
if ac.Label != "custom-ai-label" {
t.Errorf("expected label custom-ai-label, got %v", ac.Label)
}
if ac.MinConfidence != "medium" {
t.Errorf("expected min-confidence medium, got %v", ac.MinConfidence)
}
if ac.ScanPRBody != "false" {
t.Errorf("expected scan-pr-body false, got %v", ac.ScanPRBody)
}
}

func TestDetectConfigsDefault(t *testing.T) {
tempDir := t.TempDir()
workflowsDir := filepath.Join(tempDir, ".github", "workflows")
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatal(err)
}

yamlContent := `
jobs:
test:
steps:
- uses: chaoss/disclosure@v1
`
if err := os.WriteFile(filepath.Join(workflowsDir, "test.yml"), []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}

config, err := DetectConfigs(tempDir)
if err != nil {
t.Fatal(err)
}

if len(config.Configs) != 1 {
t.Fatalf("expected 1 config, got %d", len(config.Configs))
}

ac := config.Configs[0]
if ac.Label != "ai-detected" {
t.Errorf("expected label ai-detected, got %v", ac.Label)
}
if ac.MinConfidence != "low" {
t.Errorf("expected min-confidence low, got %v", ac.MinConfidence)
}
if ac.ScanPRBody != "true" {
t.Errorf("expected scan-pr-body true, got %v", ac.ScanPRBody)
}
}

func TestDetectConfigsNoWorkflows(t *testing.T) {
tempDir := t.TempDir()
config, err := DetectConfigs(tempDir)
if err != nil {
t.Fatal(err)
}
if len(config.Configs) != 0 {
t.Fatalf("expected empty configs, got %v", config.Configs)
}
}
Loading