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
5 changes: 5 additions & 0 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@
"version": "v4.1.0",
"sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121"
},
"docker/metadata-action@v6": {
"repo": "docker/metadata-action",
"version": "v6",
"sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9"
},
Comment on lines +141 to +145
"docker/metadata-action@v6.0.0": {
"repo": "docker/metadata-action",
"version": "v6.0.0",
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.lock.yml

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

53 changes: 53 additions & 0 deletions pkg/workflow/bot_aliases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package workflow

import "github.com/github/gh-aw/pkg/sliceutil"

// copilotBotNames is the list of bot identifiers that the "copilot" alias expands to.
// It covers the known GitHub Copilot bot identities:
// - "copilot-swe-agent" — the Copilot Coding Agent (actor: copilot-swe-agent[bot])
// - "Copilot" — the @Copilot interactive bot (actor: Copilot)
// - "copilot" — the base copilot form (actor: copilot[bot])
var copilotBotNames = []string{
"copilot-swe-agent",
"Copilot",
"copilot",
}

// expandBotNames expands the "copilot" shorthand alias in a list of bot names to the
// full set of GitHub Copilot bot identifiers. Other entries are passed through
// unchanged. Duplicates are removed from the result.
//
// A nil or empty input slice is returned as-is. The nil/empty distinction is
// preserved so callers can distinguish "no bots configured" (nil) from "bots
// field present but empty" ([]string{}).
//
// The "copilot" alias covers:
// - copilot-swe-agent / copilot-swe-agent[bot] — Copilot Coding Agent
// - Copilot — @Copilot interactive bot
// - copilot / copilot[bot] — base copilot bot form
func expandBotNames(bots []string) []string {
if len(bots) == 0 {
return bots
}
needsExpansion := false
for _, b := range bots {
if b == "copilot" {
needsExpansion = true
break
}
}
if !needsExpansion {
return bots
}
// Pre-allocate with the worst-case capacity: every entry is a "copilot"
// alias that expands to len(copilotBotNames) entries.
expanded := make([]string, 0, len(bots)*len(copilotBotNames))
for _, b := range bots {
if b == "copilot" {
expanded = append(expanded, copilotBotNames...)
} else {
expanded = append(expanded, b)
}
}
return sliceutil.Deduplicate(expanded)
}
67 changes: 67 additions & 0 deletions pkg/workflow/bot_aliases_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//go:build !integration

package workflow

import (
"testing"

"github.com/stretchr/testify/assert"
)

// TestExpandBotNames verifies that "copilot" is expanded to the full set of
// GitHub Copilot bot identifiers and that other bot names pass through unchanged.
func TestExpandBotNames(t *testing.T) {
tests := []struct {
name string
input []string
expected []string
}{
{
name: "empty list",
input: []string{},
expected: []string{},
},
{
name: "nil list",
input: nil,
expected: nil,
},
{
name: "copilot alias expands to all copilot bot names",
input: []string{"copilot"},
expected: []string{"copilot-swe-agent", "Copilot", "copilot"},
},
{
name: "non-copilot bots pass through unchanged",
input: []string{"dependabot[bot]", "renovate[bot]"},
expected: []string{"dependabot[bot]", "renovate[bot]"},
},
{
name: "copilot mixed with other bots deduplicates",
input: []string{"dependabot[bot]", "copilot", "renovate[bot]"},
expected: []string{"dependabot[bot]", "copilot-swe-agent", "Copilot", "copilot", "renovate[bot]"},
},
{
name: "copilot-swe-agent explicit does not double-expand",
input: []string{"copilot", "copilot-swe-agent"},
expected: []string{"copilot-swe-agent", "Copilot", "copilot"},
},
{
name: "Copilot explicit does not double-expand",
input: []string{"copilot", "Copilot"},
expected: []string{"copilot-swe-agent", "Copilot", "copilot"},
},
{
name: "no copilot alias — list unchanged",
input: []string{"github-actions[bot]"},
expected: []string{"github-actions[bot]"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := expandBotNames(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
33 changes: 33 additions & 0 deletions pkg/workflow/bots_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,39 @@ Test workflow content.`
}
}

// TestCopilotBotAliasExpansion tests that "copilot" in the bots list is expanded to all
// known GitHub Copilot bot identifiers in the compiled output.
func TestCopilotBotAliasExpansion(t *testing.T) {
tmpDir := testutil.TempDir(t, "workflow-copilot-bot-alias-test")

compiler := NewCompiler()

frontmatter := `---
on:
pull_request:
types: [opened]
bots: ["copilot"]
---

# Test Workflow with Copilot Alias
Test workflow content.`

workflowPath := filepath.Join(tmpDir, "workflow-copilot-bot.md")
err := os.WriteFile(workflowPath, []byte(frontmatter), 0644)
require.NoError(t, err, "Failed to write workflow file")

err = compiler.CompileWorkflow(workflowPath)
require.NoError(t, err, "Compilation failed")

lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(workflowPath))
require.NoError(t, err, "Failed to read lock file")
lockStr := string(lockContent)

// The "copilot" alias must be expanded to all three Copilot bot identities
assert.Contains(t, lockStr, `GH_AW_ALLOWED_BOTS: "copilot-swe-agent,Copilot,copilot"`,
`Expected compiled workflow to expand "copilot" alias to all Copilot bot identifiers`)
}

// TestBotsImportMerge tests that bots from imported workflows are merged with top-level bots
// in the compiled output (regression test for the fix in compiler_orchestrator_workflow.go).
func TestBotsImportMerge(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,11 @@ func (c *Compiler) extractAdditionalConfigurations(
}

workflowData.Roles = c.extractRoles(frontmatter)
workflowData.Bots = c.mergeBots(c.extractBots(frontmatter), importsResult.MergedBots)
workflowData.Bots = expandBotNames(c.mergeBots(c.extractBots(frontmatter), importsResult.MergedBots))
workflowData.LabelNames = c.extractLabelNames(frontmatter)
workflowData.RateLimit = c.extractRateLimitConfig(frontmatter)
workflowData.SkipRoles = c.mergeSkipRoles(c.extractSkipRoles(frontmatter), importsResult.MergedSkipRoles)
workflowData.SkipBots = c.mergeSkipBots(c.extractSkipBots(frontmatter), importsResult.MergedSkipBots)
workflowData.SkipBots = expandBotNames(c.mergeSkipBots(c.extractSkipBots(frontmatter), importsResult.MergedSkipBots))
workflowData.SkipAuthorAssociations = c.extractSkipAuthorAssociations(frontmatter)
workflowData.AllowBotAuthoredTriggerComment = c.extractAllowBotAuthoredTriggerComment(frontmatter)
workflowData.ActivationGitHubToken = c.resolveActivationGitHubToken(frontmatter, importsResult)
Expand Down