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
16 changes: 8 additions & 8 deletions .github/agents/agentic-workflows.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Workflows may optionally include:
- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md`
- Workflow lock files: `.github/workflows/*.lock.yml`
- Shared components: `.github/workflows/shared/*.md`
- Configuration: `.github/aw/github-agentic-workflows.md`
- Configuration: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md

## Problems This Solves

Expand All @@ -49,7 +49,7 @@ When you interact with this agent, it will:
### Create New Workflow
**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet

**Prompt file**: `.github/aw/create-agentic-workflow.md`
**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/create-agentic-workflow.md

**Use cases**:
- "Create a workflow that triages issues"
Expand All @@ -59,7 +59,7 @@ When you interact with this agent, it will:
### Update Existing Workflow
**Load when**: User wants to modify, improve, or refactor an existing workflow

**Prompt file**: `.github/aw/update-agentic-workflow.md`
**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/update-agentic-workflow.md

**Use cases**:
- "Add web-fetch tool to the issue-classifier workflow"
Expand All @@ -69,7 +69,7 @@ When you interact with this agent, it will:
### Debug Workflow
**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors

**Prompt file**: `.github/aw/debug-agentic-workflow.md`
**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/debug-agentic-workflow.md

**Use cases**:
- "Why is this workflow failing?"
Expand All @@ -79,7 +79,7 @@ When you interact with this agent, it will:
### Upgrade Agentic Workflows
**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations

**Prompt file**: `.github/aw/upgrade-agentic-workflows.md`
**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/upgrade-agentic-workflows.md

**Use cases**:
- "Upgrade all workflows to the latest version"
Expand All @@ -89,7 +89,7 @@ When you interact with this agent, it will:
### Create Shared Agentic Workflow
**Load when**: User wants to create a reusable workflow component or wrap an MCP server

**Prompt file**: `.github/aw/create-shared-agentic-workflow.md`
**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/create-shared-agentic-workflow.md

**Use cases**:
- "Create a shared component for Notion integration"
Expand All @@ -101,7 +101,7 @@ When you interact with this agent, it will:
When a user interacts with you:

1. **Identify the task type** from the user's request
2. **Load the appropriate prompt** using `.github/aw/<prompt-name>.md`
2. **Load the appropriate prompt** from the GitHub repository URLs listed above
3. **Follow the loaded prompt's instructions** exactly
4. **If uncertain**, ask clarifying questions to determine the right prompt

Expand Down Expand Up @@ -138,7 +138,7 @@ gh aw compile --validate

## Important Notes

- Always reference the instructions file at `.github/aw/github-agentic-workflows.md` for complete documentation
- Always reference the instructions file at https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md for complete documentation
- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud
- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions
- Follow security best practices: minimal permissions, explicit network access, no template injection
25 changes: 2 additions & 23 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ all: build

# Build the binary, run make deps before this
.PHONY: build
build: sync-templates sync-action-pins sync-action-scripts
build: sync-action-pins sync-action-scripts
go build $(LDFLAGS) -o $(BINARY_NAME) ./cmd/gh-aw

# Build for all platforms
Expand Down Expand Up @@ -579,26 +579,6 @@ clean-docs:
@echo "✓ Documentation artifacts cleaned"

# Sync templates from .github to pkg/cli/templates
.PHONY: sync-templates
sync-templates:
@echo "Syncing templates from .github to pkg/cli/templates..."
@mkdir -p pkg/cli/templates

# Workflow management templates
@cp .github/aw/github-agentic-workflows.md pkg/cli/templates/
@cp .github/aw/create-agentic-workflow.md pkg/cli/templates/
@cp .github/aw/create-shared-agentic-workflow.md pkg/cli/templates/
@cp .github/aw/debug-agentic-workflow.md pkg/cli/templates/
@cp .github/aw/update-agentic-workflow.md pkg/cli/templates/
@cp .github/aw/upgrade-agentic-workflows.md pkg/cli/templates/

# Agent templates
@cp .github/agents/agentic-workflows.agent.md pkg/cli/templates/

@echo "✓ Templates synced successfully"



# Sync action pins from .github/aw to pkg/workflow/data
.PHONY: sync-action-pins
sync-action-pins:
Expand All @@ -620,7 +600,7 @@ sync-action-scripts:

# Recompile all workflow files
.PHONY: recompile
recompile: sync-templates build
recompile: build
./$(BINARY_NAME) init --codespaces
./$(BINARY_NAME) compile --validate --verbose --purge --stats
# ./$(BINARY_NAME) compile --dir pkg/cli/workflows --validate --verbose --purge
Expand Down Expand Up @@ -736,7 +716,6 @@ help:
@echo " actionlint - Validate workflows with actionlint (depends on build)"
@echo " validate-workflows - Validate compiled workflow lock files (depends on build)"
@echo " install - Install binary locally"
@echo " sync-templates - Sync templates from .github to pkg/cli/templates (runs automatically during build)"
@echo " sync-action-pins - Sync actions-lock.json from .github/aw to pkg/workflow/data (runs automatically during build)"
@echo " sync-action-scripts - Sync install-gh-aw.sh to actions/setup-cli/install.sh (runs automatically during build)"
@echo " update - Update GitHub Actions and workflows, sync action pins, and rebuild binary"
Expand Down
54 changes: 20 additions & 34 deletions pkg/cli/agentic_workflow_agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/github/gh-aw/pkg/testutil"
Expand All @@ -16,22 +15,17 @@ func TestEnsureCreateWorkflowPrompt(t *testing.T) {
tests := []struct {
name string
existingContent string
expectedContent string
expectExists bool
}{
{
name: "creates new workflow creation prompt file",
name: "reports missing file without error",
existingContent: "",
expectedContent: strings.TrimSpace(createWorkflowPromptTemplate),
expectExists: false,
},
{
name: "does not modify existing correct file",
existingContent: createWorkflowPromptTemplate,
expectedContent: strings.TrimSpace(createWorkflowPromptTemplate),
},
{
name: "updates modified file",
existingContent: "# Modified Workflow Creation Prompt\n\nThis is a modified version.",
expectedContent: strings.TrimSpace(createWorkflowPromptTemplate),
name: "reports existing file",
existingContent: "# Test content",
expectExists: true,
},
}

Expand Down Expand Up @@ -74,24 +68,16 @@ func TestEnsureCreateWorkflowPrompt(t *testing.T) {
t.Fatalf("ensureCreateWorkflowPrompt() returned error: %v", err)
}

// Check that file exists
if _, err := os.Stat(promptPath); os.IsNotExist(err) {
t.Fatalf("Expected prompt file to exist")
}

// Check content
content, err := os.ReadFile(promptPath)
if err != nil {
t.Fatalf("Failed to read prompt: %v", err)
}
// Check that file exists or not based on test expectation
_, statErr := os.Stat(promptPath)
fileExists := statErr == nil

contentStr := strings.TrimSpace(string(content))
expectedStr := strings.TrimSpace(tt.expectedContent)

if contentStr != expectedStr {
t.Errorf("Expected content does not match.\nExpected first 100 chars: %q\nActual first 100 chars: %q",
expectedStr[:min(100, len(expectedStr))],
contentStr[:min(100, len(contentStr))])
if fileExists != tt.expectExists {
if tt.expectExists {
t.Errorf("Expected prompt file to exist, but it doesn't")
} else {
t.Errorf("Expected prompt file to not exist, but it does")
}
}
})
}
Expand Down Expand Up @@ -130,7 +116,7 @@ func TestEnsureCreateWorkflowPrompt_WithSkipInstructionsTrue(t *testing.T) {
}
}

func TestEnsureCreateWorkflowPrompt_CreatesNewFile(t *testing.T) {
func TestEnsureCreateWorkflowPrompt_ReportsNonExistent(t *testing.T) {
// Create a temporary directory for testing
tempDir := testutil.TempDir(t, "test-*")

Expand All @@ -149,16 +135,16 @@ func TestEnsureCreateWorkflowPrompt_CreatesNewFile(t *testing.T) {
t.Fatalf("Failed to init git repo: %v", err)
}

// Call the function to create prompt
// Call the function - it should not error even if file doesn't exist
err = ensureCreateWorkflowPrompt(false, false)
if err != nil {
t.Fatalf("ensureCreateWorkflowPrompt() returned error: %v", err)
}

// Check that new prompt file was created in .github/aw/
// Check that new prompt file was NOT created (files are source of truth in .github/aw/)
awDir := filepath.Join(tempDir, ".github", "aw")
newPromptPath := filepath.Join(awDir, "create-agentic-workflow.md")
if _, err := os.Stat(newPromptPath); os.IsNotExist(err) {
t.Fatalf("Expected new prompt file to be created in .github/aw/")
if _, err := os.Stat(newPromptPath); !os.IsNotExist(err) {
t.Fatalf("Expected new prompt file to NOT be created (files are source of truth)")
}
}
102 changes: 78 additions & 24 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package cli

import (
_ "embed"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/constants"
Expand All @@ -28,39 +30,91 @@ func init() {
workflow.SetDefaultVersion(version)
}

//go:embed templates/github-agentic-workflows.md
var copilotInstructionsTemplate string
// SetVersionInfo sets the version information for the CLI and workflow package
func SetVersionInfo(v string) {
version = v
workflow.SetDefaultVersion(v) // Keep workflow package in sync
}

//go:embed templates/agentic-workflows.agent.md
var agenticWorkflowsDispatcherTemplate string
// GetVersion returns the current version
func GetVersion() string {
return version
}

//go:embed templates/create-agentic-workflow.md
var createWorkflowPromptTemplate string
// downloadAgentFileFromGitHub downloads the agentic-workflows.agent.md file from GitHub
func downloadAgentFileFromGitHub(verbose bool) (string, error) {
commandsLog.Print("Downloading agentic-workflows.agent.md from GitHub")

// Determine the ref to use (tag for releases, main for dev builds)
ref := "main"
currentVersion := GetVersion()

// If version looks like a release tag (starts with v and contains dots), use it
isRelease := strings.HasPrefix(currentVersion, "v") && strings.Contains(currentVersion, ".")
Comment on lines +52 to +53
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version detection logic assumes that release versions always start with v and contain dots (e.g., v1.2.3). However, this could fail for valid version formats such as:

  • v1 (single number version)
  • v1.2.3-rc1 (pre-release with suffix)
  • 1.2.3 (without v prefix)

The current logic would incorrectly treat v1 as a dev build (missing dot) and use main branch instead of the v1 tag.

Consider using a more robust version detection approach:

  1. Check if version starts with v followed by at least one digit
  2. Or use semantic version parsing libraries
  3. Or document the exact version format requirements

Example improvement:

import "regexp"
// Match semantic version pattern: v1, v1.2, v1.2.3, v1.2.3-beta, etc.
isRelease := regexp.MustCompile(`^v\d`).MatchString(currentVersion)

This is a minor edge case that may not affect most users, but it could cause confusion if uncommon versioning schemes are used.

Copilot uses AI. Check for mistakes.
if isRelease {
ref = currentVersion
commandsLog.Printf("Using release tag: %s", ref)
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Using release version: %s", ref)))
}
} else {
commandsLog.Print("Using main branch for dev build")
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Using main branch (dev build)"))
}
}

//go:embed templates/update-agentic-workflow.md
var updateWorkflowPromptTemplate string
// Construct the raw GitHub URL
url := fmt.Sprintf("https://raw.githubusercontent.com/github/gh-aw/%s/.github/agents/agentic-workflows.agent.md", ref)
commandsLog.Printf("Downloading from URL: %s", url)

//go:embed templates/create-shared-agentic-workflow.md
var createSharedAgenticWorkflowPromptTemplate string
// Create HTTP client with timeout
client := &http.Client{
Timeout: 30 * time.Second,
}

//go:embed templates/debug-agentic-workflow.md
var debugWorkflowPromptTemplate string
// Download the file
resp, err := client.Get(url)
if err != nil {
return "", fmt.Errorf("failed to download agent file: %w", err)
}
defer resp.Body.Close()

//go:embed templates/upgrade-agentic-workflows.md
var upgradeAgenticWorkflowsPromptTemplate string
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download agent file: HTTP %d", resp.StatusCode)
}

//go:embed templates/serena-tool.md
var serenaToolTemplate string
// Read the content
content, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read agent file content: %w", err)
}

// SetVersionInfo sets the version information for the CLI and workflow package
func SetVersionInfo(v string) {
version = v
workflow.SetDefaultVersion(v) // Keep workflow package in sync
contentStr := string(content)

// Patch URLs to match the current version/ref
patchedContent := patchAgentFileURLs(contentStr, ref)
if patchedContent != contentStr && verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Patched URLs to use ref: %s", ref)))
}

commandsLog.Printf("Successfully downloaded agent file (%d bytes)", len(patchedContent))
return patchedContent, nil
}
Comment on lines +45 to 103
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The downloadAgentFileFromGitHub() function has no test coverage. While integration testing network calls can be complex, consider adding unit tests for:

  1. Error handling paths: Test that errors are properly formatted and returned
  2. URL construction logic: Test that the URL is correctly constructed for different version formats
  3. HTTP response handling: Mock HTTP responses to test status code handling (404, 500, etc.)
  4. Content patching: Test the integration between download and URL patching

Example test structure:

func TestDownloadAgentFileFromGitHub(t *testing.T) {
    // Use httptest to mock GitHub responses
    tests := []struct {
        name          string
        version       string
        mockResponse  string
        mockStatus    int
        expectedError bool
        expectedRef   string
    }{
        {
            name: "successful download for release version",
            version: "v1.2.3",
            mockResponse: "# Agent content with `.github/aw/file.md`",
            mockStatus: 200,
            expectedError: false,
            expectedRef: "v1.2.3",
        },
        // ... more test cases
    }
}

This would improve confidence in the download logic without requiring actual network calls during testing.

Copilot uses AI. Check for mistakes.

// GetVersion returns the current version
func GetVersion() string {
return version
// patchAgentFileURLs patches URLs in the agent file to use the correct ref
func patchAgentFileURLs(content, ref string) string {
// Pattern 1: Convert local paths to GitHub URLs
// `.github/aw/file.md` -> `https://github.com/github/gh-aw/blob/{ref}/.github/aw/file.md`
content = strings.ReplaceAll(content, "`.github/aw/", fmt.Sprintf("`https://github.com/github/gh-aw/blob/%s/.github/aw/", ref))

// Pattern 2: Update existing GitHub URLs to use the correct ref
// https://github.com/github/gh-aw/blob/main/ -> https://github.com/github/gh-aw/blob/{ref}/
if ref != "main" {
content = strings.ReplaceAll(content, "/blob/main/", fmt.Sprintf("/blob/%s/", ref))
}

return content
}
Comment on lines +106 to 118
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL patching logic in patchAgentFileURLs() uses simple strings.ReplaceAll() which could potentially cause unintended replacements in edge cases.

Consider this scenario: if the agent file contains explanatory text like "Files stored at .github/aw/ are treated as..." or code examples showing how to write paths like ".github/aw/file.md", ALL occurrences will be patched, not just those in actual reference links.

While this may not be a critical issue in practice (the agent file likely only has reference links), the implementation could be more precise by:

  1. Using a more specific pattern that only matches actual markdown links
  2. Or documenting that the agent file should avoid using this exact pattern in non-reference contexts

However, given the scope of the PR and the controlled nature of the agent file content, this is a minor concern that could be addressed in a follow-up if issues arise.

Copilot uses AI. Check for mistakes.

func isGHCLIAvailable() bool {
Expand Down
Loading
Loading