Skip to content
Closed
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
46 changes: 0 additions & 46 deletions pkg/cli/actions_build_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,52 +147,6 @@ func getActionDirectories(actionsDir string) ([]string, error) {
return dirs, nil
}

// validateActionYml validates that an action.yml file exists and contains required fields.
//
// This validation function is co-located with the actions build command because:
// - It's specific to GitHub Actions custom action structure
// - It's only called during the actions build process
// - It validates action metadata before bundling JavaScript
//
// The function validates:
// - action.yml file exists in the action directory
// - Required fields are present (name, description, runs)
// - Basic action metadata structure is valid
//
// This follows the principle that domain-specific validation belongs in domain files.
func validateActionYml(actionPath string) error {
ymlPath := filepath.Join(actionPath, "action.yml")

if _, err := os.Stat(ymlPath); os.IsNotExist(err) {
return fmt.Errorf("action.yml not found")
}

content, err := os.ReadFile(ymlPath)
if err != nil {
return fmt.Errorf("failed to read action.yml: %w", err)
}

contentStr := string(content)

// Check required fields
requiredFields := []string{"name:", "description:", "runs:"}
for _, field := range requiredFields {
if !strings.Contains(contentStr, field) {
return fmt.Errorf("missing required field '%s'", strings.TrimSuffix(field, ":"))
}
}

// Check that it's either a node20 or composite action
isNode20 := strings.Contains(contentStr, "using: 'node20'") || strings.Contains(contentStr, "using: \"node20\"")
isComposite := strings.Contains(contentStr, "using: 'composite'") || strings.Contains(contentStr, "using: \"composite\"")

if !isNode20 && !isComposite {
return fmt.Errorf("action must use either 'node20' or 'composite' runtime")
}

return nil
}

// buildAction builds a single action by bundling its dependencies
func buildAction(actionsDir, actionName string) error {
actionsBuildLog.Printf("Building action: %s", actionName)
Expand Down
36 changes: 36 additions & 0 deletions pkg/cli/compile_config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cli

import (
"fmt"
"path/filepath"

"github.com/githubnext/gh-aw/pkg/logger"
"github.com/githubnext/gh-aw/pkg/stringutil"
)
Expand Down Expand Up @@ -108,3 +111,36 @@ func sanitizeValidationResults(results []ValidationResult) []ValidationResult {

return sanitized
}

// validateCompileConfig validates the configuration flags before compilation
// This is extracted for faster testing without full compilation
func validateCompileConfig(config CompileConfig) error {
compileConfigLog.Printf("Validating compile config: files=%d, dependabot=%v, purge=%v, workflowDir=%s", len(config.MarkdownFiles), config.Dependabot, config.Purge, config.WorkflowDir)

// Validate dependabot flag usage
if config.Dependabot {
if len(config.MarkdownFiles) > 0 {
compileConfigLog.Print("Config validation failed: dependabot flag with specific files")
return fmt.Errorf("--dependabot flag cannot be used with specific workflow files")
}
if config.WorkflowDir != "" && config.WorkflowDir != ".github/workflows" {
compileConfigLog.Printf("Config validation failed: dependabot with custom dir: %s", config.WorkflowDir)
return fmt.Errorf("--dependabot flag cannot be used with custom --dir")
}
}

// Validate purge flag usage
if config.Purge && len(config.MarkdownFiles) > 0 {
compileConfigLog.Print("Config validation failed: purge flag with specific files")
return fmt.Errorf("--purge flag can only be used when compiling all markdown files (no specific files specified)")
}

// Validate workflow directory path
if config.WorkflowDir != "" && filepath.IsAbs(config.WorkflowDir) {
compileConfigLog.Printf("Config validation failed: absolute path in workflowDir: %s", config.WorkflowDir)
return fmt.Errorf("--dir must be a relative path, got: %s", config.WorkflowDir)
}

compileConfigLog.Print("Config validation successful")
return nil
}
88 changes: 88 additions & 0 deletions pkg/cli/compile_post_processing.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ package cli
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/logger"
Expand Down Expand Up @@ -145,6 +147,92 @@ func updateGitAttributes(successCount int, actionCache *workflow.ActionCache, ve
return nil
}

// ensureGitAttributes ensures that .gitattributes contains the entry to mark .lock.yml files as generated
func ensureGitAttributes() error {
compilePostProcessingLog.Print("Ensuring .gitattributes is updated")
gitRoot, err := findGitRoot()
if err != nil {
return err // Not in a git repository, skip
}

gitAttributesPath := filepath.Join(gitRoot, ".gitattributes")
lockYmlEntry := ".github/workflows/*.lock.yml linguist-generated=true merge=ours"
requiredEntries := []string{lockYmlEntry}

// Read existing .gitattributes file if it exists
var lines []string
if content, err := os.ReadFile(gitAttributesPath); err == nil {
lines = strings.Split(string(content), "\n")
compilePostProcessingLog.Printf("Read existing .gitattributes with %d lines", len(lines))
} else {
compilePostProcessingLog.Print("No existing .gitattributes file found")
}

modified := false
for _, required := range requiredEntries {
found := false
for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
if trimmedLine == required {
found = true
break
}
// Check for old format entries that need updating
if strings.HasPrefix(trimmedLine, ".github/workflows/*.lock.yml") && required == lockYmlEntry {
compilePostProcessingLog.Print("Updating old .gitattributes entry format")
lines[i] = lockYmlEntry
found = true
modified = true
break
}
}

if !found {
compilePostProcessingLog.Printf("Adding new .gitattributes entry: %s", required)
if len(lines) > 0 && lines[len(lines)-1] != "" {
lines = append(lines, "")
}
lines = append(lines, required)
modified = true
}
}

// Remove old campaign.g.md entries if they exist (they're now in .gitignore)
for i := len(lines) - 1; i >= 0; i-- {
trimmedLine := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmedLine, ".github/workflows/*.campaign.g.md") {
compilePostProcessingLog.Print("Removing obsolete .campaign.g.md .gitattributes entry")
lines = append(lines[:i], lines[i+1:]...)
modified = true
}
}

