diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 334d0b7954e..828732d7e61 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -138,6 +138,11 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, + "docker/metadata-action@v6": { + "repo": "docker/metadata-action", + "version": "v6", + "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" + }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 345e02e8eff..d9aaa5d6eba 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6a6bd39e2339b2b176862a0fceb3dc70c2440fe79b8689f4e476134a57bed33a","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"030e881283bb7a6894de51c315a6bfe6a94e05cf","version":"v6.0.0 (source v6)"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9","version":"v6"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -49,7 +49,7 @@ # - anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 # - docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 # - docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 -# - docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) +# - docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 # - docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 (source v4) # # Container images used: @@ -1541,7 +1541,7 @@ jobs: username: ${{ github.actor }} - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 with: images: ghcr.io/${{ github.repository }} tags: | diff --git a/pkg/workflow/bot_aliases.go b/pkg/workflow/bot_aliases.go new file mode 100644 index 00000000000..b05e207d77e --- /dev/null +++ b/pkg/workflow/bot_aliases.go @@ -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) +} diff --git a/pkg/workflow/bot_aliases_test.go b/pkg/workflow/bot_aliases_test.go new file mode 100644 index 00000000000..c58950ee061 --- /dev/null +++ b/pkg/workflow/bot_aliases_test.go @@ -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) + }) + } +} diff --git a/pkg/workflow/bots_test.go b/pkg/workflow/bots_test.go index d2350b6296b..7c4d586e91e 100644 --- a/pkg/workflow/bots_test.go +++ b/pkg/workflow/bots_test.go @@ -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) { diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 4ba0001db02..4262dcd3589 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -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)