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
1 change: 0 additions & 1 deletion .github/workflows/repository-quality-improver.lock.yml

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

22 changes: 17 additions & 5 deletions pkg/cli/update_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ func extractBaseRepo(actionPath string) string {

// UpdateActions updates GitHub Actions versions in .github/aw/actions-lock.json
// It checks each action for newer releases and updates the SHA if a newer version is found
func UpdateActions(allowMajor, verbose bool) error {
updateLog.Print("Starting action updates")
// If dryRun is true, it shows what would be updated without modifying files
func UpdateActions(allowMajor, verbose bool, dryRun bool) error {
updateLog.Printf("Starting action updates (dryRun=%v)", dryRun)

if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking for GitHub Actions updates..."))
Expand Down Expand Up @@ -90,7 +91,12 @@ func UpdateActions(allowMajor, verbose bool) error {

// Update the entry
updateLog.Printf("Updating %s from %s (%s) to %s (%s)", entry.Repo, entry.Version, entry.SHA[:7], latestVersion, latestSHA[:7])
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Updated %s from %s to %s", entry.Repo, entry.Version, latestVersion)))

if dryRun {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Would update %s from %s to %s", entry.Repo, entry.Version, latestVersion)))
} else {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Updated %s from %s to %s", entry.Repo, entry.Version, latestVersion)))
}

// Delete the old key (which has the old version)
delete(actionsLock.Entries, key)
Expand All @@ -110,7 +116,11 @@ func UpdateActions(allowMajor, verbose bool) error {
fmt.Fprintln(os.Stderr, "")

if len(updatedActions) > 0 {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Updated %d action(s):", len(updatedActions))))
if dryRun {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Would update %d action(s):", len(updatedActions))))
} else {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Updated %d action(s):", len(updatedActions))))
}
for _, action := range updatedActions {
fmt.Fprintln(os.Stderr, console.FormatListItem(action))
}
Expand All @@ -131,7 +141,7 @@ func UpdateActions(allowMajor, verbose bool) error {
}

// Save the updated actions lock file if there were any updates
if len(updatedActions) > 0 {
if len(updatedActions) > 0 && !dryRun {
// Marshal with sorted keys and pretty printing
updatedData, err := marshalActionsLockSorted(&actionsLock)
if err != nil {
Expand All @@ -147,6 +157,8 @@ func UpdateActions(allowMajor, verbose bool) error {

updateLog.Printf("Successfully wrote updated actions-lock.json with %d updates", len(updatedActions))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Updated actions-lock.json file"))
} else if len(updatedActions) > 0 && dryRun {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Would update actions-lock.json file"))
}

return nil
Expand Down
52 changes: 28 additions & 24 deletions pkg/cli/update_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,7 @@ Examples:
return runDependencyAudit(verbose, jsonOutput)
}

// Handle dry-run mode
if dryRunFlag {
// TODO: Implement dry-run mode for workflow updates
return fmt.Errorf("--dry-run mode not yet implemented for workflow updates")
}

return UpdateWorkflowsWithExtensionCheck(args, majorFlag, forceFlag, verbose, engineOverride, prFlag, workflowDir, noStopAfter, stopAfter, mergeFlag, noActions)
return UpdateWorkflowsWithExtensionCheck(args, majorFlag, forceFlag, verbose, engineOverride, prFlag, workflowDir, noStopAfter, stopAfter, mergeFlag, noActions, dryRunFlag)
},
}

Expand Down Expand Up @@ -141,8 +135,14 @@ func runDependencyAudit(verbose bool, jsonOutput bool) error {
// 3. Update workflows from source repositories (compiles each workflow after update)
// 4. Apply automatic fixes to updated workflows
// 5. Optionally create a PR
func UpdateWorkflowsWithExtensionCheck(workflowNames []string, allowMajor, force, verbose bool, engineOverride string, createPR bool, workflowsDir string, noStopAfter bool, stopAfter string, merge bool, noActions bool) error {
updateLog.Printf("Starting update process: workflows=%v, allowMajor=%v, force=%v, createPR=%v, merge=%v, noActions=%v", workflowNames, allowMajor, force, createPR, merge, noActions)
func UpdateWorkflowsWithExtensionCheck(workflowNames []string, allowMajor, force, verbose bool, engineOverride string, createPR bool, workflowsDir string, noStopAfter bool, stopAfter string, merge bool, noActions bool, dryRun bool) error {
updateLog.Printf("Starting update process: workflows=%v, allowMajor=%v, force=%v, createPR=%v, merge=%v, noActions=%v, dryRun=%v", workflowNames, allowMajor, force, createPR, merge, noActions, dryRun)

// Show dry-run mode indicator
if dryRun {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Running in dry-run mode - no files will be modified"))
fmt.Fprintln(os.Stderr, "")
}

// Step 1: Check for gh-aw extension updates
if err := checkExtensionUpdate(verbose); err != nil {
Expand All @@ -151,36 +151,40 @@ func UpdateWorkflowsWithExtensionCheck(workflowNames []string, allowMajor, force

// Step 2: Update GitHub Actions versions (unless disabled)
if !noActions {
if err := UpdateActions(allowMajor, verbose); err != nil {
if err := UpdateActions(allowMajor, verbose, dryRun); err != nil {
return fmt.Errorf("action update failed: %w", err)
}
}

// Step 3: Update workflows from source repositories
// Note: Each workflow is compiled immediately after update
if err := UpdateWorkflows(workflowNames, allowMajor, force, verbose, engineOverride, workflowsDir, noStopAfter, stopAfter, merge); err != nil {
if err := UpdateWorkflows(workflowNames, allowMajor, force, verbose, engineOverride, workflowsDir, noStopAfter, stopAfter, merge, dryRun); err != nil {
return fmt.Errorf("workflow update failed: %w", err)
}

// Step 4: Apply automatic fixes to updated workflows
fixConfig := FixConfig{
WorkflowIDs: workflowNames,
Write: true,
Verbose: verbose,
}
if err := RunFix(fixConfig); err != nil {
updateLog.Printf("Fix command failed (non-fatal): %v", err)
// Don't fail the update if fix fails - this is non-critical
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: automatic fixes failed: %v", err)))
// Step 4: Apply automatic fixes to updated workflows (skip in dry-run mode)
if !dryRun {
fixConfig := FixConfig{
WorkflowIDs: workflowNames,
Write: true,
Verbose: verbose,
}
if err := RunFix(fixConfig); err != nil {
updateLog.Printf("Fix command failed (non-fatal): %v", err)
// Don't fail the update if fix fails - this is non-critical
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: automatic fixes failed: %v", err)))
}
}
}

// Step 5: Optionally create PR if flag is set
if createPR {
// Step 5: Optionally create PR if flag is set (skip in dry-run mode)
if createPR && !dryRun {
if err := createUpdatePR(verbose); err != nil {
return fmt.Errorf("failed to create PR: %w", err)
}
} else if createPR && dryRun {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Would create PR with changes (skipped in dry-run mode)"))
}

return nil
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/update_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ func TestShowUpdateSummary(t *testing.T) {
// This test just verifies the function doesn't panic and can be called
// We don't check the exact output format since it uses console helpers
// and the exact formatting may change
showUpdateSummary(tt.successfulUpdates, tt.failedUpdates)
showUpdateSummary(tt.successfulUpdates, tt.failedUpdates, false)
})
}
}
Expand Down Expand Up @@ -829,7 +829,7 @@ func TestUpdateActions_NoFile(t *testing.T) {
os.Chdir(tmpDir)

// Should not error when file doesn't exist
err := UpdateActions(false, false)
err := UpdateActions(false, false, false)
if err != nil {
t.Errorf("Expected no error when actions-lock.json doesn't exist, got: %v", err)
}
Expand Down Expand Up @@ -860,7 +860,7 @@ func TestUpdateActions_EmptyFile(t *testing.T) {
os.Chdir(tmpDir)

// Should not error with empty file
err := UpdateActions(false, false)
err := UpdateActions(false, false, false)
if err != nil {
t.Errorf("Expected no error with empty actions-lock.json, got: %v", err)
}
Expand Down Expand Up @@ -889,7 +889,7 @@ func TestUpdateActions_InvalidJSON(t *testing.T) {
os.Chdir(tmpDir)

// Should error with invalid JSON
err := UpdateActions(false, false)
err := UpdateActions(false, false, false)
if err == nil {
t.Error("Expected error with invalid JSON, got nil")
}
Expand Down
9 changes: 7 additions & 2 deletions pkg/cli/update_display.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import (
)

// showUpdateSummary displays a summary of workflow updates using console helpers
func showUpdateSummary(successfulUpdates []string, failedUpdates []updateFailure) {
// If dryRun is true, it shows what would be updated
func showUpdateSummary(successfulUpdates []string, failedUpdates []updateFailure, dryRun bool) {
fmt.Fprintln(os.Stderr, "")

// Show successful updates
if len(successfulUpdates) > 0 {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully updated and compiled %d workflow(s):", len(successfulUpdates))))
if dryRun {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Would update and compile %d workflow(s):", len(successfulUpdates))))
} else {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully updated and compiled %d workflow(s):", len(successfulUpdates))))
}
for _, name := range successfulUpdates {
fmt.Fprintln(os.Stderr, console.FormatListItem(name))
}
Expand Down
147 changes: 147 additions & 0 deletions pkg/cli/update_dry_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package cli

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

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

// TestShowUpdateSummary_DryRun tests the update summary display in dry-run mode
func TestShowUpdateSummary_DryRun(t *testing.T) {
tests := []struct {
name string
successfulUpdates []string
failedUpdates []updateFailure
dryRun bool
}{
{
name: "dry-run with successful updates",
successfulUpdates: []string{"workflow1", "workflow2"},
failedUpdates: []updateFailure{},
dryRun: true,
},
{
name: "normal mode with successful updates",
successfulUpdates: []string{"workflow1", "workflow2"},
failedUpdates: []updateFailure{},
dryRun: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// This test just verifies the function doesn't panic and can be called with dry-run flag
showUpdateSummary(tt.successfulUpdates, tt.failedUpdates, tt.dryRun)
})
}
}

// TestUpdateActions_DryRun tests that UpdateActions respects dry-run mode
func TestUpdateActions_DryRun(t *testing.T) {
// Create a temporary directory with an actions-lock.json
tmpDir := testutil.TempDir(t, "test-*")
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)

// Create .github/aw directory
awDir := filepath.Join(tmpDir, ".github", "aw")
if err := os.MkdirAll(awDir, 0755); err != nil {
t.Fatalf("Failed to create .github/aw directory: %v", err)
}

// Create an actions-lock.json
actionsLockPath := filepath.Join(awDir, "actions-lock.json")
actionsLock := `{
"entries": {
"actions/checkout@v4": {
"repo": "actions/checkout",
"version": "v4",
"sha": "b4ffde65f46336ab88eb53be808477a3936bae11"
}
}
}`
if err := os.WriteFile(actionsLockPath, []byte(actionsLock), 0644); err != nil {
t.Fatalf("Failed to write actions-lock.json: %v", err)
}

os.Chdir(tmpDir)

// Read the original content
originalContent, err := os.ReadFile(actionsLockPath)
if err != nil {
t.Fatalf("Failed to read original actions-lock.json: %v", err)
}

// Run UpdateActions in dry-run mode
err = UpdateActions(false, false, true)
if err != nil {
t.Logf("UpdateActions returned error (may be expected in test environment): %v", err)
}

// Verify the file was not modified
afterContent, err := os.ReadFile(actionsLockPath)
if err != nil {
t.Fatalf("Failed to read actions-lock.json after dry-run: %v", err)
}

if string(originalContent) != string(afterContent) {
t.Error("Expected file to remain unchanged in dry-run mode")
}
}

// TestUpdateWorkflows_DryRun tests that UpdateWorkflows respects dry-run mode
func TestUpdateWorkflows_DryRun(t *testing.T) {
// Create a temporary directory structure
tmpDir := testutil.TempDir(t, "test-*")
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)

customWorkflowDir := filepath.Join(tmpDir, "workflows")
if err := os.MkdirAll(customWorkflowDir, 0755); err != nil {
t.Fatalf("Failed to create workflow directory: %v", err)
}

// Create a workflow file with source field
workflowContent := `---
on: push
engine: claude
source: test/repo/workflow.md@v1.0.0
---

# Test Workflow

Test content.`

workflowPath := filepath.Join(customWorkflowDir, "test-workflow.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

os.Chdir(tmpDir)

// Read the original content
originalContent, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("Failed to read original workflow: %v", err)
}

// Run UpdateWorkflows in dry-run mode
// This will fail because the source repository doesn't exist, but that's ok for testing
// We're just verifying that the file is not modified
err = UpdateWorkflows([]string{"test-workflow"}, false, false, false, "", customWorkflowDir, false, "", false, true)
if err != nil {
t.Logf("UpdateWorkflows returned error (expected in test environment): %v", err)
}

// Verify the file was not modified
afterContent, err := os.ReadFile(workflowPath)
if err != nil {
t.Fatalf("Failed to read workflow after dry-run: %v", err)
}

if string(originalContent) != string(afterContent) {
t.Error("Expected workflow file to remain unchanged in dry-run mode")
}
}
Loading