v0.1.0 cbb8ad6

token_permissions.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"sort"
7
8	"bnema.dev/ermite-actions/workflow"
9)
10
11// tokenScope is a scope of the GITHUB_TOKEN as GitHub's runner lists it.
12type tokenScope struct {
13	// key is the name in a workflow's "permissions" mapping.
14	key string
15	// display is the name "Set up job" prints.
16	display string
17	// writeAll is the level "write-all" grants: GitHub keeps models and
18	// vulnerability-alerts at read, whatever the workflow asks.
19	writeAll string
20	// readAll is the level "read-all" grants, "" when it grants nothing.
21	readAll string
22}
23
24// tokenScopes are the scopes GitHub lists in "GITHUB_TOKEN Permissions" for
25// "read-all" and "write-all" (recordings/permissions-display.json: p-read-all,
26// p-write-all). "id-token" is not here: it is never listed, with any level
27// (p-job-mixed asks for id-token: write). "copilot-requests" is listed for
28// write-all only (read-all grants nothing for it, and "read" is rejected).
29// "metadata" is not a scope a workflow can set (rejected); it is always read.
30var tokenScopes = []tokenScope{
31	{"actions", "Actions", "write", "read"},
32	{"artifact-metadata", "ArtifactMetadata", "write", "read"},
33	{"attestations", "Attestations", "write", "read"},
34	{"checks", "Checks", "write", "read"},
35	{"code-quality", "CodeQuality", "write", "read"},
36	{"contents", "Contents", "write", "read"},
37	{"copilot-requests", "CopilotRequests", "write", ""},
38	{"deployments", "Deployments", "write", "read"},
39	{"discussions", "Discussions", "write", "read"},
40	{"drives", "Drives", "write", "read"},
41	{"issues", "Issues", "write", "read"},
42	{"models", "Models", "read", "read"},
43	{"packages", "Packages", "write", "read"},
44	{"pages", "Pages", "write", "read"},
45	{"pull-requests", "PullRequests", "write", "read"},
46	{"repository-projects", "RepositoryProjects", "write", "read"},
47	{"security-events", "SecurityEvents", "write", "read"},
48	{"statuses", "Statuses", "write", "read"},
49	{"vulnerability-alerts", "VulnerabilityAlerts", "read", "read"},
50}
51
52// permissionLine is one line of the group: "<Display>: <level>".
53type permissionLine struct{ name, level string }
54
55// tokenPermissionLines returns the lines of the "GITHUB_TOKEN Permissions"
56// group for the effective permissions of a job, as GitHub prints them on a
57// push by a repository owner (the recorded case):
58//
59//   - the permissions of the job replace those of the workflow (no merge);
60//     without either key the repository default applies;
61//   - lines are sorted by display name (ordinal), Metadata included;
62//   - Metadata is always listed, as read, whatever the workflow says;
63//   - a scope set to "none" or left out is not listed, nor is id-token;
64//   - "permissions: {}" lists Metadata only;
65//   - "read-all" lists every scope but copilot-requests as read;
66//   - "write-all" lists every scope as write except models and
67//     vulnerability-alerts (read);
68//   - a mapping lists its scopes with the given level; a scope that GitHub
69//     does not know is left out (it rejects the workflow before this point
70//     for the invalid ones it was shown).
71//
72// Not recorded: a private repository, a fork pull request, an organisation
73// whose default token is restricted differently, a "pull_request_target"
74// event. They change what the server grants; the workflow side is the same.
75func tokenPermissionLines(wf *workflow.Workflow, jb *workflow.Job) []permissionLine {
76	p := jb.Permissions
77	if p == nil {
78		p = wf.Permissions
79	}
80	if p == nil {
81		return []permissionLine{{"Contents", "read"}, {"Metadata", "read"}, {"Packages", "read"}}
82	}
83	lines := []permissionLine{{"Metadata", "read"}}
84	switch p.All {
85	case "read-all":
86		for _, s := range tokenScopes {
87			if s.readAll != "" {
88				lines = append(lines, permissionLine{s.display, s.readAll})
89			}
90		}
91	case "write-all":
92		for _, s := range tokenScopes {
93			lines = append(lines, permissionLine{s.display, s.writeAll})
94		}
95	default:
96		level := map[string]string{}
97		for _, sc := range p.Scopes {
98			level[sc.Scope] = sc.Level
99		}
100		for _, s := range tokenScopes {
101			if l := level[s.key]; l == "read" || l == "write" {
102				lines = append(lines, permissionLine{s.display, l})
103			}
104		}
105	}
106	sort.Slice(lines, func(a, b int) bool { return lines[a].name < lines[b].name })
107	return lines
108}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close