detached 117121a

docs(executor): say why the token permissions group is omitted when permissions is set, and test it

commit
117121aed01aded74f784a9e323a3bb8bf5ee258
author
brice · · 2026-10-12 23:13 UTC
parent
e0a17ad
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +19 -6
M internal/executor/job.go +17 -6
@@ -586,12 +586,23 @@ const (
586586 var defaultTokenPermissions = [][2]string{{"Contents", "read"}, {"Metadata", "read"}, {"Packages", "read"}}
587587
588588 // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source
589-// and the cache mode. A workflow or job that sets "permissions" gets no group:
590-// the effective permissions depend on what the server grants, which the runner
591-// cannot know (it would print the default, which is wrong for such a workflow;
592-// GitHub's runner prints nothing when the server sends none, JobExtension.cs:
593-// 153). To print the real ones, runner.proto's Job would need the effective
594-// GITHUB_TOKEN permissions (scope and level, in display order).
589+// and the cache mode.
590+//
591+// Without a "permissions" key the group is the recorded default. With one
592+// (including {} and read-all/write-all) the group is omitted, on purpose: the
593+// runner only prints what the server gives it (system.github.token.permissions,
594+// a JSON object, JobExtension.cs:154-166), and the sources at hand do not say
595+// what the server gives for those cases: the display names of the scopes
596+// ("Pull requests" or "PullRequests", "Id-token" or "IdToken"), their order, the
597+// levels the server grants on a push (Metadata is always read; write-all and
598+// id-token depend on the repository's policy, the Permissions class of the
599+// runner only models the workflow side, Sdk/WorkflowParser/Permissions.cs and
600+// Conversion/PermissionsHelper.cs) and whether "none" scopes are listed. Only
601+// the default is in the recordings (221 of 221 jobs; no corpus scenario sets
602+// permissions). A group made up from the workflow would be a guess; add the
603+// mapping once a recording of a scenario with permissions exists. The
604+// complete fix is the server sending the effective permissions in the Job
605+// message.
595606 func (j *job) printTokenInfo() {
596607 if j.wf.Permissions == nil && j.jb.Permissions == nil {
597608 j.out(1, "##[group]GITHUB_TOKEN Permissions")
@@ -586,12 +586,23 @@ const (
586 var defaultTokenPermissions = [][2]string{{"Contents", "read"}, {"Metadata", "read"}, {"Packages", "read"}}586 var defaultTokenPermissions = [][2]string{{"Contents", "read"}, {"Metadata", "read"}, {"Packages", "read"}}
587 587
588 // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source588 // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source
589-// and the cache mode. A workflow or job that sets "permissions" gets no group:589+// and the cache mode.
590-// the effective permissions depend on what the server grants, which the runner590+//
591-// cannot know (it would print the default, which is wrong for such a workflow;591+// Without a "permissions" key the group is the recorded default. With one
592-// GitHub's runner prints nothing when the server sends none, JobExtension.cs:592+// (including {} and read-all/write-all) the group is omitted, on purpose: the
593-// 153). To print the real ones, runner.proto's Job would need the effective593+// runner only prints what the server gives it (system.github.token.permissions,
594-// GITHUB_TOKEN permissions (scope and level, in display order).594+// a JSON object, JobExtension.cs:154-166), and the sources at hand do not say
595+// what the server gives for those cases: the display names of the scopes
596+// ("Pull requests" or "PullRequests", "Id-token" or "IdToken"), their order, the
597+// levels the server grants on a push (Metadata is always read; write-all and
598+// id-token depend on the repository's policy, the Permissions class of the
599+// runner only models the workflow side, Sdk/WorkflowParser/Permissions.cs and
600+// Conversion/PermissionsHelper.cs) and whether "none" scopes are listed. Only
601+// the default is in the recordings (221 of 221 jobs; no corpus scenario sets
602+// permissions). A group made up from the workflow would be a guess; add the
603+// mapping once a recording of a scenario with permissions exists. The
604+// complete fix is the server sending the effective permissions in the Job
605+// message.
595 func (j *job) printTokenInfo() {606 func (j *job) printTokenInfo() {
596 if j.wf.Permissions == nil && j.jb.Permissions == nil {607 if j.wf.Permissions == nil && j.jb.Permissions == nil {
597 j.out(1, "##[group]GITHUB_TOKEN Permissions")608 j.out(1, "##[group]GITHUB_TOKEN Permissions")
M internal/executor/job_test.go +2 -0
@@ -703,6 +703,8 @@ func TestSetUpJobWithPermissionsPrintsNoDefaultGroup(t *testing.T) {
703703 for name, yaml := range map[string]string{
704704 "workflow": "permissions:\n contents: write\n" + wfHead + " steps:\n - run: echo hi\n",
705705 "job": wfHead + " permissions: read-all\n steps:\n - run: echo hi\n",
706+ "empty": "permissions: {}\n" + wfHead + " steps:\n - run: echo hi\n",
707+ "write": "permissions: write-all\n" + wfHead + " steps:\n - run: echo hi\n",
706708 } {
707709 got := setupLog(t, yaml, "j", nil)
708710 if strings.Contains(got, "GITHUB_TOKEN Permissions") || !strings.Contains(got, "Secret source: Actions\nCache mode: write") {
@@ -703,6 +703,8 @@ func TestSetUpJobWithPermissionsPrintsNoDefaultGroup(t *testing.T) {
703 for name, yaml := range map[string]string{703 for name, yaml := range map[string]string{
704 "workflow": "permissions:\n contents: write\n" + wfHead + " steps:\n - run: echo hi\n",704 "workflow": "permissions:\n contents: write\n" + wfHead + " steps:\n - run: echo hi\n",
705 "job": wfHead + " permissions: read-all\n steps:\n - run: echo hi\n",705 "job": wfHead + " permissions: read-all\n steps:\n - run: echo hi\n",
706+ "empty": "permissions: {}\n" + wfHead + " steps:\n - run: echo hi\n",
707+ "write": "permissions: write-all\n" + wfHead + " steps:\n - run: echo hi\n",
706 } {708 } {
707 got := setupLog(t, yaml, "j", nil)709 got := setupLog(t, yaml, "j", nil)
708 if strings.Contains(got, "GITHUB_TOKEN Permissions") || !strings.Contains(got, "Secret source: Actions\nCache mode: write") {710 if strings.Contains(got, "GITHUB_TOKEN Permissions") || !strings.Contains(got, "Secret source: Actions\nCache mode: write") {

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