diff --git a/pkg/cli/update_command.go b/pkg/cli/update_command.go index 01543615fe6..76c2b26643e 100644 --- a/pkg/cli/update_command.go +++ b/pkg/cli/update_command.go @@ -288,9 +288,6 @@ func RunUpdateWorkflows(ctx context.Context, opts UpdateWorkflowsOptions) error // This is used after container pin updates to embed digest-pinned image references // in the generated lock files. func recompileAllWorkflows(ctx context.Context, workflowsDir, engineOverride string, verbose bool, approve bool) error { - if workflowsDir == "" { - workflowsDir = getWorkflowsDir() - } return compileWorkflowsForUpdate(ctx, nil, workflowsDir, engineOverride, verbose, approve) } diff --git a/pkg/cli/update_compile_test.go b/pkg/cli/update_compile_test.go index 5a097f93902..db8cb86f48b 100644 --- a/pkg/cli/update_compile_test.go +++ b/pkg/cli/update_compile_test.go @@ -2,8 +2,12 @@ package cli import ( "context" + "os" + "os/exec" + "path/filepath" "testing" + "github.com/github/gh-aw/pkg/testutil" "github.com/stretchr/testify/require" ) @@ -48,3 +52,44 @@ func TestRecompileAllWorkflowsPropagatesCompileErrors(t *testing.T) { require.ErrorIs(t, err, context.Canceled) } + +func TestUpdateCompilation_ReconcilesDefaultActionFailureExpiry(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + workflowsDir := filepath.Join(tempDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0o755)) + t.Chdir(tempDir) + + initCmd := exec.Command("git", "init", "--quiet") + initCmd.Dir = tempDir + require.NoError(t, initCmd.Run()) + + workflowPath := filepath.Join(workflowsDir, "example.md") + workflowContent := `--- +name: Example +on: + workflow_dispatch: +engine: copilot +safe-outputs: + add-comment: +--- + +Say hello. +` + require.NoError(t, os.WriteFile(workflowPath, []byte(workflowContent), 0o644)) + + require.NoError(t, compileWorkflowsForUpdate(context.Background(), nil, "", "", false, false)) + + lockContent, err := os.ReadFile(filepath.Join(workflowsDir, "example.lock.yml")) + require.NoError(t, err) + require.Contains(t, string(lockContent), `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0"`) + require.NotContains(t, string(lockContent), `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168"`) + + _, err = os.Stat(filepath.Join(workflowsDir, "agentics-maintenance.yml")) + require.True(t, os.IsNotExist(err), "implicit expiry alone must not generate maintenance") + + require.NoError(t, recompileAllWorkflows(context.Background(), "", "", false, false)) + lockContent, err = os.ReadFile(filepath.Join(workflowsDir, "example.lock.yml")) + require.NoError(t, err) + require.Contains(t, string(lockContent), `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0"`) + require.NotContains(t, string(lockContent), `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168"`) +} diff --git a/pkg/cli/upgrade_command.go b/pkg/cli/upgrade_command.go index 6fb3a642e01..fa53aaa323f 100644 --- a/pkg/cli/upgrade_command.go +++ b/pkg/cli/upgrade_command.go @@ -33,6 +33,8 @@ type UpgradeConfig struct { } // NewUpgradeCommand creates the upgrade command +// +//nolint:largefunc // Cobra requires the command's complete discoverable flag surface to be registered together. func NewUpgradeCommand(validateEngine func(string) error) *cobra.Command { cmd := &cobra.Command{ Use: "upgrade", @@ -77,6 +79,7 @@ This command always upgrades all Markdown files in .github/workflows.`, ` + string(constants.CLIExtensionPrefix) + ` upgrade --audit --json # Output audit results in JSON format ` + string(constants.CLIExtensionPrefix) + ` upgrade --pre-releases # Include pre-release versions when upgrading the extension (stable releases are the default)`, Args: cobra.NoArgs, + //nolint:largefunc // Interdependent flag validation must run before dispatch so no caller bypasses it. RunE: func(cmd *cobra.Command, args []string) error { verbose, _ := cmd.Flags().GetBool("verbose") dir, _ := cmd.Flags().GetString("dir") @@ -232,6 +235,8 @@ type upgradeOptions struct { } // runUpgradeCommand executes the upgrade process +// +//nolint:largefunc // Upgrade side effects require ordered progress reporting and independent failure handling. func runUpgradeCommand(opts upgradeOptions) error { upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v, noActions=%v, disabledCodemodIDs=%v, skipExtensionUpgrade=%v", opts.verbose, opts.workflowDir, opts.noFix, opts.noCompile, opts.noActions, opts.disabledCodemodIDs, opts.skipExtensionUpgrade) @@ -344,34 +349,12 @@ func runUpgradeCommand(opts upgradeOptions) error { fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Compiling all workflows...")) upgradeLog.Print("Compiling all workflows") - // Create and configure compiler - compiler := createAndConfigureCompiler(CompileConfig{ - Verbose: opts.verbose, - WorkflowDir: opts.workflowDir, - Approve: opts.approve, - EngineOverride: opts.engineOverride, - }) - - // Determine workflow directory - workflowsDir := opts.workflowDir - if workflowsDir == "" { - workflowsDir = constants.GetWorkflowDir() - } - - // Compile all workflow files - stats, compileErr := compileAllWorkflowFiles(opts.ctx, compiler, workflowsDir, opts.verbose) - if compileErr != nil { + // Use the shared directory compile pipeline so post-processing, including + // maintenance workflow generation and implicit expiry reconciliation, runs. + if compileErr := compileWorkflowsForUpdate(opts.ctx, nil, opts.workflowDir, opts.engineOverride, opts.verbose, opts.approve); compileErr != nil { upgradeLog.Printf("Failed to compile workflows: %v", compileErr) // Don't fail the upgrade if compilation fails - this is non-critical fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to compile workflows: %v", compileErr))) - } else if stats != nil { - // Print compilation summary - if opts.verbose { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✓ Compiled %d workflow(s)", stats.Total-stats.Errors))) - } - if stats.Errors > 0 { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: %d workflow(s) failed to compile", stats.Errors))) - } } } else { if opts.noFix { diff --git a/pkg/cli/upgrade_integration_test.go b/pkg/cli/upgrade_integration_test.go index fe6674180a1..72a0e524206 100644 --- a/pkg/cli/upgrade_integration_test.go +++ b/pkg/cli/upgrade_integration_test.go @@ -27,7 +27,9 @@ func TestUpgradeCommand_OnExistingRepository(t *testing.T) { assert.Contains(t, outputStr, "Upgrade complete", "Should report upgrade complete") } -func TestInitAndUpgradeWithEmptyAWDirectory(t *testing.T) { +func TestUpgradeCompilesAndPreservesAWDirectory(t *testing.T) { + const noImplicitExpiry = `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0"` + setup := setupIntegrationTest(t) defer setup.cleanup() @@ -63,13 +65,15 @@ permissions: engine: copilot strict: true timeout-minutes: 5 +safe-outputs: + add-comment: --- Say hello. ` require.NoError(t, os.WriteFile(workflowPath, []byte(workflowContent), 0o644), "should create sample workflow") - upgradeCmd := exec.Command(setup.binaryPath, "upgrade", "--no-fix", "--skip-extension-upgrade") + upgradeCmd := exec.Command(setup.binaryPath, "upgrade", "--approve", "--no-actions", "--skip-extension-upgrade") upgradeCmd.Dir = setup.tempDir upgradeOutput, upgradeErr := upgradeCmd.CombinedOutput() upgradeOutputStr := string(upgradeOutput) @@ -79,4 +83,45 @@ Say hello. require.NoError(t, err, "expected actions-lock.json to be preserved after upgrade") _, err = os.Stat(filepath.Join(awDir, "logs")) require.NoError(t, err, "expected .github/aw/logs to be preserved after upgrade") + + lockContent, err := os.ReadFile(filepath.Join(setup.tempDir, ".github", "workflows", "example.lock.yml")) + require.NoError(t, err, "expected upgrade to compile the workflow") + assert.Contains(t, string(lockContent), noImplicitExpiry) + + _, err = os.Stat(filepath.Join(setup.tempDir, ".github", "workflows", "agentics-maintenance.yml")) + require.True(t, os.IsNotExist(err), "expected maintenance workflow to not be generated for implicit expiry") +} + +func TestUpgradeNoFixSkipsCompilation(t *testing.T) { + setup := setupIntegrationTest(t) + defer setup.cleanup() + + initGit := exec.Command("git", "init", "--quiet") + initGit.Dir = setup.tempDir + require.NoError(t, initGit.Run(), "git init should succeed") + + initCmd := exec.Command(setup.binaryPath, "init") + initCmd.Dir = setup.tempDir + initOutput, initErr := initCmd.CombinedOutput() + require.NoError(t, initErr, "init command should succeed, output: %s", initOutput) + + workflowPath := filepath.Join(setup.tempDir, ".github", "workflows", "example.md") + workflowContent := `--- +name: Example Agentic Workflow +on: + workflow_dispatch: +engine: copilot +--- + +Say hello. +` + require.NoError(t, os.WriteFile(workflowPath, []byte(workflowContent), 0o644)) + + noFixCmd := exec.Command(setup.binaryPath, "upgrade", "--no-fix", "--skip-extension-upgrade") + noFixCmd.Dir = setup.tempDir + noFixOutput, noFixErr := noFixCmd.CombinedOutput() + require.NoError(t, noFixErr, "upgrade --no-fix should succeed, output: %s", noFixOutput) + + _, err := os.Stat(filepath.Join(setup.tempDir, ".github", "workflows", "example.lock.yml")) + require.True(t, os.IsNotExist(err), "--no-fix should skip compilation") }