From 3732962c87ebc27cd88fe1f28c9ef868fcef7303 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:13:06 +0000 Subject: [PATCH 1/6] Initial plan From 359744cb7b7dda48be0ed36e869da3cf89de9bba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:26:59 +0000 Subject: [PATCH 2/6] Add repository feature validation for discussions and issues Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler.go | 15 ++ .../repository_features_validation_test.go | 151 ++++++++++++++++++ pkg/workflow/validation.go | 150 +++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 pkg/workflow/repository_features_validation_test.go diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index dc2119cb539..d1253d1cf0e 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -362,6 +362,21 @@ func (c *Compiler) CompileWorkflow(markdownPath string) error { }) return errors.New(formattedErr) } + + // Validate repository features (discussions, issues) + log.Print("Validating repository features") + if err := c.validateRepositoryFeatures(workflowData); err != nil { + formattedErr := console.FormatError(console.CompilerError{ + Position: console.ErrorPosition{ + File: markdownPath, + Line: 1, + Column: 1, + }, + Type: "error", + Message: fmt.Sprintf("repository feature validation failed: %v", err), + }) + return errors.New(formattedErr) + } } else if c.verbose { fmt.Println(console.FormatWarningMessage("Schema validation available but skipped (use SetSkipValidation(false) to enable)")) c.IncrementWarningCount() diff --git a/pkg/workflow/repository_features_validation_test.go b/pkg/workflow/repository_features_validation_test.go new file mode 100644 index 00000000000..37a209c416f --- /dev/null +++ b/pkg/workflow/repository_features_validation_test.go @@ -0,0 +1,151 @@ +package workflow + +import ( + "strings" + "testing" +) + +func TestValidateRepositoryFeatures(t *testing.T) { + tests := []struct { + name string + workflowData *WorkflowData + expectError bool + description string + }{ + { + name: "no safe-outputs configured", + workflowData: &WorkflowData{ + SafeOutputs: nil, + }, + expectError: false, + description: "should pass when no safe-outputs are configured", + }, + { + name: "safe-outputs without discussions or issues", + workflowData: &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + AddComments: &AddCommentsConfig{}, + }, + }, + expectError: false, + description: "should pass when safe-outputs don't require discussions or issues", + }, + { + name: "create-discussion configured", + workflowData: &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + CreateDiscussions: &CreateDiscussionsConfig{}, + }, + }, + expectError: false, // Will not error if getCurrentRepository fails or API call fails + description: "validation will check discussions but won't fail on API errors", + }, + { + name: "create-issue configured", + workflowData: &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{}, + }, + }, + expectError: false, // Will not error if getCurrentRepository fails or API call fails + description: "validation will check issues but won't fail on API errors", + }, + { + name: "both discussions and issues configured", + workflowData: &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + CreateDiscussions: &CreateDiscussionsConfig{}, + CreateIssues: &CreateIssuesConfig{}, + }, + }, + expectError: false, // Will not error if getCurrentRepository fails or API call fails + description: "validation will check both features but won't fail on API errors", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler := NewCompiler(false, "", "test") + err := compiler.validateRepositoryFeatures(tt.workflowData) + + if tt.expectError && err == nil { + t.Errorf("%s: expected error but got none", tt.description) + } + if !tt.expectError && err != nil { + t.Errorf("%s: unexpected error: %v", tt.description, err) + } + }) + } +} + +func TestGetCurrentRepository(t *testing.T) { + // This test will only pass when running in a git repository with GitHub remote + // It's expected to fail in non-git environments + repo, err := getCurrentRepository() + + if err != nil { + t.Logf("getCurrentRepository failed (expected in non-git environment): %v", err) + // Don't fail the test - this is expected when not in a git repo + return + } + + if repo == "" { + t.Error("expected non-empty repository name") + } + + // Verify format is owner/repo + if len(repo) < 3 || !strings.Contains(repo, "/") { + t.Errorf("repository name %q doesn't match expected format owner/repo", repo) + } + + t.Logf("Current repository: %s", repo) +} + +func TestCheckRepositoryHasDiscussions(t *testing.T) { + // Test with the current repository (githubnext/gh-aw) + // This test will only pass when GitHub CLI is authenticated + repo := "githubnext/gh-aw" + + hasDiscussions, err := checkRepositoryHasDiscussions(repo) + if err != nil { + t.Logf("checkRepositoryHasDiscussions failed (may be auth issue): %v", err) + // Don't fail - this could be due to auth or network issues + return + } + + t.Logf("Repository %s has discussions enabled: %v", repo, hasDiscussions) +} + +func TestCheckRepositoryHasIssues(t *testing.T) { + // Test with the current repository (githubnext/gh-aw) + // This test will only pass when GitHub CLI is authenticated + repo := "githubnext/gh-aw" + + hasIssues, err := checkRepositoryHasIssues(repo) + if err != nil { + t.Logf("checkRepositoryHasIssues failed (may be auth issue): %v", err) + // Don't fail - this could be due to auth or network issues + return + } + + t.Logf("Repository %s has issues enabled: %v", repo, hasIssues) + + // Issues should definitely be enabled for githubnext/gh-aw + if !hasIssues { + t.Error("Expected githubnext/gh-aw to have issues enabled") + } +} + +func TestCheckRepositoryInvalidFormat(t *testing.T) { + // Test with invalid repository format + _, err := checkRepositoryHasDiscussions("invalid-format") + if err == nil { + t.Error("expected error for invalid repository format") + } + + _, err = checkRepositoryHasIssues("invalid/format/too/many/slashes") + if err != nil { + // This might actually succeed if the API is lenient + t.Logf("Got error for invalid format (expected): %v", err) + } +} diff --git a/pkg/workflow/validation.go b/pkg/workflow/validation.go index a1dc2843b95..d92edcc2706 100644 --- a/pkg/workflow/validation.go +++ b/pkg/workflow/validation.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "github.com/cli/go-gh/v2" "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/logger" "github.com/githubnext/gh-aw/pkg/workflow/pretty" @@ -287,3 +288,152 @@ func validateSecretReferences(secrets []string) error { return nil } + +// validateRepositoryFeatures validates that required repository features are enabled +// when safe-outputs are configured that depend on them (discussions, issues) +func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error { + if workflowData.SafeOutputs == nil { + return nil + } + + validationLog.Print("Validating repository features for safe-outputs") + + // Get the repository from the current git context + // This will work when running in a git repository + repo, err := getCurrentRepository() + if err != nil { + validationLog.Printf("Could not determine repository: %v", err) + // Don't fail if we can't determine the repository (e.g., not in a git repo) + // This allows validation to pass in non-git environments + return nil + } + + validationLog.Printf("Checking repository features for: %s", repo) + + // Check if discussions are enabled when create-discussion is configured + if workflowData.SafeOutputs.CreateDiscussions != nil { + hasDiscussions, err := checkRepositoryHasDiscussions(repo) + if err != nil { + // If we can't check, log but don't fail + // This could happen due to network issues or auth problems + validationLog.Printf("Warning: Could not check if discussions are enabled: %v", err) + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage( + fmt.Sprintf("Could not verify if discussions are enabled: %v", err))) + } + return nil + } + + if !hasDiscussions { + return fmt.Errorf("workflow uses safe-outputs.create-discussion but repository %s does not have discussions enabled. Enable discussions in repository settings or remove create-discussion from safe-outputs", repo) + } + + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage( + fmt.Sprintf("✓ Repository %s has discussions enabled", repo))) + } + } + + // Check if issues are enabled when create-issue is configured + if workflowData.SafeOutputs.CreateIssues != nil { + hasIssues, err := checkRepositoryHasIssues(repo) + if err != nil { + // If we can't check, log but don't fail + validationLog.Printf("Warning: Could not check if issues are enabled: %v", err) + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage( + fmt.Sprintf("Could not verify if issues are enabled: %v", err))) + } + return nil + } + + if !hasIssues { + return fmt.Errorf("workflow uses safe-outputs.create-issue but repository %s does not have issues enabled. Enable issues in repository settings or remove create-issue from safe-outputs", repo) + } + + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage( + fmt.Sprintf("✓ Repository %s has issues enabled", repo))) + } + } + + return nil +} + +// getCurrentRepository gets the current repository from git context +func getCurrentRepository() (string, error) { + // Use gh CLI to get the current repository + // This works when in a git repository with GitHub remote + stdOut, _, err := gh.Exec("repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner") + if err != nil { + return "", fmt.Errorf("failed to get current repository: %w", err) + } + + repo := strings.TrimSpace(stdOut.String()) + if repo == "" { + return "", fmt.Errorf("repository name is empty") + } + + return repo, nil +} + +// checkRepositoryHasDiscussions checks if a repository has discussions enabled +func checkRepositoryHasDiscussions(repo string) (bool, error) { + // Use GitHub GraphQL API to check if discussions are enabled + // The hasDiscussionsEnabled field is the canonical way to check this + query := `query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + hasDiscussionsEnabled + } + }` + + // Split repo into owner and name + parts := strings.SplitN(repo, "/", 2) + if len(parts) != 2 { + return false, fmt.Errorf("invalid repository format: %s (expected owner/repo)", repo) + } + owner, name := parts[0], parts[1] + + // Execute GraphQL query using gh CLI + type GraphQLResponse struct { + Data struct { + Repository struct { + HasDiscussionsEnabled bool `json:"hasDiscussionsEnabled"` + } `json:"repository"` + } `json:"data"` + } + + stdOut, _, err := gh.Exec("api", "graphql", "-f", fmt.Sprintf("query=%s", query), + "-f", fmt.Sprintf("owner=%s", owner), "-f", fmt.Sprintf("name=%s", name)) + if err != nil { + return false, fmt.Errorf("failed to query discussions status: %w", err) + } + + var response GraphQLResponse + if err := json.Unmarshal(stdOut.Bytes(), &response); err != nil { + return false, fmt.Errorf("failed to parse GraphQL response: %w", err) + } + + return response.Data.Repository.HasDiscussionsEnabled, nil +} + +// checkRepositoryHasIssues checks if a repository has issues enabled +func checkRepositoryHasIssues(repo string) (bool, error) { + // Use GitHub REST API to check if issues are enabled + // The has_issues field indicates if issues are enabled + type RepositoryResponse struct { + HasIssues bool `json:"has_issues"` + } + + stdOut, _, err := gh.Exec("api", fmt.Sprintf("repos/%s", repo)) + if err != nil { + return false, fmt.Errorf("failed to query repository: %w", err) + } + + var response RepositoryResponse + if err := json.Unmarshal(stdOut.Bytes(), &response); err != nil { + return false, fmt.Errorf("failed to parse repository response: %w", err) + } + + return response.HasIssues, nil +} From b1b268a620ecd74a6e6ba46189e20f7de85e9920 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:29:53 +0000 Subject: [PATCH 3/6] Add integration tests for repository feature validation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...ry_features_validation_integration_test.go | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 pkg/workflow/repository_features_validation_integration_test.go diff --git a/pkg/workflow/repository_features_validation_integration_test.go b/pkg/workflow/repository_features_validation_integration_test.go new file mode 100644 index 00000000000..1dd1985a39b --- /dev/null +++ b/pkg/workflow/repository_features_validation_integration_test.go @@ -0,0 +1,174 @@ +//go:build integration + +package workflow + +import ( + "os" + "os/exec" + "testing" +) + +// TestRepositoryFeaturesValidationIntegration tests the repository features validation +// with actual GitHub API calls. This test requires: +// 1. Running in a git repository with GitHub remote +// 2. GitHub CLI (gh) authenticated +func TestRepositoryFeaturesValidationIntegration(t *testing.T) { + // Check if gh CLI is available + if _, err := exec.LookPath("gh"); err != nil { + t.Skip("gh CLI not available, skipping integration test") + } + + // Check if gh CLI is authenticated + cmd := exec.Command("gh", "auth", "status") + if err := cmd.Run(); err != nil { + t.Skip("gh CLI not authenticated, skipping integration test") + } + + // Get current repository + repo, err := getCurrentRepository() + if err != nil { + t.Skip("Could not determine current repository, skipping integration test") + } + + t.Logf("Testing with repository: %s", repo) + + // Test checking discussions + t.Run("check_discussions", func(t *testing.T) { + hasDiscussions, err := checkRepositoryHasDiscussions(repo) + if err != nil { + t.Errorf("Failed to check discussions: %v", err) + } + t.Logf("Repository %s has discussions enabled: %v", repo, hasDiscussions) + }) + + // Test checking issues + t.Run("check_issues", func(t *testing.T) { + hasIssues, err := checkRepositoryHasIssues(repo) + if err != nil { + t.Errorf("Failed to check issues: %v", err) + } + t.Logf("Repository %s has issues enabled: %v", repo, hasIssues) + + // Issues should be enabled for githubnext/gh-aw + if repo == "githubnext/gh-aw" && !hasIssues { + t.Error("Expected githubnext/gh-aw to have issues enabled") + } + }) + + // Test full validation with discussions + t.Run("validate_with_discussions", func(t *testing.T) { + workflowData := &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + CreateDiscussions: &CreateDiscussionsConfig{}, + }, + } + + compiler := NewCompiler(true, "", "test") + err := compiler.validateRepositoryFeatures(workflowData) + + hasDiscussions, checkErr := checkRepositoryHasDiscussions(repo) + if checkErr != nil { + t.Logf("Could not verify discussions status: %v", checkErr) + return + } + + if hasDiscussions && err != nil { + t.Errorf("Expected no error when discussions are enabled, got: %v", err) + } else if !hasDiscussions && err == nil { + t.Error("Expected error when discussions are disabled, got none") + } + }) + + // Test full validation with issues + t.Run("validate_with_issues", func(t *testing.T) { + workflowData := &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + CreateIssues: &CreateIssuesConfig{}, + }, + } + + compiler := NewCompiler(true, "", "test") + err := compiler.validateRepositoryFeatures(workflowData) + + hasIssues, checkErr := checkRepositoryHasIssues(repo) + if checkErr != nil { + t.Logf("Could not verify issues status: %v", checkErr) + return + } + + if hasIssues && err != nil { + t.Errorf("Expected no error when issues are enabled, got: %v", err) + } else if !hasIssues && err == nil { + t.Error("Expected error when issues are disabled, got none") + } + }) +} + +// TestCompileWorkflowWithRepositoryFeatureValidation tests compiling a workflow +// that requires repository features +func TestCompileWorkflowWithRepositoryFeatureValidation(t *testing.T) { + // Check if gh CLI is available and authenticated + if _, err := exec.LookPath("gh"); err != nil { + t.Skip("gh CLI not available, skipping integration test") + } + + cmd := exec.Command("gh", "auth", "status") + if err := cmd.Run(); err != nil { + t.Skip("gh CLI not authenticated, skipping integration test") + } + + // Get current repository + repo, err := getCurrentRepository() + if err != nil { + t.Skip("Could not determine current repository, skipping integration test") + } + + // Create a temporary workflow with create-discussion + tempDir := t.TempDir() + workflowPath := tempDir + "/test-discussion.md" + + workflowContent := `--- +on: + workflow_dispatch: +permissions: + contents: read +safe-outputs: + create-discussion: + category: "General" +--- + +# Test Discussion Workflow + +Test workflow for discussions validation. +` + + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write test workflow: %v", err) + } + + // Try to compile the workflow + compiler := NewCompiler(true, "", "test") + compiler.SetNoEmit(true) // Don't write lock file + + err = compiler.CompileFile(workflowPath) + + // Check if discussions are enabled + hasDiscussions, checkErr := checkRepositoryHasDiscussions(repo) + if checkErr != nil { + t.Logf("Could not verify discussions status: %v", checkErr) + t.Logf("Compilation result: %v", err) + return + } + + if hasDiscussions { + if err != nil { + t.Errorf("Expected compilation to succeed when discussions are enabled, got error: %v", err) + } + } else { + if err == nil { + t.Error("Expected compilation to fail when discussions are disabled, but it succeeded") + } else { + t.Logf("Compilation correctly failed: %v", err) + } + } +} From 0aea79ca55937704d9861237afdab5256c785871 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:33:19 +0000 Subject: [PATCH 4/6] Add validation for add-comment with discussion: true Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../repository_features_validation_test.go | 29 +++++++++++++++++++ pkg/workflow/validation.go | 15 ++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/pkg/workflow/repository_features_validation_test.go b/pkg/workflow/repository_features_validation_test.go index 37a209c416f..6ed6261cb17 100644 --- a/pkg/workflow/repository_features_validation_test.go +++ b/pkg/workflow/repository_features_validation_test.go @@ -61,6 +61,30 @@ func TestValidateRepositoryFeatures(t *testing.T) { expectError: false, // Will not error if getCurrentRepository fails or API call fails description: "validation will check both features but won't fail on API errors", }, + { + name: "add-comment with discussion: true", + workflowData: &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + AddComments: &AddCommentsConfig{ + Discussion: boolPtr(true), + }, + }, + }, + expectError: false, // Will not error if getCurrentRepository fails or API call fails + description: "validation will check discussions for add-comment but won't fail on API errors", + }, + { + name: "add-comment with discussion: false", + workflowData: &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + AddComments: &AddCommentsConfig{ + Discussion: boolPtr(false), + }, + }, + }, + expectError: false, + description: "should pass when add-comment targets issues/PRs, not discussions", + }, } for _, tt := range tests { @@ -78,6 +102,11 @@ func TestValidateRepositoryFeatures(t *testing.T) { } } +// boolPtr returns a pointer to a boolean value +func boolPtr(b bool) *bool { + return &b +} + func TestGetCurrentRepository(t *testing.T) { // This test will only pass when running in a git repository with GitHub remote // It's expected to fail in non-git environments diff --git a/pkg/workflow/validation.go b/pkg/workflow/validation.go index d92edcc2706..379b37952e6 100644 --- a/pkg/workflow/validation.go +++ b/pkg/workflow/validation.go @@ -310,8 +310,13 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error validationLog.Printf("Checking repository features for: %s", repo) - // Check if discussions are enabled when create-discussion is configured - if workflowData.SafeOutputs.CreateDiscussions != nil { + // Check if discussions are enabled when create-discussion or add-comment with discussion: true is configured + needsDiscussions := workflowData.SafeOutputs.CreateDiscussions != nil || + (workflowData.SafeOutputs.AddComments != nil && + workflowData.SafeOutputs.AddComments.Discussion != nil && + *workflowData.SafeOutputs.AddComments.Discussion) + + if needsDiscussions { hasDiscussions, err := checkRepositoryHasDiscussions(repo) if err != nil { // If we can't check, log but don't fail @@ -325,7 +330,11 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error } if !hasDiscussions { - return fmt.Errorf("workflow uses safe-outputs.create-discussion but repository %s does not have discussions enabled. Enable discussions in repository settings or remove create-discussion from safe-outputs", repo) + if workflowData.SafeOutputs.CreateDiscussions != nil { + return fmt.Errorf("workflow uses safe-outputs.create-discussion but repository %s does not have discussions enabled. Enable discussions in repository settings or remove create-discussion from safe-outputs", repo) + } + // For add-comment with discussion: true + return fmt.Errorf("workflow uses safe-outputs.add-comment with discussion: true but repository %s does not have discussions enabled. Enable discussions in repository settings or change add-comment configuration", repo) } if c.verbose { From 05d9b5b28da3472d569ae4438835edda4625bd4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:36:37 +0000 Subject: [PATCH 5/6] Fix integration test - use CompileWorkflow instead of CompileFile Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/repository_features_validation_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/repository_features_validation_integration_test.go b/pkg/workflow/repository_features_validation_integration_test.go index 1dd1985a39b..24d169f2405 100644 --- a/pkg/workflow/repository_features_validation_integration_test.go +++ b/pkg/workflow/repository_features_validation_integration_test.go @@ -150,7 +150,7 @@ Test workflow for discussions validation. compiler := NewCompiler(true, "", "test") compiler.SetNoEmit(true) // Don't write lock file - err = compiler.CompileFile(workflowPath) + err = compiler.CompileWorkflow(workflowPath) // Check if discussions are enabled hasDiscussions, checkErr := checkRepositoryHasDiscussions(repo) From dd80facdf353e5021e54ee8aef90fddee42755d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:39:48 +0000 Subject: [PATCH 6/6] Validate repository features (discussions/issues) during workflow compilation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/frontmatter-full.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 63fee1beaa3..baa6431e861 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -683,15 +683,6 @@ env: "example-value" # Feature flags to enable experimental or optional features in the workflow. Each # feature is specified as a key with a boolean value. # (optional) -# -# Available features: -# firewall: Enable AWF (Agent Workflow Firewall) for network egress control -# with domain allowlisting. Currently only supported for the Copilot -# engine. AWF is sourced from https://github.com/githubnext/gh-aw-firewall -# -# Example: -# features: -# firewall: true features: {}