if !modified {
compilePostProcessingLog.Print(".gitattributes already contains required entries")
return nil
}

// Write back to file with owner-only read/write permissions (0600) for security best practices
content := strings.Join(lines, "\n")
if err := os.WriteFile(gitAttributesPath, []byte(content), 0600); err != nil {
compilePostProcessingLog.Printf("Failed to write .gitattributes: %v", err)
return fmt.Errorf("failed to write .gitattributes: %w", err)
}

compilePostProcessingLog.Print("Successfully updated .gitattributes")
return nil
}

// stageGitAttributesIfChanged stages .gitattributes if it was modified
func stageGitAttributesIfChanged() error {
gitRoot, err := findGitRoot()
if err != nil {
return err
}
gitAttributesPath := filepath.Join(gitRoot, ".gitattributes")
return exec.Command("git", "-C", gitRoot, "add", gitAttributesPath).Run()
}

// saveActionCache saves the action cache after all compilations
func saveActionCache(actionCache *workflow.ActionCache, verbose bool) error {
if actionCache == nil {
Expand Down
33 changes: 0 additions & 33 deletions pkg/cli/compile_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,36 +187,3 @@ func CompileWorkflowDataWithValidation(compiler *workflow.Compiler, workflowData

return nil
}

// validateCompileConfig validates the configuration flags before compilation
// This is extracted for faster testing without full compilation
func validateCompileConfig(config CompileConfig) error {
compileValidationLog.Printf("Validating compile config: files=%d, dependabot=%v, purge=%v, workflowDir=%s", len(config.MarkdownFiles), config.Dependabot, config.Purge, config.WorkflowDir)

// Validate dependabot flag usage
if config.Dependabot {
if len(config.MarkdownFiles) > 0 {
compileValidationLog.Print("Config validation failed: dependabot flag with specific files")
return fmt.Errorf("--dependabot flag cannot be used with specific workflow files")
}
if config.WorkflowDir != "" && config.WorkflowDir != ".github/workflows" {
compileValidationLog.Printf("Config validation failed: dependabot with custom dir: %s", config.WorkflowDir)
return fmt.Errorf("--dependabot flag cannot be used with custom --dir")
}
}

// Validate purge flag usage
if config.Purge && len(config.MarkdownFiles) > 0 {
compileValidationLog.Print("Config validation failed: purge flag with specific files")
return fmt.Errorf("--purge flag can only be used when compiling all markdown files (no specific files specified)")
}

// Validate workflow directory path
if config.WorkflowDir != "" && filepath.IsAbs(config.WorkflowDir) {
compileValidationLog.Printf("Config validation failed: absolute path in workflowDir: %s", config.WorkflowDir)
return fmt.Errorf("--dir must be a relative path, got: %s", config.WorkflowDir)
}

compileValidationLog.Print("Config validation successful")
return nil
}
131 changes: 0 additions & 131 deletions pkg/cli/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"path/filepath"
"strings"

"github.com/charmbracelet/huh"
"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/logger"
)
Expand Down Expand Up @@ -153,92 +152,6 @@ func stageWorkflowChanges() {
}
}

