Skip to content
Merged
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
9 changes: 0 additions & 9 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
{}

Expand Down
15 changes: 15 additions & 0 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
174 changes: 174 additions & 0 deletions pkg/workflow/repository_features_validation_integration_test.go
Original file line number Diff line number Diff line change
@@ -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.CompileWorkflow(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)
}
}
}
180 changes: 180 additions & 0 deletions pkg/workflow/repository_features_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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",
},
{
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 {
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)
}
})
}
}

// 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
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)
}
}
Loading
Loading