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
12 changes: 10 additions & 2 deletions docs/src/content/docs/reference/github-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Some key toolsets are:
- `labels` (labels management)

:::note
`toolsets: [all]` does **not** include the `dependabot` toolset. Because `dependabot` requires the `vulnerability-alerts` GitHub App-only permission (not grantable via `GITHUB_TOKEN`), it must be opted into explicitly:
`toolsets: [all]` does **not** include the `dependabot` toolset. The `dependabot` toolset must be opted into explicitly:

```yaml wrap
tools:
Expand Down Expand Up @@ -167,7 +167,15 @@ gh aw secrets set GH_AW_GITHUB_MCP_SERVER_TOKEN --value "<your-pat-token>"

### Using the `dependabot` toolset

The `dependabot` toolset can only be used if authenticating with a PAT or GitHub App and also requires the `vulnerability-alerts` GitHub App permission. If you are using a GitHub App (rather than a PAT), add `vulnerability-alerts: read` to your workflow's `permissions:` field and ensure the GitHub App is configured with this permission. See [GitHub App-Only Permissions](/gh-aw/reference/permissions/#github-app-only-permissions).
The `dependabot` toolset requires the `vulnerability-alerts: read` and `security-events: read` permissions. These are now supported natively by `GITHUB_TOKEN`. Add them to your workflow's `permissions:` field:

```yaml
permissions:
vulnerability-alerts: read
security-events: read
```

Alternatively, you can authenticate with a PAT or GitHub App. If using a GitHub App, add `vulnerability-alerts: read` to your workflow's `permissions:` field and ensure the GitHub App is configured with this permission.

## Related Documentation

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ See [GitHub's permissions reference](https://docs.github.com/en/actions/using-jo

Certain permission scopes require [additional authentication](/gh-aw/reference/github-tools/#additional-authentication-for-github-tools). These include:

**Repository-level:** `administration`, `environments`, `git-signing`, `vulnerability-alerts`, `workflows`, `repository-hooks`, `single-file`, `codespaces`, `repository-custom-properties`
**Repository-level:** `administration`, `environments`, `git-signing`, `workflows`, `repository-hooks`, `single-file`, `codespaces`, `repository-custom-properties`

**Organization-level:** `organization-projects`, `members`, `organization-administration`, `team-discussions`, `organization-hooks`, `organization-members`, `organization-packages`, `organization-self-hosted-runners`, `organization-custom-org-roles`, `organization-custom-properties`, `organization-custom-repository-roles`, `organization-announcement-banners`, `organization-events`, `organization-plan`, `organization-user-blocking`, `organization-personal-access-token-requests`, `organization-personal-access-tokens`, `organization-copilot`, `organization-codespaces`

Expand Down
44 changes: 22 additions & 22 deletions pkg/cli/compile_permissions_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import (
"github.com/stretchr/testify/require"
)

// TestCompileVulnerabilityAlertsPermissionFiltered compiles the canonical
// TestCompileVulnerabilityAlertsPermissionIncluded compiles the canonical
// test-vulnerability-alerts-permission.md workflow file and verifies that
// the GitHub App-only `vulnerability-alerts` scope does NOT appear in any
// job-level permissions block of the compiled lock file.
// the `vulnerability-alerts` scope appears correctly in the compiled lock file.
//
// GitHub Actions rejects workflows that declare App-only permissions at the
// job level (e.g. vulnerability-alerts, members, administration). These scopes
// must only appear as `permission-*` inputs to actions/create-github-app-token.
func TestCompileVulnerabilityAlertsPermissionFiltered(t *testing.T) {
// Since vulnerability-alerts is now a native GITHUB_TOKEN permission scope, it
// SHOULD appear in job-level permissions blocks. It is also forwarded as a
// `permission-vulnerability-alerts` input to actions/create-github-app-token
// when a GitHub App is configured.
func TestCompileVulnerabilityAlertsPermissionIncluded(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

Expand Down Expand Up @@ -51,16 +51,17 @@ func TestCompileVulnerabilityAlertsPermissionFiltered(t *testing.T) {
assert.Contains(t, lockContentStr, "id: github-mcp-app-token",
"GitHub App token minting step should be generated")

// Critically: vulnerability-alerts must NOT appear inside any job-level permissions block.
// Parse the YAML and walk every job's permissions map to check.
// vulnerability-alerts is now a valid GITHUB_TOKEN scope and SHOULD appear in
// job-level permissions blocks.
var workflow map[string]any
require.NoError(t, goyaml.Unmarshal(lockContent, &workflow),
"Lock file should be valid YAML")

jobs, ok := workflow["jobs"].(map[string]any)
require.True(t, ok, "Lock file should have a jobs section")

for jobName, jobConfig := range jobs {
foundVulnAlerts := false
for _, jobConfig := range jobs {
jobMap, ok := jobConfig.(map[string]any)
if !ok {
continue
Expand All @@ -71,22 +72,21 @@ func TestCompileVulnerabilityAlertsPermissionFiltered(t *testing.T) {
}
permsMap, ok := perms.(map[string]any)
if !ok {
// Shorthand permissions (read-all / write-all / none) — nothing to check
continue
}
assert.NotContains(t, permsMap, "vulnerability-alerts",
"Job %q must not have vulnerability-alerts in job-level permissions block "+
"(it is a GitHub App-only scope and not a valid GitHub Actions permission)", jobName)
if _, found := permsMap["vulnerability-alerts"]; found {
foundVulnAlerts = true
}
}
assert.True(t, foundVulnAlerts,
"vulnerability-alerts should appear in at least one job-level permissions block (it is a GITHUB_TOKEN scope)")

// Extra belt-and-suspenders: the string "vulnerability-alerts: read" must not appear
// anywhere other than inside the App token step inputs.
// We verify by counting occurrences: exactly one occurrence for the App token step.
// vulnerability-alerts: read should appear in both job-level permissions
// and in the App token step inputs (permission-vulnerability-alerts: read).
occurrences := strings.Count(lockContentStr, "vulnerability-alerts: read")
// The permission-vulnerability-alerts: read line contains "vulnerability-alerts: read"
// as a substring, so we count that and only that occurrence.
appTokenOccurrences := strings.Count(lockContentStr, "permission-vulnerability-alerts: read")
assert.Equal(t, appTokenOccurrences, occurrences,
"vulnerability-alerts: read should appear only inside the App token step inputs, not elsewhere in the lock file\nLock file:\n%s",
lockContentStr)
assert.GreaterOrEqual(t, occurrences, appTokenOccurrences,
"vulnerability-alerts: read should appear at least as often as permission-vulnerability-alerts: read")
assert.Greater(t, occurrences, 0,
"vulnerability-alerts: read should appear at least once in the lock file")
}
6 changes: 3 additions & 3 deletions pkg/cli/workflows/test-vulnerability-alerts-permission.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ tools:
# Vulnerability Alerts with GitHub App Token

This workflow uses the Dependabot toolset with a GitHub App token.
The `vulnerability-alerts: read` permission is a GitHub App-only scope and must NOT
appear in the compiled job-level `permissions:` block. It should only appear as a
`permission-vulnerability-alerts: read` input to the `create-github-app-token` step.
The `vulnerability-alerts: read` permission is now a native GITHUB_TOKEN scope
and will appear in the compiled job-level `permissions:` block. It is also forwarded
as a `permission-vulnerability-alerts: read` input to the `create-github-app-token` step.
6 changes: 3 additions & 3 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10380,7 +10380,7 @@
"vulnerability-alerts": {
"type": "string",
"enum": ["read", "write", "none"],
"description": "Permission level for Dependabot vulnerability alerts (read/write/none). GitHub App-only permission: required to access Dependabot alerts via the GitHub MCP server. The GITHUB_TOKEN does not have this permission \u2014 a GitHub App must be configured."
"description": "Permission level for Dependabot vulnerability alerts (read/write/none). Allows workflows to access the Dependabot alerts API via GITHUB_TOKEN instead of requiring a PAT or GitHub App."
},
"all": {
"type": "string",
Expand Down Expand Up @@ -10536,8 +10536,8 @@
},
"vulnerability-alerts": {
"type": "string",
"enum": ["read", "none", "write"],
"description": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler). GitHub App-only permission."
"enum": ["read", "none"],
"description": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler). Also available as a GITHUB_TOKEN scope. When used with a GitHub App, forwarded as permission-vulnerability-alerts input."
},
Comment thread
pelikhan marked this conversation as resolved.
"workflows": {
"type": "string",
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_main_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) (
// In dev/script mode, automatically add contents: read if the actions folder checkout is needed
// In release mode, use the permissions as specified by the user (no automatic augmentation)
//
// GitHub App-only permissions (e.g., vulnerability-alerts) must be filtered out before
// GitHub App-only permissions (e.g., members, administration) must be filtered out before
// rendering to the job-level permissions block. These scopes are not valid GitHub Actions
// workflow permissions and cause a parse error when queued. They are handled separately
// when minting GitHub App installation access tokens (as permission-* inputs).
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/dangerous_permissions_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestFindWritePermissions(t *testing.T) {
{
name: "write-all shorthand",
permissions: NewPermissionsWriteAll(),
expectedWriteCount: 14, // All GitHub Actions permission scopes except id-token and metadata (which are excluded)
expectedWriteCount: 15, // All GitHub Actions permission scopes except id-token and metadata (which are excluded)
expectedScopes: nil, // Don't check specific scopes for shorthand
},
{
Expand Down
4 changes: 2 additions & 2 deletions pkg/workflow/frontmatter_parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, err
config.SecurityEvents = levelStr
case "statuses":
config.Statuses = levelStr
case "vulnerability-alerts":
config.VulnerabilityAlerts = levelStr
case "organization-projects":
config.OrganizationProjects = levelStr
// GitHub App-only permission scopes
Expand All @@ -213,8 +215,6 @@ func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, err
config.Environments = levelStr
case "git-signing":
config.GitSigning = levelStr
case "vulnerability-alerts":
config.VulnerabilityAlerts = levelStr
case "workflows":
config.Workflows = levelStr
case "repository-hooks":
Expand Down
6 changes: 3 additions & 3 deletions pkg/workflow/frontmatter_serialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ func permissionsConfigToMap(config *PermissionsConfig) map[string]any {
if config.Statuses != "" {
result["statuses"] = config.Statuses
}
if config.VulnerabilityAlerts != "" {
result["vulnerability-alerts"] = config.VulnerabilityAlerts
}
if config.OrganizationProjects != "" {
result["organization-projects"] = config.OrganizationProjects
}
Expand All @@ -350,9 +353,6 @@ func permissionsConfigToMap(config *PermissionsConfig) map[string]any {
if config.GitSigning != "" {
result["git-signing"] = config.GitSigning
}
if config.VulnerabilityAlerts != "" {
result["vulnerability-alerts"] = config.VulnerabilityAlerts
}
if config.Workflows != "" {
result["workflows"] = config.Workflows
}
Expand Down
28 changes: 14 additions & 14 deletions pkg/workflow/frontmatter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,20 @@ type RuntimesConfig struct {
// These scopes can be declared in the workflow's top-level permissions block and are enforced
// natively by GitHub Actions.
type GitHubActionsPermissionsConfig struct {
Actions string `json:"actions,omitempty"`
Checks string `json:"checks,omitempty"`
Contents string `json:"contents,omitempty"`
Deployments string `json:"deployments,omitempty"`
IDToken string `json:"id-token,omitempty"`
Issues string `json:"issues,omitempty"`
Discussions string `json:"discussions,omitempty"`
Packages string `json:"packages,omitempty"`
Pages string `json:"pages,omitempty"`
PullRequests string `json:"pull-requests,omitempty"`
RepositoryProjects string `json:"repository-projects,omitempty"`
SecurityEvents string `json:"security-events,omitempty"`
Statuses string `json:"statuses,omitempty"`
Actions string `json:"actions,omitempty"`
Checks string `json:"checks,omitempty"`
Contents string `json:"contents,omitempty"`
Deployments string `json:"deployments,omitempty"`
IDToken string `json:"id-token,omitempty"`
Issues string `json:"issues,omitempty"`
Discussions string `json:"discussions,omitempty"`
Packages string `json:"packages,omitempty"`
Pages string `json:"pages,omitempty"`
PullRequests string `json:"pull-requests,omitempty"`
RepositoryProjects string `json:"repository-projects,omitempty"`
SecurityEvents string `json:"security-events,omitempty"`
Statuses string `json:"statuses,omitempty"`
VulnerabilityAlerts string `json:"vulnerability-alerts,omitempty"`
}

// GitHubAppPermissionsConfig holds permission scopes that are exclusive to GitHub App
Expand Down Expand Up @@ -78,7 +79,6 @@ type GitHubAppPermissionsConfig struct {
Administration string `json:"administration,omitempty"`
Environments string `json:"environments,omitempty"`
GitSigning string `json:"git-signing,omitempty"`
VulnerabilityAlerts string `json:"vulnerability-alerts,omitempty"`
Workflows string `json:"workflows,omitempty"`
RepositoryHooks string `json:"repository-hooks,omitempty"`
SingleFile string `json:"single-file,omitempty"`
Expand Down
10 changes: 4 additions & 6 deletions pkg/workflow/github_app_permissions_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,9 @@ func TestValidateGitHubAppOnlyPermissions(t *testing.T) {
errorContains: "workflows",
},
{
name: "vulnerability-alerts permission without github-app - should error",
permissions: "permissions:\n vulnerability-alerts: read",
shouldError: true,
errorContains: "vulnerability-alerts",
name: "vulnerability-alerts permission without github-app - should pass (GITHUB_TOKEN scope)",
permissions: "permissions:\n vulnerability-alerts: read",
shouldError: false,
},
{
name: "mixed Actions and App-only permissions with github-app - should pass",
Expand Down Expand Up @@ -228,6 +227,7 @@ func TestIsGitHubAppOnlyScope(t *testing.T) {
{PermissionSecurityEvents, false},
{PermissionStatuses, false},
{PermissionDiscussions, false},
{PermissionVulnerabilityAlerts, false},
// organization-projects is a GitHub App-only scope (not in GitHub Actions GITHUB_TOKEN)
{PermissionOrganizationProj, true},
// GitHub App-only scopes - should return true
Expand All @@ -237,7 +237,6 @@ func TestIsGitHubAppOnlyScope(t *testing.T) {
{PermissionEnvironments, true},
{PermissionGitSigning, true},
{PermissionTeamDiscussions, true},
{PermissionVulnerabilityAlerts, true},
{PermissionWorkflows, true},
{PermissionRepositoryHooks, true},
{PermissionOrganizationHooks, true},
Expand Down Expand Up @@ -272,7 +271,6 @@ func TestGetAllGitHubAppOnlyScopes(t *testing.T) {
PermissionOrganizationAdministration,
PermissionEnvironments,
PermissionWorkflows,
PermissionVulnerabilityAlerts,
PermissionOrganizationPackages,
}

Expand Down
15 changes: 8 additions & 7 deletions pkg/workflow/github_mcp_app_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,14 @@ Test that permission-vulnerability-alerts is emitted in the App token minting st
assert.Contains(t, lockContent, "permission-security-events: read", "Should also include security-events read permission in App token")
// Verify the token minting step is present
assert.Contains(t, lockContent, "id: github-mcp-app-token", "GitHub App token step should be generated")
// Verify that vulnerability-alerts does NOT appear in any job-level permissions block.
// It is a GitHub App-only permission and not a valid GitHub Actions workflow permission;
// GitHub Actions rejects workflows that declare it at the job level.
// Verify that vulnerability-alerts DOES appear in job-level permissions block.
// It is now a valid GITHUB_TOKEN permission scope.
var workflow map[string]any
require.NoError(t, goyaml.Unmarshal(content, &workflow), "Lock file should be valid YAML")
jobs, ok := workflow["jobs"].(map[string]any)
require.True(t, ok, "Should have jobs section")
for jobName, jobConfig := range jobs {
foundVulnAlerts := false
for _, jobConfig := range jobs {
jobMap, ok := jobConfig.(map[string]any)
if !ok {
continue
Expand All @@ -388,9 +388,10 @@ Test that permission-vulnerability-alerts is emitted in the App token minting st
continue
}
if _, found := permsMap["vulnerability-alerts"]; found {
t.Errorf("Job %q should not have vulnerability-alerts in job-level permissions block (it is a GitHub App-only permission)", jobName)
foundVulnAlerts = true
}
}
assert.True(t, foundVulnAlerts, "vulnerability-alerts should appear in at least one job-level permissions block (it is a GITHUB_TOKEN scope)")
}

// TestGitHubMCPAppTokenWithExtraPermissions tests that extra permissions under
Expand Down Expand Up @@ -450,7 +451,7 @@ Test extra org-level permissions in GitHub App token.
}

// TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel tests that extra permissions
// under tools.github.github-app.permissions can suppress a GitHub App-only scope
// under tools.github.github-app.permissions can suppress a scope
// that was set at job level by overriding it with 'none' (nested wins).
func TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel(t *testing.T) {
compiler := NewCompiler(WithVersion("1.0.0"))
Expand All @@ -474,7 +475,7 @@ tools:

# Test Workflow

Test that nested permissions override job-level GitHub App-only scopes (nested wins).
Test that nested permissions override job-level scopes (nested wins).
`

tmpDir := t.TempDir()
Expand Down
3 changes: 1 addition & 2 deletions pkg/workflow/github_toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ var DefaultGitHubToolsets = []string{"context", "repos", "issues", "pull_request
var ActionFriendlyGitHubToolsets = []string{"context", "repos", "issues", "pull_requests"}

// GitHubToolsetsExcludedFromAll defines toolsets that are NOT included when "all" is specified.
// These toolsets require GitHub App-only permissions (e.g., vulnerability-alerts) that
// cannot be granted via GITHUB_TOKEN, so they must be opted-in to explicitly.
// These toolsets are opt-in only to avoid granting unnecessary permissions by default.
var GitHubToolsetsExcludedFromAll = []string{"dependabot"}

// ParseGitHubToolsets parses the toolsets string and expands "default" and "all"
Expand Down
Loading
Loading