// ensureGitAttributes ensures that .gitattributes contains the entry to mark .lock.yml files as generated
func ensureGitAttributes() error {
gitLog.Print("Ensuring .gitattributes is updated")
gitRoot, err := findGitRoot()
if err != nil {
return err // Not in a git repository, skip
}

gitAttributesPath := filepath.Join(gitRoot, ".gitattributes")
lockYmlEntry := ".github/workflows/*.lock.yml linguist-generated=true merge=ours"
requiredEntries := []string{lockYmlEntry}

// Read existing .gitattributes file if it exists
var lines []string
if content, err := os.ReadFile(gitAttributesPath); err == nil {
lines = strings.Split(string(content), "\n")
gitLog.Printf("Read existing .gitattributes with %d lines", len(lines))
} else {
gitLog.Print("No existing .gitattributes file found")
}

modified := false
for _, required := range requiredEntries {
found := false
for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
if trimmedLine == required {
found = true
break
}
// Check for old format entries that need updating
if strings.HasPrefix(trimmedLine, ".github/workflows/*.lock.yml") && required == lockYmlEntry {
gitLog.Print("Updating old .gitattributes entry format")
lines[i] = lockYmlEntry
found = true
modified = true
break
}
}

if !found {
gitLog.Printf("Adding new .gitattributes entry: %s", required)
if len(lines) > 0 && lines[len(lines)-1] != "" {
lines = append(lines, "")
}
lines = append(lines, required)
modified = true
}
}

// Remove old campaign.g.md entries if they exist (they're now in .gitignore)
for i := len(lines) - 1; i >= 0; i-- {
trimmedLine := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmedLine, ".github/workflows/*.campaign.g.md") {
gitLog.Print("Removing obsolete .campaign.g.md .gitattributes entry")
lines = append(lines[:i], lines[i+1:]...)
modified = true
}
}

if !modified {
gitLog.Print(".gitattributes already contains required entries")
return nil
}

// Write back to file with owner-only read/write permissions (0600) for security best practices
content := strings.Join(lines, "\n")
if err := os.WriteFile(gitAttributesPath, []byte(content), 0600); err != nil {
gitLog.Printf("Failed to write .gitattributes: %v", err)
return fmt.Errorf("failed to write .gitattributes: %w", err)
}

gitLog.Print("Successfully updated .gitattributes")
return nil
}

// stageGitAttributesIfChanged stages .gitattributes if it was modified
func stageGitAttributesIfChanged() error {
gitRoot, err := findGitRoot()
if err != nil {
return err
}
gitAttributesPath := filepath.Join(gitRoot, ".gitattributes")
return exec.Command("git", "-C", gitRoot, "add", gitAttributesPath).Run()
}

// ensureLogsGitignore ensures that .github/aw/logs/.gitignore exists to ignore log files
func ensureLogsGitignore() error {
gitLog.Print("Ensuring .github/aw/logs/.gitignore exists")
Expand Down Expand Up @@ -710,47 +623,3 @@ func checkOnDefaultBranch(verbose bool) error {
return nil
}

// confirmPushOperation prompts the user to confirm push operation (skips in CI)
func confirmPushOperation(verbose bool) error {
gitLog.Print("Checking if user confirmation is needed for push operation")

// Skip confirmation in CI environments
if IsRunningInCI() {
gitLog.Print("Running in CI, skipping user confirmation")
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Running in CI - skipping confirmation prompt"))
}
return nil
}

// Prompt user for confirmation
gitLog.Print("Prompting user for push confirmation")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("This will commit and push changes to the remote repository."))

var confirmed bool
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Do you want to proceed with commit and push?").
Description("This will stage all changes, commit them, and push to the remote repository").
Value(&confirmed),
),
).WithAccessible(console.IsAccessibleMode())

if err := form.Run(); err != nil {
gitLog.Printf("Confirmation prompt failed: %v", err)
return fmt.Errorf("confirmation prompt failed: %w", err)
}

if !confirmed {
gitLog.Print("User declined push operation")
return fmt.Errorf("push operation cancelled by user")
}

gitLog.Print("User confirmed push operation")
if verbose {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Push operation confirmed"))
}
return nil
}
Loading
Loading