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
2 changes: 1 addition & 1 deletion .github/workflows/agentics/shared/job-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ tools:

### Output Report implemented via GitHub Action Job Summary

You will use the Job Summary for GitHub Actions run ${{ github.run_id }} in ${{ github.repository }} to report progess. This means writing to the special file that is stored in the environment variable GITHUB_STEP_SUMMARY. You can write the file using "echo" or the "Write" tool. GITHUB_STEP_SUMMARY is an environment variable set by GitHub Actions which you can use to write the report. You can read this environment variable using the bash command "echo $GITHUB_STEP_SUMMARY".
You will use the Job Summary for GitHub Actions run ${{ github.run_id }} in ${{ github.repository }} to report progess. This means writing to the special file $GITHUB_STEP_SUMMARY. You can write the file using "echo" or the "Write" tool. GITHUB_STEP_SUMMARY is an environment variable set by GitHub Actions which you can use to write the report. You can read this environment variable using the bash command "echo $GITHUB_STEP_SUMMARY".

At the end of the workflow, finalize the job summry with a very, very succinct summary in note form of
- the steps you took
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/issue-triage.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/weekly-research.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

208 changes: 159 additions & 49 deletions pkg/cli/commands.go

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions pkg/cli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestAddWorkflow(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := AddWorkflow(tt.workflow, tt.number, false, "", "", false)
err := AddWorkflowWithTracking(tt.workflow, tt.number, false, "", "", false, nil)

if tt.expectError && err == nil {
t.Errorf("Expected error for test '%s', got nil", tt.name)
Expand All @@ -68,13 +68,13 @@ func TestAddWorkflowForce(t *testing.T) {
// It doesn't test the actual file system operations

// Test that force=false fails when a file "exists" (simulated by empty workflow name which triggers help)
err := AddWorkflow("", 1, false, "", "", false)
err := AddWorkflowWithTracking("", 1, false, "", "", false, nil)
if err != nil {
t.Errorf("Expected no error for empty workflow (shows help), got: %v", err)
}

// Test that force=true works with same parameters
err = AddWorkflow("", 1, false, "", "", true)
err = AddWorkflowWithTracking("", 1, false, "", "", true, nil)
if err != nil {
t.Errorf("Expected no error for empty workflow with force=true, got: %v", err)
}
Expand Down Expand Up @@ -180,13 +180,13 @@ func TestAllCommandsExist(t *testing.T) {
name string
}{
{func() error { return ListWorkflows(false) }, false, "ListWorkflows"},
{func() error { return AddWorkflow("", 1, false, "", "", false) }, false, "AddWorkflow (empty name)"}, // Shows help when empty, doesn't error
{func() error { return CompileWorkflows("", false, "", false, false, false, false) }, false, "CompileWorkflows"}, // Should compile existing markdown files successfully
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows("test", false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return EnableWorkflows("test") }, false, "EnableWorkflows"}, // Should handle missing directory gracefully
{func() error { return DisableWorkflows("test") }, false, "DisableWorkflows"}, // Should handle missing directory gracefully
{func() error { return RunWorkflowOnGitHub("", false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
{func() error { return AddWorkflowWithTracking("", 1, false, "", "", false, nil) }, false, "AddWorkflowWithTracking (empty name)"}, // Shows help when empty, doesn't error
{func() error { return CompileWorkflows("", false, "", false, false, false, false) }, false, "CompileWorkflows"}, // Should compile existing markdown files successfully
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows("test", false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return EnableWorkflows("test") }, false, "EnableWorkflows"}, // Should handle missing directory gracefully
{func() error { return DisableWorkflows("test") }, false, "DisableWorkflows"}, // Should handle missing directory gracefully
{func() error { return RunWorkflowOnGitHub("", false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
}

for _, test := range tests {
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/compile_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func setupIntegrationTest(t *testing.T) *integrationTestSetup {
if err := copyFile(srcBinary, binaryPath); err != nil {
t.Fatalf("Failed to copy gh-aw binary to temp directory: %v", err)
}

// Make the binary executable
if err := os.Chmod(binaryPath, 0755); err != nil {
t.Fatalf("Failed to make binary executable: %v", err)
Expand Down
175 changes: 175 additions & 0 deletions pkg/cli/file_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cli

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)

// FileTracker keeps track of files created or modified during workflow operations
// to enable proper staging and rollback functionality
type FileTracker struct {
CreatedFiles []string
ModifiedFiles []string
OriginalContent map[string][]byte // Store original content for rollback
gitRoot string
}

// NewFileTracker creates a new file tracker
func NewFileTracker() (*FileTracker, error) {
gitRoot, err := findGitRoot()
if err != nil {
return nil, fmt.Errorf("file tracker requires being in a git repository: %w", err)
}
return &FileTracker{
CreatedFiles: make([]string, 0),
ModifiedFiles: make([]string, 0),
OriginalContent: make(map[string][]byte),
gitRoot: gitRoot,
}, nil
}

// TrackCreated adds a file to the created files list
func (ft *FileTracker) TrackCreated(filePath string) {
absPath, err := filepath.Abs(filePath)
if err != nil {
absPath = filePath
}
ft.CreatedFiles = append(ft.CreatedFiles, absPath)
}

// TrackModified adds a file to the modified files list and stores its original content
func (ft *FileTracker) TrackModified(filePath string) {
absPath, err := filepath.Abs(filePath)
if err != nil {
absPath = filePath
}

// Store original content if not already stored
if _, exists := ft.OriginalContent[absPath]; !exists {
if content, err := os.ReadFile(absPath); err == nil {
ft.OriginalContent[absPath] = content
}
}

ft.ModifiedFiles = append(ft.ModifiedFiles, absPath)
}

// GetAllFiles returns all tracked files (created and modified)
func (ft *FileTracker) GetAllFiles() []string {
all := make([]string, 0, len(ft.CreatedFiles)+len(ft.ModifiedFiles))
all = append(all, ft.CreatedFiles...)
all = append(all, ft.ModifiedFiles...)
return all
}

// StageAllFiles stages all tracked files using git add
func (ft *FileTracker) StageAllFiles(verbose bool) error {
allFiles := ft.GetAllFiles()
if len(allFiles) == 0 {
if verbose {
fmt.Println("No files to stage")
}
return nil
}

if verbose {
fmt.Printf("Staging %d files...\n", len(allFiles))
for _, file := range allFiles {
fmt.Printf(" - %s\n", file)
}
}

// Stage all files in a single git add command
args := append([]string{"add"}, allFiles...)
cmd := exec.Command("git", args...)
cmd.Dir = ft.gitRoot
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to stage files: %w", err)
}

return nil
}

// RollbackCreatedFiles deletes all files that were created during the operation
func (ft *FileTracker) RollbackCreatedFiles(verbose bool) error {
if len(ft.CreatedFiles) == 0 {
return nil
}

if verbose {
fmt.Printf("Rolling back %d created files...\n", len(ft.CreatedFiles))
}

var errors []string
for _, file := range ft.CreatedFiles {
if verbose {
fmt.Printf(" - Deleting %s\n", file)
}
if err := os.Remove(file); err != nil && !os.IsNotExist(err) {
errors = append(errors, fmt.Sprintf("failed to delete %s: %v", file, err))
}
}

if len(errors) > 0 {
return fmt.Errorf("rollback errors: %s", strings.Join(errors, "; "))
}

return nil
}

// RollbackModifiedFiles restores all modified files to their original state
func (ft *FileTracker) RollbackModifiedFiles(verbose bool) error {
if len(ft.ModifiedFiles) == 0 {
return nil
}

if verbose {
fmt.Printf("Rolling back %d modified files...\n", len(ft.ModifiedFiles))
}

var errors []string
for _, file := range ft.ModifiedFiles {
if verbose {
fmt.Printf(" - Restoring %s\n", file)
}

// Restore original content if we have it
if originalContent, exists := ft.OriginalContent[file]; exists {
if err := os.WriteFile(file, originalContent, 0644); err != nil {
errors = append(errors, fmt.Sprintf("failed to restore %s: %v", file, err))
}
} else {
if verbose {
fmt.Printf(" Warning: No original content stored for %s\n", file)
}
}
}

if len(errors) > 0 {
return fmt.Errorf("rollback errors: %s", strings.Join(errors, "; "))
}

return nil
}

// RollbackAllFiles rolls back both created and modified files
func (ft *FileTracker) RollbackAllFiles(verbose bool) error {
var errors []string

if err := ft.RollbackCreatedFiles(verbose); err != nil {
errors = append(errors, fmt.Sprintf("created files rollback: %v", err))
}

if err := ft.RollbackModifiedFiles(verbose); err != nil {
errors = append(errors, fmt.Sprintf("modified files rollback: %v", err))
}

if len(errors) > 0 {
return fmt.Errorf("rollback errors: %s", strings.Join(errors, "; "))
}

return nil
}
Loading