From a45fcbe1b94515e5c30877d04b9b59aa442ffda2 Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Sat, 30 May 2026 12:57:22 +0530 Subject: [PATCH 1/2] feat: enable self-detection of actions config via CLI Signed-off-by: abhinavgautam01 --- cmd/actions.go | 42 +++++++++++++++++++ cmd/cmd.go | 1 + go.mod | 1 + workflow/workflow.go | 88 +++++++++++++++++++++++++++++++++++++++ workflow/workflow_test.go | 75 +++++++++++++++++++++++++++++++++ 5 files changed, 207 insertions(+) create mode 100644 cmd/actions.go create mode 100644 workflow/workflow.go create mode 100644 workflow/workflow_test.go diff --git a/cmd/actions.go b/cmd/actions.go new file mode 100644 index 0000000..e240cb2 --- /dev/null +++ b/cmd/actions.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/chaoss/disclosure/workflow" + "github.com/spf13/cobra" +) + +func actionsCommand(stdout, stderr io.Writer, exitCode *int) *cobra.Command { + cmd := &cobra.Command{ + Use: "actions [repo-path]", + Short: "Detect configured AI labels 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.DetectLabels(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 +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 4b9bd7c..554044b 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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(actionsCommand(stdout, stderr, &exitCode)) rootCmd.AddCommand(versionCommand(stdout, &exitCode)) rootCmd.SetArgs(args) diff --git a/go.mod b/go.mod index fa75ae6..65a4556 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/workflow/workflow.go b/workflow/workflow.go new file mode 100644 index 0000000..6615798 --- /dev/null +++ b/workflow/workflow.go @@ -0,0 +1,88 @@ +package workflow + +import ( + "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]string `yaml:"with"` +} + +// Config represents the extracted AI labels from workflow files. +type Config struct { + Labels []string `json:"labels"` +} + +// DetectLabels scans the .github/workflows directory for the chaoss/disclosure action +// and returns all configured labels. If the action is found but no label is specified, +// it returns the default "ai-detected". +func DetectLabels(repoPath string) (*Config, error) { + workflowsDir := filepath.Join(repoPath, ".github", "workflows") + entries, err := os.ReadDir(workflowsDir) + if err != nil { + if os.IsNotExist(err) { + return &Config{Labels: []string{}}, nil + } + return nil, err + } + + seenLabels := make(map[string]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" { + label := step.With["label"] + if label == "" { + label = "ai-detected" + } + seenLabels[label] = true + } + } + } + } + + var labels []string + for l := range seenLabels { + labels = append(labels, l) + } + + if labels == nil { + labels = []string{} // return empty slice rather than null in json + } + + return &Config{Labels: labels}, nil +} diff --git a/workflow/workflow_test.go b/workflow/workflow_test.go new file mode 100644 index 0000000..e3c85f0 --- /dev/null +++ b/workflow/workflow_test.go @@ -0,0 +1,75 @@ +package workflow + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDetectLabels(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 +` + if err := os.WriteFile(filepath.Join(workflowsDir, "test.yml"), []byte(yamlContent), 0644); err != nil { + t.Fatal(err) + } + + config, err := DetectLabels(tempDir) + if err != nil { + t.Fatal(err) + } + + if len(config.Labels) != 1 || config.Labels[0] != "custom-ai-label" { + t.Fatalf("expected [custom-ai-label], got %v", config.Labels) + } +} + +func TestDetectLabelsDefault(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 := DetectLabels(tempDir) + if err != nil { + t.Fatal(err) + } + + if len(config.Labels) != 1 || config.Labels[0] != "ai-detected" { + t.Fatalf("expected [ai-detected], got %v", config.Labels) + } +} + +func TestDetectLabelsNoWorkflows(t *testing.T) { + tempDir := t.TempDir() + config, err := DetectLabels(tempDir) + if err != nil { + t.Fatal(err) + } + if len(config.Labels) != 0 { + t.Fatalf("expected [], got %v", config.Labels) + } +} From d8ff8021ed1a37652fe027e4faf094201e373dc8 Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Sun, 31 May 2026 08:40:20 +0530 Subject: [PATCH 2/2] refactor: rename command to find-config and surface all action parameters Signed-off-by: abhinavgautam01 --- cmd/cmd.go | 2 +- cmd/{actions.go => find_config.go} | 8 ++--- workflow/workflow.go | 55 +++++++++++++++++++----------- workflow/workflow_test.go | 48 +++++++++++++++++++------- 4 files changed, 76 insertions(+), 37 deletions(-) rename cmd/{actions.go => find_config.go} (73%) diff --git a/cmd/cmd.go b/cmd/cmd.go index 554044b..2a8748c 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -50,7 +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(actionsCommand(stdout, stderr, &exitCode)) + rootCmd.AddCommand(findConfigCommand(stdout, stderr, &exitCode)) rootCmd.AddCommand(versionCommand(stdout, &exitCode)) rootCmd.SetArgs(args) diff --git a/cmd/actions.go b/cmd/find_config.go similarity index 73% rename from cmd/actions.go rename to cmd/find_config.go index e240cb2..a600ad0 100644 --- a/cmd/actions.go +++ b/cmd/find_config.go @@ -9,10 +9,10 @@ import ( "github.com/spf13/cobra" ) -func actionsCommand(stdout, stderr io.Writer, exitCode *int) *cobra.Command { +func findConfigCommand(stdout, stderr io.Writer, exitCode *int) *cobra.Command { cmd := &cobra.Command{ - Use: "actions [repo-path]", - Short: "Detect configured AI labels from GitHub Actions workflows", + 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 := "." @@ -20,7 +20,7 @@ func actionsCommand(stdout, stderr io.Writer, exitCode *int) *cobra.Command { repoPath = args[0] } - config, err := workflow.DetectLabels(repoPath) + config, err := workflow.DetectConfigs(repoPath) if err != nil { fmt.Fprintf(stderr, "error reading workflows: %v\n", err) *exitCode = ExitError diff --git a/workflow/workflow.go b/workflow/workflow.go index 6615798..ba723e4 100644 --- a/workflow/workflow.go +++ b/workflow/workflow.go @@ -1,6 +1,7 @@ package workflow import ( + "fmt" "os" "path/filepath" "strings" @@ -17,29 +18,42 @@ type Job struct { } type Step struct { - Uses string `yaml:"uses"` - With map[string]string `yaml:"with"` + Uses string `yaml:"uses"` + With map[string]interface{} `yaml:"with"` } -// Config represents the extracted AI labels from workflow files. +// 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 { - Labels []string `json:"labels"` + 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 } -// DetectLabels scans the .github/workflows directory for the chaoss/disclosure action -// and returns all configured labels. If the action is found but no label is specified, -// it returns the default "ai-detected". -func DetectLabels(repoPath string) (*Config, error) { +// 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{Labels: []string{}}, nil + return &Config{Configs: []ActionConfig{}}, nil } return nil, err } - seenLabels := make(map[string]bool) + seenConfigs := make(map[ActionConfig]bool) for _, entry := range entries { if entry.IsDir() { @@ -65,24 +79,25 @@ func DetectLabels(repoPath string) (*Config, error) { for _, step := range job.Steps { uses := strings.TrimSpace(step.Uses) if strings.HasPrefix(uses, "chaoss/disclosure@") || uses == "chaoss/disclosure" { - label := step.With["label"] - if label == "" { - label = "ai-detected" + ac := ActionConfig{ + Label: getString(step.With, "label", "ai-detected"), + MinConfidence: getString(step.With, "min-confidence", "low"), + ScanPRBody: getString(step.With, "scan-pr-body", "true"), } - seenLabels[label] = true + seenConfigs[ac] = true } } } } - var labels []string - for l := range seenLabels { - labels = append(labels, l) + var configs []ActionConfig + for c := range seenConfigs { + configs = append(configs, c) } - if labels == nil { - labels = []string{} // return empty slice rather than null in json + if configs == nil { + configs = []ActionConfig{} } - return &Config{Labels: labels}, nil + return &Config{Configs: configs}, nil } diff --git a/workflow/workflow_test.go b/workflow/workflow_test.go index e3c85f0..364de02 100644 --- a/workflow/workflow_test.go +++ b/workflow/workflow_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestDetectLabels(t *testing.T) { +func TestDetectConfigs(t *testing.T) { tempDir := t.TempDir() workflowsDir := filepath.Join(tempDir, ".github", "workflows") if err := os.MkdirAll(workflowsDir, 0755); err != nil { @@ -21,22 +21,35 @@ jobs: - 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 := DetectLabels(tempDir) + config, err := DetectConfigs(tempDir) if err != nil { t.Fatal(err) } - if len(config.Labels) != 1 || config.Labels[0] != "custom-ai-label" { - t.Fatalf("expected [custom-ai-label], got %v", config.Labels) + 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 TestDetectLabelsDefault(t *testing.T) { +func TestDetectConfigsDefault(t *testing.T) { tempDir := t.TempDir() workflowsDir := filepath.Join(tempDir, ".github", "workflows") if err := os.MkdirAll(workflowsDir, 0755); err != nil { @@ -53,23 +66,34 @@ jobs: t.Fatal(err) } - config, err := DetectLabels(tempDir) + config, err := DetectConfigs(tempDir) if err != nil { t.Fatal(err) } - if len(config.Labels) != 1 || config.Labels[0] != "ai-detected" { - t.Fatalf("expected [ai-detected], got %v", config.Labels) + 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 TestDetectLabelsNoWorkflows(t *testing.T) { +func TestDetectConfigsNoWorkflows(t *testing.T) { tempDir := t.TempDir() - config, err := DetectLabels(tempDir) + config, err := DetectConfigs(tempDir) if err != nil { t.Fatal(err) } - if len(config.Labels) != 0 { - t.Fatalf("expected [], got %v", config.Labels) + if len(config.Configs) != 0 { + t.Fatalf("expected empty configs, got %v", config.Configs) } }