diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml
index 294506cfce3..2b8f9094a22 100644
--- a/.github/workflows/cli-version-checker.lock.yml
+++ b/.github/workflows/cli-version-checker.lock.yml
@@ -3013,19 +3013,26 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
+ function generatePatchPreview(patchContent) {
+ if (!patchContent || !patchContent.trim()) {
+ return "";
+ }
+ const lines = patchContent.split("\n");
+ const maxLines = 500;
+ const maxChars = 2000;
+ let preview = lines.length <= maxLines ? patchContent : lines.slice(0, maxLines).join("\n");
+ const lineTruncated = lines.length > maxLines;
+ const charTruncated = preview.length > maxChars;
+ if (charTruncated) {
+ preview = preview.slice(0, maxChars);
+ }
+ const truncated = lineTruncated || charTruncated;
+ const summary = truncated
+ ? `Show patch preview (${Math.min(maxLines, lines.length)} of ${lines.length} lines)`
+ : `Show patch (${lines.length} lines)`;
+ return `\n\n${summary}
\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n `;
+ }
async function main() {
- function generatePatchPreview(patchContent) {
- if (!patchContent || !patchContent.trim()) {
- return "";
- }
- const lines = patchContent.split("\n");
- const maxLines = 500;
- if (lines.length <= maxLines) {
- return `\n\nShow patch (${lines.length} lines)
\n\n\`\`\`diff\n${patchContent}\n\`\`\`\n\n `;
- }
- const preview = lines.slice(0, maxLines).join("\n");
- return `\n\nShow patch preview (${maxLines} of ${lines.length} lines)
\n\n\`\`\`diff\n${preview}\n... (truncated)\n\`\`\`\n\n `;
- }
const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true";
const workflowId = process.env.GITHUB_AW_WORKFLOW_ID;
if (!workflowId) {
diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml
index a6e468fa508..b036d5c6240 100644
--- a/.github/workflows/poem-bot.lock.yml
+++ b/.github/workflows/poem-bot.lock.yml
@@ -4524,19 +4524,26 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- async function main() {
- function generatePatchPreview(patchContent) {
- if (!patchContent || !patchContent.trim()) {
- return "";
- }
- const lines = patchContent.split("\n");
- const maxLines = 500;
- if (lines.length <= maxLines) {
- return `\n\nShow patch (${lines.length} lines)
\n\n\`\`\`diff\n${patchContent}\n\`\`\`\n\n `;
- }
- const preview = lines.slice(0, maxLines).join("\n");
- return `\n\nShow patch preview (${maxLines} of ${lines.length} lines)
\n\n\`\`\`diff\n${preview}\n... (truncated)\n\`\`\`\n\n `;
+ function generatePatchPreview(patchContent) {
+ if (!patchContent || !patchContent.trim()) {
+ return "";
}
+ const lines = patchContent.split("\n");
+ const maxLines = 500;
+ const maxChars = 2000;
+ let preview = lines.length <= maxLines ? patchContent : lines.slice(0, maxLines).join("\n");
+ const lineTruncated = lines.length > maxLines;
+ const charTruncated = preview.length > maxChars;
+ if (charTruncated) {
+ preview = preview.slice(0, maxChars);
+ }
+ const truncated = lineTruncated || charTruncated;
+ const summary = truncated
+ ? `Show patch preview (${Math.min(maxLines, lines.length)} of ${lines.length} lines)`
+ : `Show patch (${lines.length} lines)`;
+ return `\n\n${summary}
\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n `;
+ }
+ async function main() {
const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true";
const workflowId = process.env.GITHUB_AW_WORKFLOW_ID;
if (!workflowId) {
diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml
index 4aabaa1ace1..113104e0792 100644
--- a/.github/workflows/security-fix-pr.lock.yml
+++ b/.github/workflows/security-fix-pr.lock.yml
@@ -3101,19 +3101,26 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
+ function generatePatchPreview(patchContent) {
+ if (!patchContent || !patchContent.trim()) {
+ return "";
+ }
+ const lines = patchContent.split("\n");
+ const maxLines = 500;
+ const maxChars = 2000;
+ let preview = lines.length <= maxLines ? patchContent : lines.slice(0, maxLines).join("\n");
+ const lineTruncated = lines.length > maxLines;
+ const charTruncated = preview.length > maxChars;
+ if (charTruncated) {
+ preview = preview.slice(0, maxChars);
+ }
+ const truncated = lineTruncated || charTruncated;
+ const summary = truncated
+ ? `Show patch preview (${Math.min(maxLines, lines.length)} of ${lines.length} lines)`
+ : `Show patch (${lines.length} lines)`;
+ return `\n\n${summary}
\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n `;
+ }
async function main() {
- function generatePatchPreview(patchContent) {
- if (!patchContent || !patchContent.trim()) {
- return "";
- }
- const lines = patchContent.split("\n");
- const maxLines = 500;
- if (lines.length <= maxLines) {
- return `\n\nShow patch (${lines.length} lines)
\n\n\`\`\`diff\n${patchContent}\n\`\`\`\n\n `;
- }
- const preview = lines.slice(0, maxLines).join("\n");
- return `\n\nShow patch preview (${maxLines} of ${lines.length} lines)
\n\n\`\`\`diff\n${preview}\n... (truncated)\n\`\`\`\n\n `;
- }
const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true";
const workflowId = process.env.GITHUB_AW_WORKFLOW_ID;
if (!workflowId) {
diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml
index f05ea1fb8c4..feaffdc3e8a 100644
--- a/.github/workflows/technical-doc-writer.lock.yml
+++ b/.github/workflows/technical-doc-writer.lock.yml
@@ -3478,19 +3478,26 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- async function main() {
- function generatePatchPreview(patchContent) {
- if (!patchContent || !patchContent.trim()) {
- return "";
- }
- const lines = patchContent.split("\n");
- const maxLines = 500;
- if (lines.length <= maxLines) {
- return `\n\nShow patch (${lines.length} lines)
\n\n\`\`\`diff\n${patchContent}\n\`\`\`\n\n `;
- }
- const preview = lines.slice(0, maxLines).join("\n");
- return `\n\nShow patch preview (${maxLines} of ${lines.length} lines)
\n\n\`\`\`diff\n${preview}\n... (truncated)\n\`\`\`\n\n `;
+ function generatePatchPreview(patchContent) {
+ if (!patchContent || !patchContent.trim()) {
+ return "";
}
+ const lines = patchContent.split("\n");
+ const maxLines = 500;
+ const maxChars = 2000;
+ let preview = lines.length <= maxLines ? patchContent : lines.slice(0, maxLines).join("\n");
+ const lineTruncated = lines.length > maxLines;
+ const charTruncated = preview.length > maxChars;
+ if (charTruncated) {
+ preview = preview.slice(0, maxChars);
+ }
+ const truncated = lineTruncated || charTruncated;
+ const summary = truncated
+ ? `Show patch preview (${Math.min(maxLines, lines.length)} of ${lines.length} lines)`
+ : `Show patch (${lines.length} lines)`;
+ return `\n\n${summary}
\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n `;
+ }
+ async function main() {
const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true";
const workflowId = process.env.GITHUB_AW_WORKFLOW_ID;
if (!workflowId) {
diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml
index db9557c0945..32b70668b0a 100644
--- a/.github/workflows/tidy.lock.yml
+++ b/.github/workflows/tidy.lock.yml
@@ -3493,19 +3493,26 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- async function main() {
- function generatePatchPreview(patchContent) {
- if (!patchContent || !patchContent.trim()) {
- return "";
- }
- const lines = patchContent.split("\n");
- const maxLines = 500;
- if (lines.length <= maxLines) {
- return `\n\nShow patch (${lines.length} lines)
\n\n\`\`\`diff\n${patchContent}\n\`\`\`\n\n `;
- }
- const preview = lines.slice(0, maxLines).join("\n");
- return `\n\nShow patch preview (${maxLines} of ${lines.length} lines)
\n\n\`\`\`diff\n${preview}\n... (truncated)\n\`\`\`\n\n `;
+ function generatePatchPreview(patchContent) {
+ if (!patchContent || !patchContent.trim()) {
+ return "";
}
+ const lines = patchContent.split("\n");
+ const maxLines = 500;
+ const maxChars = 2000;
+ let preview = lines.length <= maxLines ? patchContent : lines.slice(0, maxLines).join("\n");
+ const lineTruncated = lines.length > maxLines;
+ const charTruncated = preview.length > maxChars;
+ if (charTruncated) {
+ preview = preview.slice(0, maxChars);
+ }
+ const truncated = lineTruncated || charTruncated;
+ const summary = truncated
+ ? `Show patch preview (${Math.min(maxLines, lines.length)} of ${lines.length} lines)`
+ : `Show patch (${lines.length} lines)`;
+ return `\n\n${summary}
\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n `;
+ }
+ async function main() {
const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true";
const workflowId = process.env.GITHUB_AW_WORKFLOW_ID;
if (!workflowId) {
diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml
index 501ef36a8a4..bf70d08ebab 100644
--- a/.github/workflows/unbloat-docs.lock.yml
+++ b/.github/workflows/unbloat-docs.lock.yml
@@ -3715,19 +3715,26 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- async function main() {
- function generatePatchPreview(patchContent) {
- if (!patchContent || !patchContent.trim()) {
- return "";
- }
- const lines = patchContent.split("\n");
- const maxLines = 500;
- if (lines.length <= maxLines) {
- return `\n\nShow patch (${lines.length} lines)
\n\n\`\`\`diff\n${patchContent}\n\`\`\`\n\n `;
- }
- const preview = lines.slice(0, maxLines).join("\n");
- return `\n\nShow patch preview (${maxLines} of ${lines.length} lines)
\n\n\`\`\`diff\n${preview}\n... (truncated)\n\`\`\`\n\n `;
+ function generatePatchPreview(patchContent) {
+ if (!patchContent || !patchContent.trim()) {
+ return "";
}
+ const lines = patchContent.split("\n");
+ const maxLines = 500;
+ const maxChars = 2000;
+ let preview = lines.length <= maxLines ? patchContent : lines.slice(0, maxLines).join("\n");
+ const lineTruncated = lines.length > maxLines;
+ const charTruncated = preview.length > maxChars;
+ if (charTruncated) {
+ preview = preview.slice(0, maxChars);
+ }
+ const truncated = lineTruncated || charTruncated;
+ const summary = truncated
+ ? `Show patch preview (${Math.min(maxLines, lines.length)} of ${lines.length} lines)`
+ : `Show patch (${lines.length} lines)`;
+ return `\n\n${summary}
\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n `;
+ }
+ async function main() {
const isStaged = process.env.GITHUB_AW_SAFE_OUTPUTS_STAGED === "true";
const workflowId = process.env.GITHUB_AW_WORKFLOW_ID;
if (!workflowId) {
diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go
index 1b92107c885..1f44c806fef 100644
--- a/pkg/cli/add_command.go
+++ b/pkg/cli/add_command.go
@@ -804,6 +804,42 @@ func addSourceToWorkflow(content, source string) (string, error) {
return "", fmt.Errorf("failed to parse frontmatter: %w", err)
}
+ // Try to preserve original frontmatter formatting by manually inserting the source field
+ if len(result.FrontmatterLines) > 0 {
+ // Check if source field already exists
+ if result.Frontmatter != nil {
+ if _, exists := result.Frontmatter["source"]; exists {
+ // Source field exists, replace it by parsing and re-marshaling (fallback behavior)
+ return addSourceToWorkflowFallback(result, source)
+ }
+ }
+
+ // Source field doesn't exist, insert it manually to preserve formatting
+ frontmatterLines := make([]string, len(result.FrontmatterLines))
+ copy(frontmatterLines, result.FrontmatterLines)
+
+ // Add source field at the end of the frontmatter, preserving original formatting
+ sourceField := fmt.Sprintf("source: %s", source)
+ frontmatterLines = append(frontmatterLines, sourceField)
+
+ // Reconstruct the file with preserved formatting
+ var lines []string
+ lines = append(lines, "---")
+ lines = append(lines, frontmatterLines...)
+ lines = append(lines, "---")
+ if result.Markdown != "" {
+ lines = append(lines, result.Markdown)
+ }
+
+ return strings.Join(lines, "\n"), nil
+ }
+
+ // Fallback to original behavior if no frontmatter lines are available
+ return addSourceToWorkflowFallback(result, source)
+}
+
+// addSourceToWorkflowFallback implements the original behavior as a fallback
+func addSourceToWorkflowFallback(result *parser.FrontmatterResult, source string) (string, error) {
// Initialize frontmatter if it doesn't exist
if result.Frontmatter == nil {
result.Frontmatter = make(map[string]any)
diff --git a/pkg/cli/add_source_test.go b/pkg/cli/add_source_test.go
index 2b213f62917..739fb5614de 100644
--- a/pkg/cli/add_source_test.go
+++ b/pkg/cli/add_source_test.go
@@ -79,6 +79,41 @@ This workflow has complex 'on' triggers.`,
expectError: false,
checkSource: true,
},
+ {
+ name: "preserve_formatting_with_comments_and_blank_lines",
+ content: `---
+on:
+ workflow_dispatch:
+
+ schedule:
+ # Run daily at 2am UTC, all days except Saturday and Sunday
+ - cron: "0 2 * * 1-5"
+
+ stop-after: +48h # workflow will no longer trigger after 48 hours
+
+timeout_minutes: 30
+
+permissions: read-all
+
+network: defaults
+
+engine: claude
+
+tools:
+ # Web tools for testing
+ web-search: null
+
+ # Memory cache
+ cache-memory: true
+---
+
+# Well Formatted Workflow
+
+This workflow has proper formatting with comments and blank lines.`,
+ source: "githubnext/agentics/workflows/formatted.md@v1.0.0",
+ expectError: false,
+ checkSource: true,
+ },
}
for _, tt := range tests {
@@ -113,11 +148,31 @@ This workflow has complex 'on' triggers.`,
if strings.Contains(tt.content, "# Test Workflow") && !strings.Contains(result, "# Test Workflow") {
t.Errorf("addSourceToWorkflow() result does not preserve markdown content")
}
+ if strings.Contains(tt.content, "# Well Formatted Workflow") && !strings.Contains(result, "# Well Formatted Workflow") {
+ t.Errorf("addSourceToWorkflow() result does not preserve markdown content")
+ }
// Verify that "on" keyword is not quoted
if strings.Contains(result, `"on":`) {
t.Errorf("addSourceToWorkflow() result contains quoted 'on' keyword, should be unquoted. Result:\n%s", result)
}
+
+ // For the formatting preservation test, verify that comments and blank lines are preserved
+ if tt.name == "preserve_formatting_with_comments_and_blank_lines" {
+ if !strings.Contains(result, "# Run daily at 2am UTC, all days except Saturday and Sunday") {
+ t.Errorf("addSourceToWorkflow() result does not preserve comments")
+ }
+ if !strings.Contains(result, "stop-after: +48h # workflow will no longer trigger") {
+ t.Errorf("addSourceToWorkflow() result does not preserve inline comments")
+ }
+ if !strings.Contains(result, " # Web tools for testing") {
+ t.Errorf("addSourceToWorkflow() result does not preserve indented comments")
+ }
+ // Check that there are still blank lines by checking for consecutive newlines
+ if !strings.Contains(result, "\n\n") {
+ t.Errorf("addSourceToWorkflow() result does not preserve blank lines")
+ }
+ }
}
})
}
diff --git a/pkg/cli/format_preservation_test.go b/pkg/cli/format_preservation_test.go
new file mode 100644
index 00000000000..b64fbff8611
--- /dev/null
+++ b/pkg/cli/format_preservation_test.go
@@ -0,0 +1,69 @@
+package cli
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func TestFormatPreservationManual(t *testing.T) {
+ content := `---
+on:
+ workflow_dispatch:
+
+ schedule:
+ # Daily run at 9 AM UTC on weekdays
+ - cron: "0 9 * * 1-5"
+
+ # Auto-stop workflow after 2 hours
+ stop-after: +2h
+
+timeout_minutes: 45
+
+permissions: read-all
+
+network: defaults
+
+engine: claude
+
+tools:
+ # Enable web search functionality
+ web-search: null
+
+ # Memory caching for better performance
+ cache-memory: true
+
+---
+
+# Test Formatting Preservation
+
+This workflow is designed to test whether the formatting is preserved.`
+
+ result, err := addSourceToWorkflow(content, "test/repo/workflow.md@v1.0.0")
+ if err != nil {
+ t.Fatalf("Error adding source: %v", err)
+ }
+
+ fmt.Printf("=== ORIGINAL ===\n%s\n\n", content)
+ fmt.Printf("=== RESULT ===\n%s\n", result)
+
+ // Check if comments are preserved
+ if !strings.Contains(result, "# Daily run at 9 AM UTC on weekdays") {
+ t.Error("Comments were not preserved")
+ }
+
+ // Check if blank lines are preserved (look for consecutive newlines)
+ if !strings.Contains(result, "\n\n") {
+ t.Error("Blank lines were not preserved")
+ }
+
+ // Check if inline comments are preserved
+ if !strings.Contains(result, "stop-after: +2h") {
+ t.Error("Inline comments were not preserved")
+ }
+
+ // Check if indentation is preserved (4-space indentation)
+ if !strings.Contains(result, " workflow_dispatch:") {
+ t.Error("4-space indentation was not preserved")
+ }
+}
diff --git a/pkg/cli/update_command.go b/pkg/cli/update_command.go
index 19e5b4119a3..38929e72b36 100644
--- a/pkg/cli/update_command.go
+++ b/pkg/cli/update_command.go
@@ -2,6 +2,7 @@ package cli
import (
"fmt"
+ "math/rand"
"os"
"os/exec"
"path/filepath"
@@ -18,33 +19,38 @@ import (
func NewUpdateCommand(validateEngine func(string) error) *cobra.Command {
cmd := &cobra.Command{
Use: "update [workflow-name]...",
- Short: "Update workflows from their source repositories",
- Long: `Update one or more workflows from their source repositories.
+ Short: "Update workflows from their source repositories and check for gh-aw updates",
+ Long: `Update one or more workflows from their source repositories and check for gh-aw updates.
-The command uses the 'source' field in the workflow frontmatter to determine
-the source repository and version. It then fetches the latest version based on
-the current ref:
+The command:
+1. Checks if a newer version of gh-aw is available
+2. Updates workflows using the 'source' field in the workflow frontmatter
+3. Recompiles all workflows
+
+For workflow updates, it fetches the latest version based on the current ref:
- If the ref is a tag, it updates to the latest release (use --major for major version updates)
- If the ref is a branch, it fetches the latest commit from that branch
- Otherwise, it fetches the latest commit from the default branch
Examples:
- ` + constants.CLIExtensionPrefix + ` update # Update all workflows with source field
- ` + constants.CLIExtensionPrefix + ` update ci-doctor # Update specific workflow
+ ` + constants.CLIExtensionPrefix + ` update # Check gh-aw updates and update all workflows
+ ` + constants.CLIExtensionPrefix + ` update ci-doctor # Check gh-aw updates and update specific workflow
` + constants.CLIExtensionPrefix + ` update ci-doctor --major # Allow major version updates
+ ` + constants.CLIExtensionPrefix + ` update --pr # Create PR with changes
` + constants.CLIExtensionPrefix + ` update --force # Force update even if no changes`,
Run: func(cmd *cobra.Command, args []string) {
majorFlag, _ := cmd.Flags().GetBool("major")
forceFlag, _ := cmd.Flags().GetBool("force")
engineOverride, _ := cmd.Flags().GetString("engine")
verbose, _ := cmd.Flags().GetBool("verbose")
+ prFlag, _ := cmd.Flags().GetBool("pr")
if err := validateEngine(engineOverride); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
os.Exit(1)
}
- if err := UpdateWorkflows(args, majorFlag, forceFlag, verbose, engineOverride); err != nil {
+ if err := UpdateWorkflowsWithExtensionCheck(args, majorFlag, forceFlag, verbose, engineOverride, prFlag); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
os.Exit(1)
}
@@ -54,10 +60,188 @@ Examples:
cmd.Flags().Bool("major", false, "Allow major version updates when updating tagged releases")
cmd.Flags().Bool("force", false, "Force update even if no changes detected")
cmd.Flags().StringP("engine", "a", "", "Override AI engine (claude, codex, copilot)")
+ cmd.Flags().Bool("pr", false, "Create a pull request with the workflow changes")
return cmd
}
+// checkExtensionUpdate checks if a newer version of gh-aw is available
+func checkExtensionUpdate(verbose bool) error {
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Checking for gh-aw extension updates..."))
+ }
+
+ // Run gh extension upgrade --dry-run to check for updates
+ cmd := exec.Command("gh", "extension", "upgrade", "githubnext/gh-aw", "--dry-run")
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check for extension updates: %v", err)))
+ }
+ return nil // Don't fail the whole command if update check fails
+ }
+
+ outputStr := strings.TrimSpace(string(output))
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Extension update check output: %s", outputStr)))
+ }
+
+ // Parse the output to see if an update is available
+ // Expected format: "[aw]: would have upgraded from v0.14.0 to v0.18.1"
+ lines := strings.Split(outputStr, "\n")
+ for _, line := range lines {
+ if strings.Contains(line, "[aw]: would have upgraded from") {
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage(line))
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Run 'gh extension upgrade githubnext/gh-aw' to update"))
+ return nil
+ }
+ }
+
+ if strings.Contains(outputStr, "✓ Successfully checked extension upgrades") {
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("gh-aw extension is up to date"))
+ }
+ }
+
+ return nil
+}
+
+// runCompileWorkflows runs the compile command to recompile all workflows
+func runCompileWorkflows(verbose bool, engineOverride string) error {
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Compiling workflows..."))
+ }
+
+ // Create a compiler instance similar to how compile command does it
+ compiler := workflow.NewCompiler(verbose, engineOverride, GetVersion())
+
+ // Compile all workflows in the workflows directory
+ workflowsDir := getWorkflowsDir()
+ err := compileAllWorkflowFiles(compiler, workflowsDir, verbose)
+ if err != nil {
+ return fmt.Errorf("failed to compile workflows: %w", err)
+ }
+
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Successfully compiled all workflows"))
+ }
+ return nil
+}
+
+// UpdateWorkflowsWithExtensionCheck performs the complete update process:
+// 1. Check for gh-aw extension updates
+// 2. Update workflows from source repositories
+// 3. Compile all workflows
+// 4. Optionally create a PR
+func UpdateWorkflowsWithExtensionCheck(workflowNames []string, allowMajor, force, verbose bool, engineOverride string, createPR bool) error {
+ // Step 1: Check for gh-aw extension updates
+ if err := checkExtensionUpdate(verbose); err != nil {
+ return fmt.Errorf("extension update check failed: %w", err)
+ }
+
+ // Step 2: Update workflows from source repositories
+ if err := UpdateWorkflows(workflowNames, allowMajor, force, verbose, engineOverride); err != nil {
+ return fmt.Errorf("workflow update failed: %w", err)
+ }
+
+ // Step 3: Compile all workflows
+ if err := runCompileWorkflows(verbose, engineOverride); err != nil {
+ return fmt.Errorf("compile failed: %w", err)
+ }
+
+ // Step 4: Optionally create PR if flag is set
+ if createPR {
+ if err := createUpdatePR(verbose); err != nil {
+ return fmt.Errorf("failed to create PR: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// hasGitChanges checks if there are any uncommitted changes
+func hasGitChanges() (bool, error) {
+ cmd := exec.Command("git", "status", "--porcelain")
+ output, err := cmd.Output()
+ if err != nil {
+ return false, fmt.Errorf("failed to check git status: %w", err)
+ }
+
+ return len(strings.TrimSpace(string(output))) > 0, nil
+}
+
+// runGitCommand runs a git command with the specified arguments
+func runGitCommand(args ...string) error {
+ cmd := exec.Command("git", args...)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("git %s failed: %w", strings.Join(args, " "), err)
+ }
+ return nil
+}
+
+// createUpdatePR creates a pull request with the workflow changes
+func createUpdatePR(verbose bool) error {
+ // Check if GitHub CLI is available
+ if !isGHCLIAvailable() {
+ return fmt.Errorf("GitHub CLI (gh) is required for PR creation but not found in PATH")
+ }
+
+ // Check if there are any changes to commit
+ hasChanges, err := hasGitChanges()
+ if err != nil {
+ return fmt.Errorf("failed to check git status: %w", err)
+ }
+
+ if !hasChanges {
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No changes to create PR for"))
+ return nil
+ }
+
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Creating pull request with workflow updates..."))
+ }
+
+ // Create a branch name with timestamp
+ randomNum := rand.Intn(9000) + 1000 // Generate number between 1000-9999
+ branchName := fmt.Sprintf("update-workflows-%d", randomNum)
+
+ // Create and checkout new branch
+ if err := runGitCommand("checkout", "-b", branchName); err != nil {
+ return fmt.Errorf("failed to create branch: %w", err)
+ }
+
+ // Add all changes
+ if err := runGitCommand("add", "."); err != nil {
+ return fmt.Errorf("failed to add changes: %w", err)
+ }
+
+ // Commit changes
+ commitMsg := "Update workflows and recompile"
+ if err := runGitCommand("commit", "-m", commitMsg); err != nil {
+ return fmt.Errorf("failed to commit changes: %w", err)
+ }
+
+ // Push branch
+ if err := runGitCommand("push", "-u", "origin", branchName); err != nil {
+ return fmt.Errorf("failed to push branch: %w", err)
+ }
+
+ // Create PR
+ cmd := exec.Command("gh", "pr", "create",
+ "--title", "Update workflows and recompile",
+ "--body", "This PR updates workflows from their source repositories and recompiles them.\n\nGenerated by `gh aw update --pr`")
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("failed to create PR: %w\nOutput: %s", err, string(output))
+ }
+
+ fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Successfully created pull request"))
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage(strings.TrimSpace(string(output))))
+
+ return nil
+}
+
// UpdateWorkflows updates workflows from their source repositories
func UpdateWorkflows(workflowNames []string, allowMajor, force, verbose bool, engineOverride string) error {
workflowsDir := getWorkflowsDir()