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
5 changes: 5 additions & 0 deletions pkg/parser/schemas/included_file_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@
"permissions": {
"description": "GitHub Actions permissions for the workflow (merged with main workflow permissions)",
"oneOf": [
{
"type": "string",
"enum": ["read-all", "write-all", "read", "write"],
"description": "Simple permissions string: 'read-all' (all read permissions), 'write-all' (all write permissions), 'read' or 'write' (basic level)"
},
{
"type": "object",
"description": "Permission scopes and levels",
Expand Down
221 changes: 221 additions & 0 deletions pkg/workflow/permissions_shortcut_included_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package workflow

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

"github.com/githubnext/gh-aw/pkg/testutil"
)

// TestPermissionsShortcutInIncludedFiles tests that permissions shortcuts (read-all, write-all, read, write)
// work correctly in included files, matching the UX of main workflows.
func TestPermissionsShortcutInIncludedFiles(t *testing.T) {
tests := []struct {
name string
includedPermissions string
mainPermissions string
expectCompilationError bool
expectLockFileContains string
}{
{
name: "read-all shortcut in included file",
includedPermissions: "permissions: read-all",
mainPermissions: "permissions: read-all",
expectCompilationError: false,
expectLockFileContains: "permissions: read-all",
},
{
name: "write-all shortcut in included file",
includedPermissions: "permissions: write-all",
mainPermissions: "permissions: write-all",
expectCompilationError: false,
expectLockFileContains: "permissions: write-all",
},
{
name: "read shortcut in included file",
includedPermissions: "permissions: read",
mainPermissions: "permissions: read",
expectCompilationError: false,
expectLockFileContains: "permissions: read",
},
{
name: "write shortcut in included file",
includedPermissions: "permissions: write",
mainPermissions: "permissions: write",
expectCompilationError: false,
expectLockFileContains: "permissions: write",
},
{
name: "object form still works in included file",
includedPermissions: `permissions:
contents: read
issues: write`,
mainPermissions: `permissions:
contents: read
issues: write
pull-requests: read`,
expectCompilationError: false,
expectLockFileContains: "issues: write",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a temporary directory for test files
tempDir := testutil.TempDir(t, "test-*")
sharedDir := filepath.Join(tempDir, ".github", "workflows", "shared")
if err := os.MkdirAll(sharedDir, 0755); err != nil {
t.Fatalf("Failed to create shared directory: %v", err)
}

// Create a shared workflow file with permissions shortcut
sharedWorkflowContent := "---\n" + tt.includedPermissions + "\n---\n\n# Shared workflow\n"
sharedWorkflowPath := filepath.Join(sharedDir, "shared-permissions.md")
if err := os.WriteFile(sharedWorkflowPath, []byte(sharedWorkflowContent), 0644); err != nil {
t.Fatalf("Failed to create shared workflow file: %v", err)
}

// Create main workflow that imports the shared file
mainWorkflowContent := `---
on: issues
engine: copilot
strict: false
` + tt.mainPermissions + `
imports:
- shared/shared-permissions.md
tools:
github:
toolsets: [default]
---

# Main workflow
`
mainWorkflowPath := filepath.Join(tempDir, ".github", "workflows", "test-workflow.md")
if err := os.WriteFile(mainWorkflowPath, []byte(mainWorkflowContent), 0644); err != nil {
t.Fatalf("Failed to create main workflow file: %v", err)
}

// Compile the workflow
compiler := NewCompiler(false, "", "test")
err := compiler.CompileWorkflow(mainWorkflowPath)

if tt.expectCompilationError {
if err == nil {
t.Fatalf("Expected compilation to fail but it succeeded")
}
return
}

if err != nil {
t.Fatalf("Expected compilation to succeed but got error: %v", err)
}

// Read the generated lock file
lockFilePath := filepath.Join(tempDir, ".github", "workflows", "test-workflow.lock.yml")
lockContent, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}

lockStr := string(lockContent)
if !strings.Contains(lockStr, tt.expectLockFileContains) {
t.Errorf("Expected lock file to contain '%s', but it doesn't. Lock file:\n%s", tt.expectLockFileContains, lockStr)
}
})
}
}

// TestPermissionsShortcutMixedUsage tests that shortcuts and object form can be mixed across files
func TestPermissionsShortcutMixedUsage(t *testing.T) {
tests := []struct {
name string
includedPermissions string
mainPermissions string
expectCompilationError bool
expectLockFileContains []string
}{
{
name: "shortcut in included file, object in main",
includedPermissions: "permissions: read-all",
mainPermissions: "permissions:\n contents: read\n issues: read",
expectCompilationError: false,
expectLockFileContains: []string{"contents: read", "issues: read"},
},
{
name: "object in included file, shortcut in main",
includedPermissions: "permissions:\n contents: read",
mainPermissions: "permissions: read-all",
expectCompilationError: false,
expectLockFileContains: []string{"permissions: read-all"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a temporary directory for test files
tempDir := testutil.TempDir(t, "test-*")
sharedDir := filepath.Join(tempDir, ".github", "workflows", "shared")
if err := os.MkdirAll(sharedDir, 0755); err != nil {
t.Fatalf("Failed to create shared directory: %v", err)
}

// Create a shared workflow file with permissions
sharedWorkflowContent := "---\n" + tt.includedPermissions + "\n---\n\n# Shared workflow\n"
sharedWorkflowPath := filepath.Join(sharedDir, "shared-permissions.md")
if err := os.WriteFile(sharedWorkflowPath, []byte(sharedWorkflowContent), 0644); err != nil {
t.Fatalf("Failed to create shared workflow file: %v", err)
}

// Create main workflow
mainWorkflowContent := `---
on: issues
engine: copilot
strict: false
` + tt.mainPermissions + `
imports:
- shared/shared-permissions.md
tools:
github:
toolsets: [default]
---

# Main workflow
`
mainWorkflowPath := filepath.Join(tempDir, ".github", "workflows", "test-workflow.md")
if err := os.WriteFile(mainWorkflowPath, []byte(mainWorkflowContent), 0644); err != nil {
t.Fatalf("Failed to create main workflow file: %v", err)
}

// Compile the workflow
compiler := NewCompiler(false, "", "test")
err := compiler.CompileWorkflow(mainWorkflowPath)

if tt.expectCompilationError {
if err == nil {
t.Fatalf("Expected compilation to fail but it succeeded")
}
return
}

if err != nil {
t.Fatalf("Expected compilation to succeed but got error: %v", err)
}

// Read the generated lock file
lockFilePath := filepath.Join(tempDir, ".github", "workflows", "test-workflow.lock.yml")
lockContent, err := os.ReadFile(lockFilePath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}

lockStr := string(lockContent)
for _, expected := range tt.expectLockFileContains {
if !strings.Contains(lockStr, expected) {
t.Errorf("Expected lock file to contain '%s', but it doesn't", expected)
}
}
})
}
}