Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
9 changes: 8 additions & 1 deletion .github/workflows/test-claude.lock.yml

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

4 changes: 3 additions & 1 deletion docs/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ output:
pull-request:
title-prefix: "[ai] " # Optional: prefix for PR titles
labels: [automation, ai-agent] # Optional: labels to attach to PRs
draft: true # Optional: create as draft PR (defaults to true)
```

### Issue Creation (`output.issue`)
Expand Down Expand Up @@ -353,7 +354,7 @@ This automatically creates GitHub issues or comments from the agent's analysis w
- **Dependencies**: Runs after the main agent job (`needs: [main-job-name]`)
- **Permissions**: Only the pull request creation job has `contents: write` and `pull-requests: write` permissions
- **Timeout**: 10-minute timeout to prevent hanging
- **Environment Variables**: Configuration passed via `GITHUB_AW_PR_TITLE_PREFIX`, `GITHUB_AW_PR_LABELS`, `GITHUB_AW_WORKFLOW_ID`, and `GITHUB_AW_BASE_BRANCH`
- **Environment Variables**: Configuration passed via `GITHUB_AW_PR_TITLE_PREFIX`, `GITHUB_AW_PR_LABELS`, `GITHUB_AW_PR_DRAFT`, `GITHUB_AW_WORKFLOW_ID`, and `GITHUB_AW_BASE_BRANCH`
- **Branch Creation**: Uses cryptographic random hex for secure branch naming (`{workflowId}/{randomHex}`)
- **Git Operations**: Creates branch using git CLI, applies patches, commits changes, and pushes to GitHub
- **Outputs**: Returns `pr_number` and `pr_url` for downstream jobs
Expand All @@ -364,6 +365,7 @@ output:
pull-request:
title-prefix: "[ai] " # Optional: prefix for PR titles
labels: [automation, ai-agent] # Optional: labels to attach to PRs
draft: true # Optional: create as draft PR (defaults to true)
```

**Example workflow using pull request creation:**
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

3 changes: 3 additions & 0 deletions pkg/cli/templates/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ The YAML frontmatter supports these fields:
pull-request:
title-prefix: "[ai] " # Optional: prefix for PR titles
labels: [automation, ai-agent] # Optional: labels to attach to PRs
draft: true # Optional: create as draft PR (defaults to true)
```
**Important**: When using `output.pull-request`, the main job does **not** need `contents: write` or `pull-requests: write` permissions since PR creation is handled by a separate job with appropriate permissions. The agent must create git patches in `/tmp/aw.patch`.

Expand Down Expand Up @@ -392,6 +393,7 @@ output:
# pull-request:
# title-prefix: "[ai] "
# labels: [automation]
# draft: false # Create non-draft PR
# OR for comments:
# comment: {}
```
Expand Down Expand Up @@ -450,6 +452,7 @@ output:
pull-request:
title-prefix: "[bot] "
labels: [automation, ai-generated]
draft: false # Create non-draft PR for immediate review
---

# Code Improvement Agent
Expand Down
4 changes: 4 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,10 @@
"items": {
"type": "string"
}
},
"draft": {
"type": "boolean",
"description": "Whether to create pull request as draft (defaults to true)"
}
},
"additionalProperties": false
Expand Down
14 changes: 14 additions & 0 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ type CommentConfig struct {
type PullRequestConfig struct {
TitlePrefix string `yaml:"title-prefix,omitempty"`
Labels []string `yaml:"labels,omitempty"`
Draft *bool `yaml:"draft,omitempty"` // Pointer to distinguish between unset (nil) and explicitly false
}

// CompileWorkflow converts a markdown workflow to GitHub Actions YAML
Expand Down Expand Up @@ -1769,6 +1770,12 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa
labelsStr := strings.Join(data.Output.PullRequest.Labels, ",")
steps = append(steps, fmt.Sprintf(" GITHUB_AW_PR_LABELS: %q\n", labelsStr))
}
// Pass draft setting - default to true for backwards compatibility
draftValue := true // Default value
if data.Output.PullRequest.Draft != nil {
draftValue = *data.Output.PullRequest.Draft
}
steps = append(steps, fmt.Sprintf(" GITHUB_AW_PR_DRAFT: %q\n", fmt.Sprintf("%t", draftValue)))

steps = append(steps, " with:\n")
steps = append(steps, " script: |\n")
Expand Down Expand Up @@ -2246,6 +2253,13 @@ func (c *Compiler) extractOutputConfig(frontmatter map[string]any) *OutputConfig
}
}

// Parse draft
if draft, exists := pullRequestMap["draft"]; exists {
if draftBool, ok := draft.(bool); ok {
pullRequestConfig.Draft = &draftBool
}
}

