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
8 changes: 4 additions & 4 deletions actions/setup/js/push_signed_commits.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,10 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c
if (err instanceof PushSignedCommitsUnsupportedShape) {
throw new Error(
`pushSignedCommits: refusing unsigned push for branch '${branch}': ${err.message}. ` +
`GitHub's createCommitOnBranch GraphQL mutation cannot represent merge commits, symlinks (mode 120000), ` +
`submodule entries (mode 160000), or executable bits (mode 100755). ` +
`Rewrite the commits to use only regular files (mode 100644) with no merge commits, ` +
`or set push-signed-commits: false if the repository does not require signed commits.`,
`GitHub's createCommitOnBranch GraphQL mutation cannot represent merge commits, symlinks (mode 120000), ` +
`submodule entries (mode 160000), or executable bits (mode 100755). ` +
`Rewrite the commits to use only regular files (mode 100644) with no merge commits, ` +
`or set push-signed-commits: false if the repository does not require signed commits.`,
{ cause: err }
);
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/workflow/github_app_permissions_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,17 @@ func TestGetAllGitHubAppOnlyScopes(t *testing.T) {
}
}

func TestConvertStringToPermissionScope_RoundTrip(t *testing.T) {
allScopes := append(GetAllPermissionScopes(), GetAllGitHubAppOnlyScopes()...)
for _, scope := range allScopes {
key := string(scope)
got := convertStringToPermissionScope(key)
if got != scope {
t.Errorf("convertStringToPermissionScope(%q) = %q, want %q", key, got, scope)
}
}
}

Comment on lines +311 to +313
func TestGitHubAppOnlyPermissionsRenderToYAML(t *testing.T) {
tests := []struct {
name string
Expand Down
128 changes: 25 additions & 103 deletions pkg/workflow/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,116 +8,38 @@ import (

var permissionsLog = logger.New("workflow:permissions")

var validPermissionScopes = func() map[string]struct{} {
scopes := GetAllPermissionScopes()
appOnlyScopes := GetAllGitHubAppOnlyScopes()

m := make(map[string]struct{}, len(scopes)+len(appOnlyScopes))
for _, scope := range scopes {
m[string(scope)] = struct{}{}
}
for _, scope := range appOnlyScopes {
m[string(scope)] = struct{}{}
}
// copilot-requests is intentionally excluded from GetAllPermissionScopes()
// because read-all should not grant this write-capable scope, but it must still
// be recognized when explicitly set in frontmatter.
m[string(PermissionCopilotRequests)] = struct{}{}

return m
}()

// convertStringToPermissionScope converts a string key to a PermissionScope
func convertStringToPermissionScope(key string) PermissionScope {
switch key {
// GitHub Actions permission scopes (supported by GITHUB_TOKEN)
case "actions":
return PermissionActions
case "attestations":
return PermissionAttestations
case "checks":
return PermissionChecks
case "contents":
return PermissionContents
case "deployments":
return PermissionDeployments
case "discussions":
return PermissionDiscussions
case "id-token":
return PermissionIdToken
case "issues":
return PermissionIssues
case "metadata":
return PermissionMetadata
case "models":
return PermissionModels
case "packages":
return PermissionPackages
case "pages":
return PermissionPages
case "pull-requests":
return PermissionPullRequests
case "repository-projects":
return PermissionRepositoryProj
case "security-events":
return PermissionSecurityEvents
case "statuses":
return PermissionStatuses
case "copilot-requests":
return PermissionCopilotRequests
case "vulnerability-alerts":
return PermissionVulnerabilityAlerts
// GitHub App-only permission scopes (not supported by GITHUB_TOKEN, require a GitHub App)
// organization-projects is included here because it is a GitHub App-only scope
// (it is excluded from GetAllPermissionScopes() and skipped in YAML rendering).
case "organization-projects":
return PermissionOrganizationProj
case "administration":
return PermissionAdministration
case "members":
return PermissionMembers
case "organization-administration":
return PermissionOrganizationAdministration
case "environments":
return PermissionEnvironments
case "git-signing":
return PermissionGitSigning
case "team-discussions":
return PermissionTeamDiscussions
case "workflows":
return PermissionWorkflows
case "repository-hooks":
return PermissionRepositoryHooks
case "organization-hooks":
return PermissionOrganizationHooks
case "organization-members":
return PermissionOrganizationMembers
case "organization-packages":
return PermissionOrganizationPackages
case "organization-self-hosted-runners":
return PermissionOrganizationSelfHostedRunners
case "single-file":
return PermissionSingleFile
case "codespaces":
return PermissionCodespaces
case "email-addresses":
return PermissionEmailAddresses
case "repository-custom-properties":
return PermissionRepositoryCustomProperties
case "organization-custom-org-roles":
return PermissionOrganizationCustomOrgRoles
case "organization-custom-properties":
return PermissionOrganizationCustomProperties
case "organization-custom-repository-roles":
return PermissionOrganizationCustomRepositoryRoles
case "organization-announcement-banners":
return PermissionOrganizationAnnouncementBanners
case "organization-events":
return PermissionOrganizationEvents
case "organization-plan":
return PermissionOrganizationPlan
case "organization-user-blocking":
return PermissionOrganizationUserBlocking
case "organization-personal-access-token-requests":
return PermissionOrganizationPersonalAccessTokenReqs
case "organization-personal-access-tokens":
return PermissionOrganizationPersonalAccessTokens
case "organization-copilot":
return PermissionOrganizationCopilot
case "organization-codespaces":
return PermissionOrganizationCodespaces
case "codespaces-lifecycle-admin":
return PermissionCodespacesLifecycleAdmin
case "codespaces-metadata":
return PermissionCodespacesMetadata
case "all":
if key == "all" {
// "all" is a meta-key handled at the parser level; it is not a real scope
return ""
default:
}

if _, exists := validPermissionScopes[key]; !exists {
permissionsLog.Printf("Unknown permission scope key: %s", key)
return ""
}

return PermissionScope(key)
}

// PermissionLevel represents the level of access (read, write, none)
Expand Down