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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +19 -6
M internal/executor/job.go +17 -6
| @@ -586,12 +586,23 @@ const ( | |||
| 586 | 586 | var defaultTokenPermissions = [][2]string{{"Contents", "read"}, {"Metadata", "read"}, {"Packages", "read"}} | |
| 587 | 587 | ||
| 588 | 588 | // 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. | |
| 595 | 606 | func (j *job) printTokenInfo() { | |
| 596 | 607 | if j.wf.Permissions == nil && j.jb.Permissions == nil { | |
| 597 | 608 | 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 source | 588 | // 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 runner | 590 | + | // |
| 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 effective | 593 | + | // 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) { | |||
| 703 | 703 | for name, yaml := range map[string]string{ | |
| 704 | 704 | "workflow": "permissions:\n contents: write\n" + wfHead + " steps:\n - run: echo hi\n", | |
| 705 | 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 | 709 | got := setupLog(t, yaml, "j", nil) | |
| 708 | 710 | 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") { | ||