config.PullRequest = pullRequestConfig
}
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/workflow/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,13 @@ async function main() {
const labelsEnv = process.env.GITHUB_AW_PR_LABELS;
const labels = labelsEnv ? labelsEnv.split(',').map(label => label.trim()).filter(label => label) : [];

// Parse draft setting from environment variable (defaults to true)
const draftEnv = process.env.GITHUB_AW_PR_DRAFT;
const draft = draftEnv ? draftEnv.toLowerCase() === 'true' : true;

console.log('Creating pull request with title:', title);
console.log('Labels:', labels);
console.log('Draft:', draft);
console.log('Body length:', body.length);

// Generate unique branch name using cryptographic random hex
Expand Down Expand Up @@ -124,7 +129,8 @@ async function main() {
title: title,
body: body,
head: branchName,
base: baseBranch
base: baseBranch,
draft: draft
});

console.log('Created pull request #' + pullRequest.number + ': ' + pullRequest.html_url);
Expand Down
155 changes: 155 additions & 0 deletions pkg/workflow/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,10 +564,165 @@ This workflow tests the create_pull_request job generation.
t.Error("Expected automation label to be set as environment variable")
}

// Verify draft setting defaults to true
if !strings.Contains(lockContentStr, "GITHUB_AW_PR_DRAFT: \"true\"") {
t.Error("Expected draft to default to true when not specified")
}

// Verify job dependencies
if !strings.Contains(lockContentStr, "needs: test-output-pull-request-job-generation") {
t.Error("Expected create_pull_request job to depend on main job")
}

t.Logf("Generated workflow content:\n%s", lockContentStr)
}

func TestOutputPullRequestDraftFalse(t *testing.T) {
// Create temporary directory for test files
tmpDir, err := os.MkdirTemp("", "output-pr-draft-false-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)

// Test case with output.pull-request configuration with draft: false
testContent := `---
on: push
permissions:
contents: read
pull-requests: write
tools:
github:
allowed: [list_issues]
engine: claude
output:
pull-request:
title-prefix: "[agent] "
labels: [automation]
draft: false
---

# Test Output Pull Request with Draft False

This workflow tests the create_pull_request job generation with draft: false.
`

testFile := filepath.Join(tmpDir, "test-output-pr-draft-false.md")
if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
t.Fatal(err)
}

compiler := NewCompiler(false, "", "test")

// Compile the workflow
err = compiler.CompileWorkflow(testFile)
if err != nil {
t.Fatalf("Unexpected error compiling workflow with output pull-request draft: false: %v", err)
}

// Read the generated lock file
lockFile := strings.TrimSuffix(testFile, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("Failed to read generated lock file: %v", err)
}

// Convert to string for easier testing
lockContentStr := string(lockContent)

// Verify create_pull_request job is present
if !strings.Contains(lockContentStr, "create_pull_request:") {
t.Error("Expected 'create_pull_request' job to be in generated workflow")
}

// Verify draft setting is false
if !strings.Contains(lockContentStr, "GITHUB_AW_PR_DRAFT: \"false\"") {
t.Error("Expected draft to be set to false when explicitly specified")
}

// Verify other expected environment variables are still present
if !strings.Contains(lockContentStr, "GITHUB_AW_PR_TITLE_PREFIX: \"[agent] \"") {
t.Error("Expected title prefix to be set as environment variable")
}

if !strings.Contains(lockContentStr, "GITHUB_AW_PR_LABELS: \"automation\"") {
t.Error("Expected automation label to be set as environment variable")
}

t.Logf("Generated workflow content:\n%s", lockContentStr)
}

func TestOutputPullRequestDraftTrue(t *testing.T) {
// Create temporary directory for test files
tmpDir, err := os.MkdirTemp("", "output-pr-draft-true-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)

// Test case with output.pull-request configuration with draft: true
testContent := `---
on: push
permissions:
contents: read
pull-requests: write
tools:
github:
allowed: [list_issues]
engine: claude
output:
pull-request:
title-prefix: "[agent] "
labels: [automation]
draft: true
---

# Test Output Pull Request with Draft True

This workflow tests the create_pull_request job generation with draft: true.
`

testFile := filepath.Join(tmpDir, "test-output-pr-draft-true.md")
if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil {
t.Fatal(err)
}

compiler := NewCompiler(false, "", "test")

// Compile the workflow
err = compiler.CompileWorkflow(testFile)
if err != nil {
t.Fatalf("Unexpected error compiling workflow with output pull-request draft: true: %v", err)
}

// Read the generated lock file
lockFile := strings.TrimSuffix(testFile, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("Failed to read generated lock file: %v", err)
}

// Convert to string for easier testing
lockContentStr := string(lockContent)

// Verify create_pull_request job is present
if !strings.Contains(lockContentStr, "create_pull_request:") {
t.Error("Expected 'create_pull_request' job to be in generated workflow")
}

// Verify draft setting is true
if !strings.Contains(lockContentStr, "GITHUB_AW_PR_DRAFT: \"true\"") {
t.Error("Expected draft to be set to true when explicitly specified")
}

// Verify other expected environment variables are still present
if !strings.Contains(lockContentStr, "GITHUB_AW_PR_TITLE_PREFIX: \"[agent] \"") {
t.Error("Expected title prefix to be set as environment variable")
}

if !strings.Contains(lockContentStr, "GITHUB_AW_PR_LABELS: \"automation\"") {
t.Error("Expected automation label to be set as environment variable")
}

t.Logf("Generated workflow content:\n%s", lockContentStr)
}