detached 9c50523

feat(executor): print server-side token permissions (#26)

commit
9c50523b2cfc843e1f78eb02c98252b727b0ccaf
author
brice · · 2026-10-14 23:30 UTC
committer
bnema · · 2026-10-14 23:30 UTC
parent
c936897
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
9 files · +414 -103
M internal/execstream/exec.proto +12 -0
@@ -107,6 +107,18 @@ message JobSpec {
107107 // secrets are not available. Never set for servers without the feature, for
108108 // which an absent Job.trusted only means "not sent".
109109 bool secrets_withheld = 19;
110+ // token_permissions are the effective permissions of the job's GITHUB_TOKEN
111+ // as the server sent them (Job.permissions, feature ci.secrets): "Set up job"
112+ // prints them instead of working them out from the workflow. Unset when the
113+ // server sent none (an older server): the executor then reads them from the
114+ // workflow.
115+ TokenPermissions token_permissions = 20;
116+}
117+
118+// TokenPermissions maps a permission key ("contents", "pull-requests", ...) to
119+// "read", "write" or "none"; a key that is absent is none.
120+message TokenPermissions {
121+ map<string, string> levels = 1;
110122 }
111123
112124 enum StreamKind {
@@ -107,6 +107,18 @@ message JobSpec {
107 // secrets are not available. Never set for servers without the feature, for107 // secrets are not available. Never set for servers without the feature, for
108 // which an absent Job.trusted only means "not sent".108 // which an absent Job.trusted only means "not sent".
109 bool secrets_withheld = 19;109 bool secrets_withheld = 19;
110+ // token_permissions are the effective permissions of the job's GITHUB_TOKEN
111+ // as the server sent them (Job.permissions, feature ci.secrets): "Set up job"
112+ // prints them instead of working them out from the workflow. Unset when the
113+ // server sent none (an older server): the executor then reads them from the
114+ // workflow.
115+ TokenPermissions token_permissions = 20;
116+}
117+
118+// TokenPermissions maps a permission key ("contents", "pull-requests", ...) to
119+// "read", "write" or "none"; a key that is absent is none.
120+message TokenPermissions {
121+ map<string, string> levels = 1;
110 }122 }
111 123
112 enum StreamKind {124 enum StreamKind {
M internal/execstream/execpb/exec.pb.go +164 -95
@@ -756,8 +756,14 @@ type JobSpec struct {
756756 // secrets are not available. Never set for servers without the feature, for
757757 // which an absent Job.trusted only means "not sent".
758758 SecretsWithheld bool `protobuf:"varint,19,opt,name=secrets_withheld,json=secretsWithheld,proto3" json:"secrets_withheld,omitempty"`
759- unknownFields protoimpl.UnknownFields
760- sizeCache protoimpl.SizeCache
759+ // token_permissions are the effective permissions of the job's GITHUB_TOKEN
760+ // as the server sent them (Job.permissions, feature ci.secrets): "Set up job"
761+ // prints them instead of working them out from the workflow. Unset when the
762+ // server sent none (an older server): the executor then reads them from the
763+ // workflow.
764+ TokenPermissions *TokenPermissions `protobuf:"bytes,20,opt,name=token_permissions,json=tokenPermissions,proto3" json:"token_permissions,omitempty"`
765+ unknownFields protoimpl.UnknownFields
766+ sizeCache protoimpl.SizeCache
761767 }
762768
763769 func (x *JobSpec) Reset() {
@@ -909,6 +915,59 @@ func (x *JobSpec) GetSecretsWithheld() bool {
909915 return false
910916 }
911917
918+func (x *JobSpec) GetTokenPermissions() *TokenPermissions {
919+ if x != nil {
920+ return x.TokenPermissions
921+ }
922+ return nil
923+}
924+
925+// TokenPermissions maps a permission key ("contents", "pull-requests", ...) to
926+// "read", "write" or "none"; a key that is absent is none.
927+type TokenPermissions struct {
928+ state protoimpl.MessageState `protogen:"open.v1"`
929+ Levels map[string]string `protobuf:"bytes,1,rep,name=levels,proto3" json:"levels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
930+ unknownFields protoimpl.UnknownFields
931+ sizeCache protoimpl.SizeCache
932+}
933+
934+func (x *TokenPermissions) Reset() {
935+ *x = TokenPermissions{}
936+ mi := &file_exec_proto_msgTypes[5]
937+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
938+ ms.StoreMessageInfo(mi)
939+}
940+
941+func (x *TokenPermissions) String() string {
942+ return protoimpl.X.MessageStringOf(x)
943+}
944+
945+func (*TokenPermissions) ProtoMessage() {}
946+
947+func (x *TokenPermissions) ProtoReflect() protoreflect.Message {
948+ mi := &file_exec_proto_msgTypes[5]
949+ if x != nil {
950+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
951+ if ms.LoadMessageInfo() == nil {
952+ ms.StoreMessageInfo(mi)
953+ }
954+ return ms
955+ }
956+ return mi.MessageOf(x)
957+}
958+
959+// Deprecated: Use TokenPermissions.ProtoReflect.Descriptor instead.
960+func (*TokenPermissions) Descriptor() ([]byte, []int) {
961+ return file_exec_proto_rawDescGZIP(), []int{5}
962+}
963+
964+func (x *TokenPermissions) GetLevels() map[string]string {
965+ if x != nil {
966+ return x.Levels
967+ }
968+ return nil
969+}
970+
912971 type Output struct {
913972 state protoimpl.MessageState `protogen:"open.v1"`
914973 Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`
@@ -920,7 +979,7 @@ type Output struct {
920979
921980 func (x *Output) Reset() {
922981 *x = Output{}
923- mi := &file_exec_proto_msgTypes[5]
982+ mi := &file_exec_proto_msgTypes[6]
924983 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
925984 ms.StoreMessageInfo(mi)
926985 }
@@ -932,7 +991,7 @@ func (x *Output) String() string {
932991 func (*Output) ProtoMessage() {}
933992
934993 func (x *Output) ProtoReflect() protoreflect.Message {
935- mi := &file_exec_proto_msgTypes[5]
994+ mi := &file_exec_proto_msgTypes[6]
936995 if x != nil {
937996 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
938997 if ms.LoadMessageInfo() == nil {
@@ -945,7 +1004,7 @@ func (x *Output) ProtoReflect() protoreflect.Message {
9451004
9461005 // Deprecated: Use Output.ProtoReflect.Descriptor instead.
9471006 func (*Output) Descriptor() ([]byte, []int) {
948- return file_exec_proto_rawDescGZIP(), []int{5}
1007+ return file_exec_proto_rawDescGZIP(), []int{6}
9491008 }
9501009
9511010 func (x *Output) GetStream() StreamKind {
@@ -979,7 +1038,7 @@ type Exit struct {
9791038
9801039 func (x *Exit) Reset() {
9811040 *x = Exit{}
982- mi := &file_exec_proto_msgTypes[6]
1041+ mi := &file_exec_proto_msgTypes[7]
9831042 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9841043 ms.StoreMessageInfo(mi)
9851044 }
@@ -991,7 +1050,7 @@ func (x *Exit) String() string {
9911050 func (*Exit) ProtoMessage() {}
9921051
9931052 func (x *Exit) ProtoReflect() protoreflect.Message {
994- mi := &file_exec_proto_msgTypes[6]
1053+ mi := &file_exec_proto_msgTypes[7]
9951054 if x != nil {
9961055 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9971056 if ms.LoadMessageInfo() == nil {
@@ -1004,7 +1063,7 @@ func (x *Exit) ProtoReflect() protoreflect.Message {
10041063
10051064 // Deprecated: Use Exit.ProtoReflect.Descriptor instead.
10061065 func (*Exit) Descriptor() ([]byte, []int) {
1007- return file_exec_proto_rawDescGZIP(), []int{6}
1066+ return file_exec_proto_rawDescGZIP(), []int{7}
10081067 }
10091068
10101069 func (x *Exit) GetCode() int32 {
@@ -1038,7 +1097,7 @@ type Cancel struct {
10381097
10391098 func (x *Cancel) Reset() {
10401099 *x = Cancel{}
1041- mi := &file_exec_proto_msgTypes[7]
1100+ mi := &file_exec_proto_msgTypes[8]
10421101 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10431102 ms.StoreMessageInfo(mi)
10441103 }
@@ -1050,7 +1109,7 @@ func (x *Cancel) String() string {
10501109 func (*Cancel) ProtoMessage() {}
10511110
10521111 func (x *Cancel) ProtoReflect() protoreflect.Message {
1053- mi := &file_exec_proto_msgTypes[7]
1112+ mi := &file_exec_proto_msgTypes[8]
10541113 if x != nil {
10551114 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10561115 if ms.LoadMessageInfo() == nil {
@@ -1063,7 +1122,7 @@ func (x *Cancel) ProtoReflect() protoreflect.Message {
10631122
10641123 // Deprecated: Use Cancel.ProtoReflect.Descriptor instead.
10651124 func (*Cancel) Descriptor() ([]byte, []int) {
1066- return file_exec_proto_rawDescGZIP(), []int{7}
1125+ return file_exec_proto_rawDescGZIP(), []int{8}
10671126 }
10681127
10691128 func (x *Cancel) GetReason() string {
@@ -1093,7 +1152,7 @@ type StepStarted struct {
10931152
10941153 func (x *StepStarted) Reset() {
10951154 *x = StepStarted{}
1096- mi := &file_exec_proto_msgTypes[8]
1155+ mi := &file_exec_proto_msgTypes[9]
10971156 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10981157 ms.StoreMessageInfo(mi)
10991158 }
@@ -1105,7 +1164,7 @@ func (x *StepStarted) String() string {
11051164 func (*StepStarted) ProtoMessage() {}
11061165
11071166 func (x *StepStarted) ProtoReflect() protoreflect.Message {
1108- mi := &file_exec_proto_msgTypes[8]
1167+ mi := &file_exec_proto_msgTypes[9]
11091168 if x != nil {
11101169 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11111170 if ms.LoadMessageInfo() == nil {
@@ -1118,7 +1177,7 @@ func (x *StepStarted) ProtoReflect() protoreflect.Message {
11181177
11191178 // Deprecated: Use StepStarted.ProtoReflect.Descriptor instead.
11201179 func (*StepStarted) Descriptor() ([]byte, []int) {
1121- return file_exec_proto_rawDescGZIP(), []int{8}
1180+ return file_exec_proto_rawDescGZIP(), []int{9}
11221181 }
11231182
11241183 func (x *StepStarted) GetNumber() int32 {
@@ -1158,7 +1217,7 @@ type StepCompleted struct {
11581217
11591218 func (x *StepCompleted) Reset() {
11601219 *x = StepCompleted{}
1161- mi := &file_exec_proto_msgTypes[9]
1220+ mi := &file_exec_proto_msgTypes[10]
11621221 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11631222 ms.StoreMessageInfo(mi)
11641223 }
@@ -1170,7 +1229,7 @@ func (x *StepCompleted) String() string {
11701229 func (*StepCompleted) ProtoMessage() {}
11711230
11721231 func (x *StepCompleted) ProtoReflect() protoreflect.Message {
1173- mi := &file_exec_proto_msgTypes[9]
1232+ mi := &file_exec_proto_msgTypes[10]
11741233 if x != nil {
11751234 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11761235 if ms.LoadMessageInfo() == nil {
@@ -1183,7 +1242,7 @@ func (x *StepCompleted) ProtoReflect() protoreflect.Message {
11831242
11841243 // Deprecated: Use StepCompleted.ProtoReflect.Descriptor instead.
11851244 func (*StepCompleted) Descriptor() ([]byte, []int) {
1186- return file_exec_proto_rawDescGZIP(), []int{9}
1245+ return file_exec_proto_rawDescGZIP(), []int{10}
11871246 }
11881247
11891248 func (x *StepCompleted) GetNumber() int32 {
@@ -1239,7 +1298,7 @@ type Annotation struct {
12391298
12401299 func (x *Annotation) Reset() {
12411300 *x = Annotation{}
1242- mi := &file_exec_proto_msgTypes[10]
1301+ mi := &file_exec_proto_msgTypes[11]
12431302 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12441303 ms.StoreMessageInfo(mi)
12451304 }
@@ -1251,7 +1310,7 @@ func (x *Annotation) String() string {
12511310 func (*Annotation) ProtoMessage() {}
12521311
12531312 func (x *Annotation) ProtoReflect() protoreflect.Message {
1254- mi := &file_exec_proto_msgTypes[10]
1313+ mi := &file_exec_proto_msgTypes[11]
12551314 if x != nil {
12561315 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12571316 if ms.LoadMessageInfo() == nil {
@@ -1264,7 +1323,7 @@ func (x *Annotation) ProtoReflect() protoreflect.Message {
12641323
12651324 // Deprecated: Use Annotation.ProtoReflect.Descriptor instead.
12661325 func (*Annotation) Descriptor() ([]byte, []int) {
1267- return file_exec_proto_rawDescGZIP(), []int{10}
1326+ return file_exec_proto_rawDescGZIP(), []int{11}
12681327 }
12691328
12701329 func (x *Annotation) GetLevel() AnnotationLevel {
@@ -1337,7 +1396,7 @@ type StepAnnotations struct {
13371396
13381397 func (x *StepAnnotations) Reset() {
13391398 *x = StepAnnotations{}
1340- mi := &file_exec_proto_msgTypes[11]
1399+ mi := &file_exec_proto_msgTypes[12]
13411400 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13421401 ms.StoreMessageInfo(mi)
13431402 }
@@ -1349,7 +1408,7 @@ func (x *StepAnnotations) String() string {
13491408 func (*StepAnnotations) ProtoMessage() {}
13501409
13511410 func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
1352- mi := &file_exec_proto_msgTypes[11]
1411+ mi := &file_exec_proto_msgTypes[12]
13531412 if x != nil {
13541413 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13551414 if ms.LoadMessageInfo() == nil {
@@ -1362,7 +1421,7 @@ func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
13621421
13631422 // Deprecated: Use StepAnnotations.ProtoReflect.Descriptor instead.
13641423 func (*StepAnnotations) Descriptor() ([]byte, []int) {
1365- return file_exec_proto_rawDescGZIP(), []int{11}
1424+ return file_exec_proto_rawDescGZIP(), []int{12}
13661425 }
13671426
13681427 func (x *StepAnnotations) GetStep() int32 {
@@ -1391,7 +1450,7 @@ type StepSummary struct {
13911450
13921451 func (x *StepSummary) Reset() {
13931452 *x = StepSummary{}
1394- mi := &file_exec_proto_msgTypes[12]
1453+ mi := &file_exec_proto_msgTypes[13]
13951454 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13961455 ms.StoreMessageInfo(mi)
13971456 }
@@ -1403,7 +1462,7 @@ func (x *StepSummary) String() string {
14031462 func (*StepSummary) ProtoMessage() {}
14041463
14051464 func (x *StepSummary) ProtoReflect() protoreflect.Message {
1406- mi := &file_exec_proto_msgTypes[12]
1465+ mi := &file_exec_proto_msgTypes[13]
14071466 if x != nil {
14081467 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14091468 if ms.LoadMessageInfo() == nil {
@@ -1416,7 +1475,7 @@ func (x *StepSummary) ProtoReflect() protoreflect.Message {
14161475
14171476 // Deprecated: Use StepSummary.ProtoReflect.Descriptor instead.
14181477 func (*StepSummary) Descriptor() ([]byte, []int) {
1419- return file_exec_proto_rawDescGZIP(), []int{12}
1478+ return file_exec_proto_rawDescGZIP(), []int{13}
14201479 }
14211480
14221481 func (x *StepSummary) GetStep() int32 {
@@ -1448,7 +1507,7 @@ type Log struct {
14481507
14491508 func (x *Log) Reset() {
14501509 *x = Log{}
1451- mi := &file_exec_proto_msgTypes[13]
1510+ mi := &file_exec_proto_msgTypes[14]
14521511 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14531512 ms.StoreMessageInfo(mi)
14541513 }
@@ -1460,7 +1519,7 @@ func (x *Log) String() string {
14601519 func (*Log) ProtoMessage() {}
14611520
14621521 func (x *Log) ProtoReflect() protoreflect.Message {
1463- mi := &file_exec_proto_msgTypes[13]
1522+ mi := &file_exec_proto_msgTypes[14]
14641523 if x != nil {
14651524 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14661525 if ms.LoadMessageInfo() == nil {
@@ -1473,7 +1532,7 @@ func (x *Log) ProtoReflect() protoreflect.Message {
14731532
14741533 // Deprecated: Use Log.ProtoReflect.Descriptor instead.
14751534 func (*Log) Descriptor() ([]byte, []int) {
1476- return file_exec_proto_rawDescGZIP(), []int{13}
1535+ return file_exec_proto_rawDescGZIP(), []int{14}
14771536 }
14781537
14791538 func (x *Log) GetStep() int32 {
@@ -1517,7 +1576,7 @@ type JobResult struct {
15171576
15181577 func (x *JobResult) Reset() {
15191578 *x = JobResult{}
1520- mi := &file_exec_proto_msgTypes[14]
1579+ mi := &file_exec_proto_msgTypes[15]
15211580 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15221581 ms.StoreMessageInfo(mi)
15231582 }
@@ -1529,7 +1588,7 @@ func (x *JobResult) String() string {
15291588 func (*JobResult) ProtoMessage() {}
15301589
15311590 func (x *JobResult) ProtoReflect() protoreflect.Message {
1532- mi := &file_exec_proto_msgTypes[14]
1591+ mi := &file_exec_proto_msgTypes[15]
15331592 if x != nil {
15341593 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15351594 if ms.LoadMessageInfo() == nil {
@@ -1542,7 +1601,7 @@ func (x *JobResult) ProtoReflect() protoreflect.Message {
15421601
15431602 // Deprecated: Use JobResult.ProtoReflect.Descriptor instead.
15441603 func (*JobResult) Descriptor() ([]byte, []int) {
1545- return file_exec_proto_rawDescGZIP(), []int{14}
1604+ return file_exec_proto_rawDescGZIP(), []int{15}
15461605 }
15471606
15481607 func (x *JobResult) GetConclusion() StepConclusion {
@@ -1587,7 +1646,7 @@ type ResolveRequest struct {
15871646
15881647 func (x *ResolveRequest) Reset() {
15891648 *x = ResolveRequest{}
1590- mi := &file_exec_proto_msgTypes[15]
1649+ mi := &file_exec_proto_msgTypes[16]
15911650 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15921651 ms.StoreMessageInfo(mi)
15931652 }
@@ -1599,7 +1658,7 @@ func (x *ResolveRequest) String() string {
15991658 func (*ResolveRequest) ProtoMessage() {}
16001659
16011660 func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
1602- mi := &file_exec_proto_msgTypes[15]
1661+ mi := &file_exec_proto_msgTypes[16]
16031662 if x != nil {
16041663 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16051664 if ms.LoadMessageInfo() == nil {
@@ -1612,7 +1671,7 @@ func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
16121671
16131672 // Deprecated: Use ResolveRequest.ProtoReflect.Descriptor instead.
16141673 func (*ResolveRequest) Descriptor() ([]byte, []int) {
1615- return file_exec_proto_rawDescGZIP(), []int{15}
1674+ return file_exec_proto_rawDescGZIP(), []int{16}
16161675 }
16171676
16181677 func (x *ResolveRequest) GetId() int64 {
@@ -1642,7 +1701,7 @@ type ResolveResponse struct {
16421701
16431702 func (x *ResolveResponse) Reset() {
16441703 *x = ResolveResponse{}
1645- mi := &file_exec_proto_msgTypes[16]
1704+ mi := &file_exec_proto_msgTypes[17]
16461705 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16471706 ms.StoreMessageInfo(mi)
16481707 }
@@ -1654,7 +1713,7 @@ func (x *ResolveResponse) String() string {
16541713 func (*ResolveResponse) ProtoMessage() {}
16551714
16561715 func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
1657- mi := &file_exec_proto_msgTypes[16]
1716+ mi := &file_exec_proto_msgTypes[17]
16581717 if x != nil {
16591718 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16601719 if ms.LoadMessageInfo() == nil {
@@ -1667,7 +1726,7 @@ func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
16671726
16681727 // Deprecated: Use ResolveResponse.ProtoReflect.Descriptor instead.
16691728 func (*ResolveResponse) Descriptor() ([]byte, []int) {
1670- return file_exec_proto_rawDescGZIP(), []int{16}
1729+ return file_exec_proto_rawDescGZIP(), []int{17}
16711730 }
16721731
16731732 func (x *ResolveResponse) GetId() int64 {
@@ -1714,7 +1773,7 @@ type ResolvedAction struct {
17141773
17151774 func (x *ResolvedAction) Reset() {
17161775 *x = ResolvedAction{}
1717- mi := &file_exec_proto_msgTypes[17]
1776+ mi := &file_exec_proto_msgTypes[18]
17181777 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
17191778 ms.StoreMessageInfo(mi)
17201779 }
@@ -1726,7 +1785,7 @@ func (x *ResolvedAction) String() string {
17261785 func (*ResolvedAction) ProtoMessage() {}
17271786
17281787 func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
1729- mi := &file_exec_proto_msgTypes[17]
1788+ mi := &file_exec_proto_msgTypes[18]
17301789 if x != nil {
17311790 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
17321791 if ms.LoadMessageInfo() == nil {
@@ -1739,7 +1798,7 @@ func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
17391798
17401799 // Deprecated: Use ResolvedAction.ProtoReflect.Descriptor instead.
17411800 func (*ResolvedAction) Descriptor() ([]byte, []int) {
1742- return file_exec_proto_rawDescGZIP(), []int{17}
1801+ return file_exec_proto_rawDescGZIP(), []int{18}
17431802 }
17441803
17451804 func (x *ResolvedAction) GetRef() string {
@@ -1844,7 +1903,7 @@ const file_exec_proto_rawDesc = "" +
18441903 "\x03job\x18\x03 \x01(\v2\x1e.ermite.runner.exec.v1.JobSpecR\x03job\x1a6\n" +
18451904 "\bEnvEntry\x12\x10\n" +
18461905 "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1847- "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe3\x04\n" +
1906+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb9\x05\n" +
18481907 "\aJobSpec\x12#\n" +
18491908 "\rworkflow_path\x18\x01 \x01(\tR\fworkflowPath\x12#\n" +
18501909 "\rworkflow_yaml\x18\x02 \x01(\tR\fworkflowYaml\x12\x17\n" +
@@ -1867,7 +1926,13 @@ const file_exec_proto_rawDesc = "" +
18671926 "cache_port\x18\x0f \x01(\rR\tcachePort\x12\x1f\n" +
18681927 "\vcache_token\x18\x10 \x01(\tR\n" +
18691928 "cacheToken\x12)\n" +
1870- "\x10secrets_withheld\x18\x13 \x01(\bR\x0fsecretsWithheldJ\x04\b\x11\x10\x12J\x04\b\x12\x10\x13R\asecretsR\x04vars\"t\n" +
1929+ "\x10secrets_withheld\x18\x13 \x01(\bR\x0fsecretsWithheld\x12T\n" +
1930+ "\x11token_permissions\x18\x14 \x01(\v2'.ermite.runner.exec.v1.TokenPermissionsR\x10tokenPermissionsJ\x04\b\x11\x10\x12J\x04\b\x12\x10\x13R\asecretsR\x04vars\"\x9a\x01\n" +
1931+ "\x10TokenPermissions\x12K\n" +
1932+ "\x06levels\x18\x01 \x03(\v23.ermite.runner.exec.v1.TokenPermissions.LevelsEntryR\x06levels\x1a9\n" +
1933+ "\vLevelsEntry\x12\x10\n" +
1934+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1935+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"t\n" +
18711936 "\x06Output\x129\n" +
18721937 "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +
18731938 "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +
@@ -1979,65 +2044,69 @@ func file_exec_proto_rawDescGZIP() []byte {
19792044 }
19802045
19812046 var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
1982-var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
2047+var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
19832048 var file_exec_proto_goTypes = []any{
1984- (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind
1985- (CancelKind)(0), // 1: ermite.runner.exec.v1.CancelKind
1986- (AnnotationLevel)(0), // 2: ermite.runner.exec.v1.AnnotationLevel
1987- (StepConclusion)(0), // 3: ermite.runner.exec.v1.StepConclusion
1988- (*Frame)(nil), // 4: ermite.runner.exec.v1.Frame
1989- (*NamedValue)(nil), // 5: ermite.runner.exec.v1.NamedValue
1990- (*Hello)(nil), // 6: ermite.runner.exec.v1.Hello
1991- (*Run)(nil), // 7: ermite.runner.exec.v1.Run
1992- (*JobSpec)(nil), // 8: ermite.runner.exec.v1.JobSpec
1993- (*Output)(nil), // 9: ermite.runner.exec.v1.Output
1994- (*Exit)(nil), // 10: ermite.runner.exec.v1.Exit
1995- (*Cancel)(nil), // 11: ermite.runner.exec.v1.Cancel
1996- (*StepStarted)(nil), // 12: ermite.runner.exec.v1.StepStarted
1997- (*StepCompleted)(nil), // 13: ermite.runner.exec.v1.StepCompleted
1998- (*Annotation)(nil), // 14: ermite.runner.exec.v1.Annotation
1999- (*StepAnnotations)(nil), // 15: ermite.runner.exec.v1.StepAnnotations
2000- (*StepSummary)(nil), // 16: ermite.runner.exec.v1.StepSummary
2001- (*Log)(nil), // 17: ermite.runner.exec.v1.Log
2002- (*JobResult)(nil), // 18: ermite.runner.exec.v1.JobResult
2003- (*ResolveRequest)(nil), // 19: ermite.runner.exec.v1.ResolveRequest
2004- (*ResolveResponse)(nil), // 20: ermite.runner.exec.v1.ResolveResponse
2005- (*ResolvedAction)(nil), // 21: ermite.runner.exec.v1.ResolvedAction
2006- nil, // 22: ermite.runner.exec.v1.Run.EnvEntry
2007- nil, // 23: ermite.runner.exec.v1.JobResult.OutputsEntry
2049+ (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind
2050+ (CancelKind)(0), // 1: ermite.runner.exec.v1.CancelKind
2051+ (AnnotationLevel)(0), // 2: ermite.runner.exec.v1.AnnotationLevel
2052+ (StepConclusion)(0), // 3: ermite.runner.exec.v1.StepConclusion
2053+ (*Frame)(nil), // 4: ermite.runner.exec.v1.Frame
2054+ (*NamedValue)(nil), // 5: ermite.runner.exec.v1.NamedValue
2055+ (*Hello)(nil), // 6: ermite.runner.exec.v1.Hello
2056+ (*Run)(nil), // 7: ermite.runner.exec.v1.Run
2057+ (*JobSpec)(nil), // 8: ermite.runner.exec.v1.JobSpec
2058+ (*TokenPermissions)(nil), // 9: ermite.runner.exec.v1.TokenPermissions
2059+ (*Output)(nil), // 10: ermite.runner.exec.v1.Output
2060+ (*Exit)(nil), // 11: ermite.runner.exec.v1.Exit
2061+ (*Cancel)(nil), // 12: ermite.runner.exec.v1.Cancel
2062+ (*StepStarted)(nil), // 13: ermite.runner.exec.v1.StepStarted
2063+ (*StepCompleted)(nil), // 14: ermite.runner.exec.v1.StepCompleted
2064+ (*Annotation)(nil), // 15: ermite.runner.exec.v1.Annotation
2065+ (*StepAnnotations)(nil), // 16: ermite.runner.exec.v1.StepAnnotations
2066+ (*StepSummary)(nil), // 17: ermite.runner.exec.v1.StepSummary
2067+ (*Log)(nil), // 18: ermite.runner.exec.v1.Log
2068+ (*JobResult)(nil), // 19: ermite.runner.exec.v1.JobResult
2069+ (*ResolveRequest)(nil), // 20: ermite.runner.exec.v1.ResolveRequest
2070+ (*ResolveResponse)(nil), // 21: ermite.runner.exec.v1.ResolveResponse
2071+ (*ResolvedAction)(nil), // 22: ermite.runner.exec.v1.ResolvedAction
2072+ nil, // 23: ermite.runner.exec.v1.Run.EnvEntry
2073+ nil, // 24: ermite.runner.exec.v1.TokenPermissions.LevelsEntry
2074+ nil, // 25: ermite.runner.exec.v1.JobResult.OutputsEntry
20082075 }
20092076 var file_exec_proto_depIdxs = []int32{
20102077 6, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello
20112078 7, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run
2012- 9, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output
2013- 10, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit
2014- 11, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel
2015- 12, // 5: ermite.runner.exec.v1.Frame.step_started:type_name -> ermite.runner.exec.v1.StepStarted
2016- 13, // 6: ermite.runner.exec.v1.Frame.step_completed:type_name -> ermite.runner.exec.v1.StepCompleted
2017- 17, // 7: ermite.runner.exec.v1.Frame.log:type_name -> ermite.runner.exec.v1.Log
2018- 18, // 8: ermite.runner.exec.v1.Frame.job_result:type_name -> ermite.runner.exec.v1.JobResult
2019- 15, // 9: ermite.runner.exec.v1.Frame.step_annotations:type_name -> ermite.runner.exec.v1.StepAnnotations
2020- 16, // 10: ermite.runner.exec.v1.Frame.step_summary:type_name -> ermite.runner.exec.v1.StepSummary
2021- 19, // 11: ermite.runner.exec.v1.Frame.resolve_request:type_name -> ermite.runner.exec.v1.ResolveRequest
2022- 20, // 12: ermite.runner.exec.v1.Frame.resolve_response:type_name -> ermite.runner.exec.v1.ResolveResponse
2079+ 10, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output
2080+ 11, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit
2081+ 12, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel
2082+ 13, // 5: ermite.runner.exec.v1.Frame.step_started:type_name -> ermite.runner.exec.v1.StepStarted
2083+ 14, // 6: ermite.runner.exec.v1.Frame.step_completed:type_name -> ermite.runner.exec.v1.StepCompleted
2084+ 18, // 7: ermite.runner.exec.v1.Frame.log:type_name -> ermite.runner.exec.v1.Log
2085+ 19, // 8: ermite.runner.exec.v1.Frame.job_result:type_name -> ermite.runner.exec.v1.JobResult
2086+ 16, // 9: ermite.runner.exec.v1.Frame.step_annotations:type_name -> ermite.runner.exec.v1.StepAnnotations
2087+ 17, // 10: ermite.runner.exec.v1.Frame.step_summary:type_name -> ermite.runner.exec.v1.StepSummary
2088+ 20, // 11: ermite.runner.exec.v1.Frame.resolve_request:type_name -> ermite.runner.exec.v1.ResolveRequest
2089+ 21, // 12: ermite.runner.exec.v1.Frame.resolve_response:type_name -> ermite.runner.exec.v1.ResolveResponse
20232090 5, // 13: ermite.runner.exec.v1.Frame.secret:type_name -> ermite.runner.exec.v1.NamedValue
20242091 5, // 14: ermite.runner.exec.v1.Frame.var:type_name -> ermite.runner.exec.v1.NamedValue
2025- 22, // 15: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry
2092+ 23, // 15: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry
20262093 8, // 16: ermite.runner.exec.v1.Run.job:type_name -> ermite.runner.exec.v1.JobSpec
2027- 0, // 17: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind
2028- 1, // 18: ermite.runner.exec.v1.Cancel.kind:type_name -> ermite.runner.exec.v1.CancelKind
2029- 3, // 19: ermite.runner.exec.v1.StepCompleted.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
2030- 3, // 20: ermite.runner.exec.v1.StepCompleted.outcome:type_name -> ermite.runner.exec.v1.StepConclusion
2031- 2, // 21: ermite.runner.exec.v1.Annotation.level:type_name -> ermite.runner.exec.v1.AnnotationLevel
2032- 14, // 22: ermite.runner.exec.v1.StepAnnotations.annotations:type_name -> ermite.runner.exec.v1.Annotation
2033- 3, // 23: ermite.runner.exec.v1.JobResult.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
2034- 23, // 24: ermite.runner.exec.v1.JobResult.outputs:type_name -> ermite.runner.exec.v1.JobResult.OutputsEntry
2035- 21, // 25: ermite.runner.exec.v1.ResolveResponse.actions:type_name -> ermite.runner.exec.v1.ResolvedAction
2036- 26, // [26:26] is the sub-list for method output_type
2037- 26, // [26:26] is the sub-list for method input_type
2038- 26, // [26:26] is the sub-list for extension type_name
2039- 26, // [26:26] is the sub-list for extension extendee
2040- 0, // [0:26] is the sub-list for field type_name
2094+ 9, // 17: ermite.runner.exec.v1.JobSpec.token_permissions:type_name -> ermite.runner.exec.v1.TokenPermissions
2095+ 24, // 18: ermite.runner.exec.v1.TokenPermissions.levels:type_name -> ermite.runner.exec.v1.TokenPermissions.LevelsEntry
2096+ 0, // 19: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind
2097+ 1, // 20: ermite.runner.exec.v1.Cancel.kind:type_name -> ermite.runner.exec.v1.CancelKind
2098+ 3, // 21: ermite.runner.exec.v1.StepCompleted.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
2099+ 3, // 22: ermite.runner.exec.v1.StepCompleted.outcome:type_name -> ermite.runner.exec.v1.StepConclusion
2100+ 2, // 23: ermite.runner.exec.v1.Annotation.level:type_name -> ermite.runner.exec.v1.AnnotationLevel
2101+ 15, // 24: ermite.runner.exec.v1.StepAnnotations.annotations:type_name -> ermite.runner.exec.v1.Annotation
2102+ 3, // 25: ermite.runner.exec.v1.JobResult.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
2103+ 25, // 26: ermite.runner.exec.v1.JobResult.outputs:type_name -> ermite.runner.exec.v1.JobResult.OutputsEntry
2104+ 22, // 27: ermite.runner.exec.v1.ResolveResponse.actions:type_name -> ermite.runner.exec.v1.ResolvedAction
2105+ 28, // [28:28] is the sub-list for method output_type
2106+ 28, // [28:28] is the sub-list for method input_type
2107+ 28, // [28:28] is the sub-list for extension type_name
2108+ 28, // [28:28] is the sub-list for extension extendee
2109+ 0, // [0:28] is the sub-list for field type_name
20412110 }
20422111
20432112 func init() { file_exec_proto_init() }
@@ -2068,7 +2137,7 @@ func file_exec_proto_init() {
20682137 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
20692138 RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),
20702139 NumEnums: 4,
2071- NumMessages: 20,
2140+ NumMessages: 22,
20722141 NumExtensions: 0,
20732142 NumServices: 0,
20742143 },
@@ -756,8 +756,14 @@ type JobSpec struct {
756 // secrets are not available. Never set for servers without the feature, for756 // secrets are not available. Never set for servers without the feature, for
757 // which an absent Job.trusted only means "not sent".757 // which an absent Job.trusted only means "not sent".
758 SecretsWithheld bool `protobuf:"varint,19,opt,name=secrets_withheld,json=secretsWithheld,proto3" json:"secrets_withheld,omitempty"`758 SecretsWithheld bool `protobuf:"varint,19,opt,name=secrets_withheld,json=secretsWithheld,proto3" json:"secrets_withheld,omitempty"`
759- unknownFields protoimpl.UnknownFields759+ // token_permissions are the effective permissions of the job's GITHUB_TOKEN
760- sizeCache protoimpl.SizeCache760+ // as the server sent them (Job.permissions, feature ci.secrets): "Set up job"
761+ // prints them instead of working them out from the workflow. Unset when the
762+ // server sent none (an older server): the executor then reads them from the
763+ // workflow.
764+ TokenPermissions *TokenPermissions `protobuf:"bytes,20,opt,name=token_permissions,json=tokenPermissions,proto3" json:"token_permissions,omitempty"`
765+ unknownFields protoimpl.UnknownFields
766+ sizeCache protoimpl.SizeCache
761 }767 }
762 768
763 func (x *JobSpec) Reset() {769 func (x *JobSpec) Reset() {
@@ -909,6 +915,59 @@ func (x *JobSpec) GetSecretsWithheld() bool {
909 return false915 return false
910 }916 }
911 917
918+func (x *JobSpec) GetTokenPermissions() *TokenPermissions {
919+ if x != nil {
920+ return x.TokenPermissions
921+ }
922+ return nil
923+}
924+
925+// TokenPermissions maps a permission key ("contents", "pull-requests", ...) to
926+// "read", "write" or "none"; a key that is absent is none.
927+type TokenPermissions struct {
928+ state protoimpl.MessageState `protogen:"open.v1"`
929+ Levels map[string]string `protobuf:"bytes,1,rep,name=levels,proto3" json:"levels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
930+ unknownFields protoimpl.UnknownFields
931+ sizeCache protoimpl.SizeCache
932+}
933+
934+func (x *TokenPermissions) Reset() {
935+ *x = TokenPermissions{}
936+ mi := &file_exec_proto_msgTypes[5]
937+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
938+ ms.StoreMessageInfo(mi)
939+}
940+
941+func (x *TokenPermissions) String() string {
942+ return protoimpl.X.MessageStringOf(x)
943+}
944+
945+func (*TokenPermissions) ProtoMessage() {}
946+
947+func (x *TokenPermissions) ProtoReflect() protoreflect.Message {
948+ mi := &file_exec_proto_msgTypes[5]
949+ if x != nil {
950+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
951+ if ms.LoadMessageInfo() == nil {
952+ ms.StoreMessageInfo(mi)
953+ }
954+ return ms
955+ }
956+ return mi.MessageOf(x)
957+}
958+
959+// Deprecated: Use TokenPermissions.ProtoReflect.Descriptor instead.
960+func (*TokenPermissions) Descriptor() ([]byte, []int) {
961+ return file_exec_proto_rawDescGZIP(), []int{5}
962+}
963+
964+func (x *TokenPermissions) GetLevels() map[string]string {
965+ if x != nil {
966+ return x.Levels
967+ }
968+ return nil
969+}
970+
912 type Output struct {971 type Output struct {
913 state protoimpl.MessageState `protogen:"open.v1"`972 state protoimpl.MessageState `protogen:"open.v1"`
914 Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`973 Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`
@@ -920,7 +979,7 @@ type Output struct {
920 979
921 func (x *Output) Reset() {980 func (x *Output) Reset() {
922 *x = Output{}981 *x = Output{}
923- mi := &file_exec_proto_msgTypes[5]982+ mi := &file_exec_proto_msgTypes[6]
924 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))983 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
925 ms.StoreMessageInfo(mi)984 ms.StoreMessageInfo(mi)
926 }985 }
@@ -932,7 +991,7 @@ func (x *Output) String() string {
932 func (*Output) ProtoMessage() {}991 func (*Output) ProtoMessage() {}
933 992
934 func (x *Output) ProtoReflect() protoreflect.Message {993 func (x *Output) ProtoReflect() protoreflect.Message {
935- mi := &file_exec_proto_msgTypes[5]994+ mi := &file_exec_proto_msgTypes[6]
936 if x != nil {995 if x != nil {
937 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))996 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
938 if ms.LoadMessageInfo() == nil {997 if ms.LoadMessageInfo() == nil {
@@ -945,7 +1004,7 @@ func (x *Output) ProtoReflect() protoreflect.Message {
945 1004
946 // Deprecated: Use Output.ProtoReflect.Descriptor instead.1005 // Deprecated: Use Output.ProtoReflect.Descriptor instead.
947 func (*Output) Descriptor() ([]byte, []int) {1006 func (*Output) Descriptor() ([]byte, []int) {
948- return file_exec_proto_rawDescGZIP(), []int{5}1007+ return file_exec_proto_rawDescGZIP(), []int{6}
949 }1008 }
950 1009
951 func (x *Output) GetStream() StreamKind {1010 func (x *Output) GetStream() StreamKind {
@@ -979,7 +1038,7 @@ type Exit struct {
979 1038
980 func (x *Exit) Reset() {1039 func (x *Exit) Reset() {
981 *x = Exit{}1040 *x = Exit{}
982- mi := &file_exec_proto_msgTypes[6]1041+ mi := &file_exec_proto_msgTypes[7]
983 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1042 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
984 ms.StoreMessageInfo(mi)1043 ms.StoreMessageInfo(mi)
985 }1044 }
@@ -991,7 +1050,7 @@ func (x *Exit) String() string {
991 func (*Exit) ProtoMessage() {}1050 func (*Exit) ProtoMessage() {}
992 1051
993 func (x *Exit) ProtoReflect() protoreflect.Message {1052 func (x *Exit) ProtoReflect() protoreflect.Message {
994- mi := &file_exec_proto_msgTypes[6]1053+ mi := &file_exec_proto_msgTypes[7]
995 if x != nil {1054 if x != nil {
996 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1055 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
997 if ms.LoadMessageInfo() == nil {1056 if ms.LoadMessageInfo() == nil {
@@ -1004,7 +1063,7 @@ func (x *Exit) ProtoReflect() protoreflect.Message {
1004 1063
1005 // Deprecated: Use Exit.ProtoReflect.Descriptor instead.1064 // Deprecated: Use Exit.ProtoReflect.Descriptor instead.
1006 func (*Exit) Descriptor() ([]byte, []int) {1065 func (*Exit) Descriptor() ([]byte, []int) {
1007- return file_exec_proto_rawDescGZIP(), []int{6}1066+ return file_exec_proto_rawDescGZIP(), []int{7}
1008 }1067 }
1009 1068
1010 func (x *Exit) GetCode() int32 {1069 func (x *Exit) GetCode() int32 {
@@ -1038,7 +1097,7 @@ type Cancel struct {
1038 1097
1039 func (x *Cancel) Reset() {1098 func (x *Cancel) Reset() {
1040 *x = Cancel{}1099 *x = Cancel{}
1041- mi := &file_exec_proto_msgTypes[7]1100+ mi := &file_exec_proto_msgTypes[8]
1042 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1101 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1043 ms.StoreMessageInfo(mi)1102 ms.StoreMessageInfo(mi)
1044 }1103 }
@@ -1050,7 +1109,7 @@ func (x *Cancel) String() string {
1050 func (*Cancel) ProtoMessage() {}1109 func (*Cancel) ProtoMessage() {}
1051 1110
1052 func (x *Cancel) ProtoReflect() protoreflect.Message {1111 func (x *Cancel) ProtoReflect() protoreflect.Message {
1053- mi := &file_exec_proto_msgTypes[7]1112+ mi := &file_exec_proto_msgTypes[8]
1054 if x != nil {1113 if x != nil {
1055 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1114 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1056 if ms.LoadMessageInfo() == nil {1115 if ms.LoadMessageInfo() == nil {
@@ -1063,7 +1122,7 @@ func (x *Cancel) ProtoReflect() protoreflect.Message {
1063 1122
1064 // Deprecated: Use Cancel.ProtoReflect.Descriptor instead.1123 // Deprecated: Use Cancel.ProtoReflect.Descriptor instead.
1065 func (*Cancel) Descriptor() ([]byte, []int) {1124 func (*Cancel) Descriptor() ([]byte, []int) {
1066- return file_exec_proto_rawDescGZIP(), []int{7}1125+ return file_exec_proto_rawDescGZIP(), []int{8}
1067 }1126 }
1068 1127
1069 func (x *Cancel) GetReason() string {1128 func (x *Cancel) GetReason() string {
@@ -1093,7 +1152,7 @@ type StepStarted struct {
1093 1152
1094 func (x *StepStarted) Reset() {1153 func (x *StepStarted) Reset() {
1095 *x = StepStarted{}1154 *x = StepStarted{}
1096- mi := &file_exec_proto_msgTypes[8]1155+ mi := &file_exec_proto_msgTypes[9]
1097 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1156 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1098 ms.StoreMessageInfo(mi)1157 ms.StoreMessageInfo(mi)
1099 }1158 }
@@ -1105,7 +1164,7 @@ func (x *StepStarted) String() string {
1105 func (*StepStarted) ProtoMessage() {}1164 func (*StepStarted) ProtoMessage() {}
1106 1165
1107 func (x *StepStarted) ProtoReflect() protoreflect.Message {1166 func (x *StepStarted) ProtoReflect() protoreflect.Message {
1108- mi := &file_exec_proto_msgTypes[8]1167+ mi := &file_exec_proto_msgTypes[9]
1109 if x != nil {1168 if x != nil {
1110 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1169 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1111 if ms.LoadMessageInfo() == nil {1170 if ms.LoadMessageInfo() == nil {
@@ -1118,7 +1177,7 @@ func (x *StepStarted) ProtoReflect() protoreflect.Message {
1118 1177
1119 // Deprecated: Use StepStarted.ProtoReflect.Descriptor instead.1178 // Deprecated: Use StepStarted.ProtoReflect.Descriptor instead.
1120 func (*StepStarted) Descriptor() ([]byte, []int) {1179 func (*StepStarted) Descriptor() ([]byte, []int) {
1121- return file_exec_proto_rawDescGZIP(), []int{8}1180+ return file_exec_proto_rawDescGZIP(), []int{9}
1122 }1181 }
1123 1182
1124 func (x *StepStarted) GetNumber() int32 {1183 func (x *StepStarted) GetNumber() int32 {
@@ -1158,7 +1217,7 @@ type StepCompleted struct {
1158 1217
1159 func (x *StepCompleted) Reset() {1218 func (x *StepCompleted) Reset() {
1160 *x = StepCompleted{}1219 *x = StepCompleted{}
1161- mi := &file_exec_proto_msgTypes[9]1220+ mi := &file_exec_proto_msgTypes[10]
1162 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1221 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1163 ms.StoreMessageInfo(mi)1222 ms.StoreMessageInfo(mi)
1164 }1223 }
@@ -1170,7 +1229,7 @@ func (x *StepCompleted) String() string {
1170 func (*StepCompleted) ProtoMessage() {}1229 func (*StepCompleted) ProtoMessage() {}
1171 1230
1172 func (x *StepCompleted) ProtoReflect() protoreflect.Message {1231 func (x *StepCompleted) ProtoReflect() protoreflect.Message {
1173- mi := &file_exec_proto_msgTypes[9]1232+ mi := &file_exec_proto_msgTypes[10]
1174 if x != nil {1233 if x != nil {
1175 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1234 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1176 if ms.LoadMessageInfo() == nil {1235 if ms.LoadMessageInfo() == nil {
@@ -1183,7 +1242,7 @@ func (x *StepCompleted) ProtoReflect() protoreflect.Message {
1183 1242
1184 // Deprecated: Use StepCompleted.ProtoReflect.Descriptor instead.1243 // Deprecated: Use StepCompleted.ProtoReflect.Descriptor instead.
1185 func (*StepCompleted) Descriptor() ([]byte, []int) {1244 func (*StepCompleted) Descriptor() ([]byte, []int) {
1186- return file_exec_proto_rawDescGZIP(), []int{9}1245+ return file_exec_proto_rawDescGZIP(), []int{10}
1187 }1246 }
1188 1247
1189 func (x *StepCompleted) GetNumber() int32 {1248 func (x *StepCompleted) GetNumber() int32 {
@@ -1239,7 +1298,7 @@ type Annotation struct {
1239 1298
1240 func (x *Annotation) Reset() {1299 func (x *Annotation) Reset() {
1241 *x = Annotation{}1300 *x = Annotation{}
1242- mi := &file_exec_proto_msgTypes[10]1301+ mi := &file_exec_proto_msgTypes[11]
1243 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1302 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1244 ms.StoreMessageInfo(mi)1303 ms.StoreMessageInfo(mi)
1245 }1304 }
@@ -1251,7 +1310,7 @@ func (x *Annotation) String() string {
1251 func (*Annotation) ProtoMessage() {}1310 func (*Annotation) ProtoMessage() {}
1252 1311
1253 func (x *Annotation) ProtoReflect() protoreflect.Message {1312 func (x *Annotation) ProtoReflect() protoreflect.Message {
1254- mi := &file_exec_proto_msgTypes[10]1313+ mi := &file_exec_proto_msgTypes[11]
1255 if x != nil {1314 if x != nil {
1256 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1315 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1257 if ms.LoadMessageInfo() == nil {1316 if ms.LoadMessageInfo() == nil {
@@ -1264,7 +1323,7 @@ func (x *Annotation) ProtoReflect() protoreflect.Message {
1264 1323
1265 // Deprecated: Use Annotation.ProtoReflect.Descriptor instead.1324 // Deprecated: Use Annotation.ProtoReflect.Descriptor instead.
1266 func (*Annotation) Descriptor() ([]byte, []int) {1325 func (*Annotation) Descriptor() ([]byte, []int) {
1267- return file_exec_proto_rawDescGZIP(), []int{10}1326+ return file_exec_proto_rawDescGZIP(), []int{11}
1268 }1327 }
1269 1328
1270 func (x *Annotation) GetLevel() AnnotationLevel {1329 func (x *Annotation) GetLevel() AnnotationLevel {
@@ -1337,7 +1396,7 @@ type StepAnnotations struct {
1337 1396
1338 func (x *StepAnnotations) Reset() {1397 func (x *StepAnnotations) Reset() {
1339 *x = StepAnnotations{}1398 *x = StepAnnotations{}
1340- mi := &file_exec_proto_msgTypes[11]1399+ mi := &file_exec_proto_msgTypes[12]
1341 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1400 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1342 ms.StoreMessageInfo(mi)1401 ms.StoreMessageInfo(mi)
1343 }1402 }
@@ -1349,7 +1408,7 @@ func (x *StepAnnotations) String() string {
1349 func (*StepAnnotations) ProtoMessage() {}1408 func (*StepAnnotations) ProtoMessage() {}
1350 1409
1351 func (x *StepAnnotations) ProtoReflect() protoreflect.Message {1410 func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
1352- mi := &file_exec_proto_msgTypes[11]1411+ mi := &file_exec_proto_msgTypes[12]
1353 if x != nil {1412 if x != nil {
1354 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1413 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1355 if ms.LoadMessageInfo() == nil {1414 if ms.LoadMessageInfo() == nil {
@@ -1362,7 +1421,7 @@ func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
1362 1421
1363 // Deprecated: Use StepAnnotations.ProtoReflect.Descriptor instead.1422 // Deprecated: Use StepAnnotations.ProtoReflect.Descriptor instead.
1364 func (*StepAnnotations) Descriptor() ([]byte, []int) {1423 func (*StepAnnotations) Descriptor() ([]byte, []int) {
1365- return file_exec_proto_rawDescGZIP(), []int{11}1424+ return file_exec_proto_rawDescGZIP(), []int{12}
1366 }1425 }
1367 1426
1368 func (x *StepAnnotations) GetStep() int32 {1427 func (x *StepAnnotations) GetStep() int32 {
@@ -1391,7 +1450,7 @@ type StepSummary struct {
1391 1450
1392 func (x *StepSummary) Reset() {1451 func (x *StepSummary) Reset() {
1393 *x = StepSummary{}1452 *x = StepSummary{}
1394- mi := &file_exec_proto_msgTypes[12]1453+ mi := &file_exec_proto_msgTypes[13]
1395 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1454 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1396 ms.StoreMessageInfo(mi)1455 ms.StoreMessageInfo(mi)
1397 }1456 }
@@ -1403,7 +1462,7 @@ func (x *StepSummary) String() string {
1403 func (*StepSummary) ProtoMessage() {}1462 func (*StepSummary) ProtoMessage() {}
1404 1463
1405 func (x *StepSummary) ProtoReflect() protoreflect.Message {1464 func (x *StepSummary) ProtoReflect() protoreflect.Message {
1406- mi := &file_exec_proto_msgTypes[12]1465+ mi := &file_exec_proto_msgTypes[13]
1407 if x != nil {1466 if x != nil {
1408 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1467 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1409 if ms.LoadMessageInfo() == nil {1468 if ms.LoadMessageInfo() == nil {
@@ -1416,7 +1475,7 @@ func (x *StepSummary) ProtoReflect() protoreflect.Message {
1416 1475
1417 // Deprecated: Use StepSummary.ProtoReflect.Descriptor instead.1476 // Deprecated: Use StepSummary.ProtoReflect.Descriptor instead.
1418 func (*StepSummary) Descriptor() ([]byte, []int) {1477 func (*StepSummary) Descriptor() ([]byte, []int) {
1419- return file_exec_proto_rawDescGZIP(), []int{12}1478+ return file_exec_proto_rawDescGZIP(), []int{13}
1420 }1479 }
1421 1480
1422 func (x *StepSummary) GetStep() int32 {1481 func (x *StepSummary) GetStep() int32 {
@@ -1448,7 +1507,7 @@ type Log struct {
1448 1507
1449 func (x *Log) Reset() {1508 func (x *Log) Reset() {
1450 *x = Log{}1509 *x = Log{}
1451- mi := &file_exec_proto_msgTypes[13]1510+ mi := &file_exec_proto_msgTypes[14]
1452 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1511 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1453 ms.StoreMessageInfo(mi)1512 ms.StoreMessageInfo(mi)
1454 }1513 }
@@ -1460,7 +1519,7 @@ func (x *Log) String() string {
1460 func (*Log) ProtoMessage() {}1519 func (*Log) ProtoMessage() {}
1461 1520
1462 func (x *Log) ProtoReflect() protoreflect.Message {1521 func (x *Log) ProtoReflect() protoreflect.Message {
1463- mi := &file_exec_proto_msgTypes[13]1522+ mi := &file_exec_proto_msgTypes[14]
1464 if x != nil {1523 if x != nil {
1465 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1524 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1466 if ms.LoadMessageInfo() == nil {1525 if ms.LoadMessageInfo() == nil {
@@ -1473,7 +1532,7 @@ func (x *Log) ProtoReflect() protoreflect.Message {
1473 1532
1474 // Deprecated: Use Log.ProtoReflect.Descriptor instead.1533 // Deprecated: Use Log.ProtoReflect.Descriptor instead.
1475 func (*Log) Descriptor() ([]byte, []int) {1534 func (*Log) Descriptor() ([]byte, []int) {
1476- return file_exec_proto_rawDescGZIP(), []int{13}1535+ return file_exec_proto_rawDescGZIP(), []int{14}
1477 }1536 }
1478 1537
1479 func (x *Log) GetStep() int32 {1538 func (x *Log) GetStep() int32 {
@@ -1517,7 +1576,7 @@ type JobResult struct {
1517 1576
1518 func (x *JobResult) Reset() {1577 func (x *JobResult) Reset() {
1519 *x = JobResult{}1578 *x = JobResult{}
1520- mi := &file_exec_proto_msgTypes[14]1579+ mi := &file_exec_proto_msgTypes[15]
1521 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1580 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1522 ms.StoreMessageInfo(mi)1581 ms.StoreMessageInfo(mi)
1523 }1582 }
@@ -1529,7 +1588,7 @@ func (x *JobResult) String() string {
1529 func (*JobResult) ProtoMessage() {}1588 func (*JobResult) ProtoMessage() {}
1530 1589
1531 func (x *JobResult) ProtoReflect() protoreflect.Message {1590 func (x *JobResult) ProtoReflect() protoreflect.Message {
1532- mi := &file_exec_proto_msgTypes[14]1591+ mi := &file_exec_proto_msgTypes[15]
1533 if x != nil {1592 if x != nil {
1534 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1593 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1535 if ms.LoadMessageInfo() == nil {1594 if ms.LoadMessageInfo() == nil {
@@ -1542,7 +1601,7 @@ func (x *JobResult) ProtoReflect() protoreflect.Message {
1542 1601
1543 // Deprecated: Use JobResult.ProtoReflect.Descriptor instead.1602 // Deprecated: Use JobResult.ProtoReflect.Descriptor instead.
1544 func (*JobResult) Descriptor() ([]byte, []int) {1603 func (*JobResult) Descriptor() ([]byte, []int) {
1545- return file_exec_proto_rawDescGZIP(), []int{14}1604+ return file_exec_proto_rawDescGZIP(), []int{15}
1546 }1605 }
1547 1606
1548 func (x *JobResult) GetConclusion() StepConclusion {1607 func (x *JobResult) GetConclusion() StepConclusion {
@@ -1587,7 +1646,7 @@ type ResolveRequest struct {
1587 1646
1588 func (x *ResolveRequest) Reset() {1647 func (x *ResolveRequest) Reset() {
1589 *x = ResolveRequest{}1648 *x = ResolveRequest{}
1590- mi := &file_exec_proto_msgTypes[15]1649+ mi := &file_exec_proto_msgTypes[16]
1591 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1650 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1592 ms.StoreMessageInfo(mi)1651 ms.StoreMessageInfo(mi)
1593 }1652 }
@@ -1599,7 +1658,7 @@ func (x *ResolveRequest) String() string {
1599 func (*ResolveRequest) ProtoMessage() {}1658 func (*ResolveRequest) ProtoMessage() {}
1600 1659
1601 func (x *ResolveRequest) ProtoReflect() protoreflect.Message {1660 func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
1602- mi := &file_exec_proto_msgTypes[15]1661+ mi := &file_exec_proto_msgTypes[16]
1603 if x != nil {1662 if x != nil {
1604 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1663 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1605 if ms.LoadMessageInfo() == nil {1664 if ms.LoadMessageInfo() == nil {
@@ -1612,7 +1671,7 @@ func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
1612 1671
1613 // Deprecated: Use ResolveRequest.ProtoReflect.Descriptor instead.1672 // Deprecated: Use ResolveRequest.ProtoReflect.Descriptor instead.
1614 func (*ResolveRequest) Descriptor() ([]byte, []int) {1673 func (*ResolveRequest) Descriptor() ([]byte, []int) {
1615- return file_exec_proto_rawDescGZIP(), []int{15}1674+ return file_exec_proto_rawDescGZIP(), []int{16}
1616 }1675 }
1617 1676
1618 func (x *ResolveRequest) GetId() int64 {1677 func (x *ResolveRequest) GetId() int64 {
@@ -1642,7 +1701,7 @@ type ResolveResponse struct {
1642 1701
1643 func (x *ResolveResponse) Reset() {1702 func (x *ResolveResponse) Reset() {
1644 *x = ResolveResponse{}1703 *x = ResolveResponse{}
1645- mi := &file_exec_proto_msgTypes[16]1704+ mi := &file_exec_proto_msgTypes[17]
1646 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1705 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1647 ms.StoreMessageInfo(mi)1706 ms.StoreMessageInfo(mi)
1648 }1707 }
@@ -1654,7 +1713,7 @@ func (x *ResolveResponse) String() string {
1654 func (*ResolveResponse) ProtoMessage() {}1713 func (*ResolveResponse) ProtoMessage() {}
1655 1714
1656 func (x *ResolveResponse) ProtoReflect() protoreflect.Message {1715 func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
1657- mi := &file_exec_proto_msgTypes[16]1716+ mi := &file_exec_proto_msgTypes[17]
1658 if x != nil {1717 if x != nil {
1659 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1718 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1660 if ms.LoadMessageInfo() == nil {1719 if ms.LoadMessageInfo() == nil {
@@ -1667,7 +1726,7 @@ func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
1667 1726
1668 // Deprecated: Use ResolveResponse.ProtoReflect.Descriptor instead.1727 // Deprecated: Use ResolveResponse.ProtoReflect.Descriptor instead.
1669 func (*ResolveResponse) Descriptor() ([]byte, []int) {1728 func (*ResolveResponse) Descriptor() ([]byte, []int) {
1670- return file_exec_proto_rawDescGZIP(), []int{16}1729+ return file_exec_proto_rawDescGZIP(), []int{17}
1671 }1730 }
1672 1731
1673 func (x *ResolveResponse) GetId() int64 {1732 func (x *ResolveResponse) GetId() int64 {
@@ -1714,7 +1773,7 @@ type ResolvedAction struct {
1714 1773
1715 func (x *ResolvedAction) Reset() {1774 func (x *ResolvedAction) Reset() {
1716 *x = ResolvedAction{}1775 *x = ResolvedAction{}
1717- mi := &file_exec_proto_msgTypes[17]1776+ mi := &file_exec_proto_msgTypes[18]
1718 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1777 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1719 ms.StoreMessageInfo(mi)1778 ms.StoreMessageInfo(mi)
1720 }1779 }
@@ -1726,7 +1785,7 @@ func (x *ResolvedAction) String() string {
1726 func (*ResolvedAction) ProtoMessage() {}1785 func (*ResolvedAction) ProtoMessage() {}
1727 1786
1728 func (x *ResolvedAction) ProtoReflect() protoreflect.Message {1787 func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
1729- mi := &file_exec_proto_msgTypes[17]1788+ mi := &file_exec_proto_msgTypes[18]
1730 if x != nil {1789 if x != nil {
1731 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1790 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1732 if ms.LoadMessageInfo() == nil {1791 if ms.LoadMessageInfo() == nil {
@@ -1739,7 +1798,7 @@ func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
1739 1798
1740 // Deprecated: Use ResolvedAction.ProtoReflect.Descriptor instead.1799 // Deprecated: Use ResolvedAction.ProtoReflect.Descriptor instead.
1741 func (*ResolvedAction) Descriptor() ([]byte, []int) {1800 func (*ResolvedAction) Descriptor() ([]byte, []int) {
1742- return file_exec_proto_rawDescGZIP(), []int{17}1801+ return file_exec_proto_rawDescGZIP(), []int{18}
1743 }1802 }
1744 1803
1745 func (x *ResolvedAction) GetRef() string {1804 func (x *ResolvedAction) GetRef() string {
@@ -1844,7 +1903,7 @@ const file_exec_proto_rawDesc = "" +
1844 "\x03job\x18\x03 \x01(\v2\x1e.ermite.runner.exec.v1.JobSpecR\x03job\x1a6\n" +1903 "\x03job\x18\x03 \x01(\v2\x1e.ermite.runner.exec.v1.JobSpecR\x03job\x1a6\n" +
1845 "\bEnvEntry\x12\x10\n" +1904 "\bEnvEntry\x12\x10\n" +
1846 "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +1905 "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1847- "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe3\x04\n" +1906+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb9\x05\n" +
1848 "\aJobSpec\x12#\n" +1907 "\aJobSpec\x12#\n" +
1849 "\rworkflow_path\x18\x01 \x01(\tR\fworkflowPath\x12#\n" +1908 "\rworkflow_path\x18\x01 \x01(\tR\fworkflowPath\x12#\n" +
1850 "\rworkflow_yaml\x18\x02 \x01(\tR\fworkflowYaml\x12\x17\n" +1909 "\rworkflow_yaml\x18\x02 \x01(\tR\fworkflowYaml\x12\x17\n" +
@@ -1867,7 +1926,13 @@ const file_exec_proto_rawDesc = "" +
1867 "cache_port\x18\x0f \x01(\rR\tcachePort\x12\x1f\n" +1926 "cache_port\x18\x0f \x01(\rR\tcachePort\x12\x1f\n" +
1868 "\vcache_token\x18\x10 \x01(\tR\n" +1927 "\vcache_token\x18\x10 \x01(\tR\n" +
1869 "cacheToken\x12)\n" +1928 "cacheToken\x12)\n" +
1870- "\x10secrets_withheld\x18\x13 \x01(\bR\x0fsecretsWithheldJ\x04\b\x11\x10\x12J\x04\b\x12\x10\x13R\asecretsR\x04vars\"t\n" +1929+ "\x10secrets_withheld\x18\x13 \x01(\bR\x0fsecretsWithheld\x12T\n" +
1930+ "\x11token_permissions\x18\x14 \x01(\v2'.ermite.runner.exec.v1.TokenPermissionsR\x10tokenPermissionsJ\x04\b\x11\x10\x12J\x04\b\x12\x10\x13R\asecretsR\x04vars\"\x9a\x01\n" +
1931+ "\x10TokenPermissions\x12K\n" +
1932+ "\x06levels\x18\x01 \x03(\v23.ermite.runner.exec.v1.TokenPermissions.LevelsEntryR\x06levels\x1a9\n" +
1933+ "\vLevelsEntry\x12\x10\n" +
1934+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1935+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"t\n" +
1871 "\x06Output\x129\n" +1936 "\x06Output\x129\n" +
1872 "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +1937 "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +
1873 "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +1938 "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +
@@ -1979,65 +2044,69 @@ func file_exec_proto_rawDescGZIP() []byte {
1979 }2044 }
1980 2045
1981 var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 4)2046 var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
1982-var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 20)2047+var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
1983 var file_exec_proto_goTypes = []any{2048 var file_exec_proto_goTypes = []any{
1984- (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind2049+ (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind
1985- (CancelKind)(0), // 1: ermite.runner.exec.v1.CancelKind2050+ (CancelKind)(0), // 1: ermite.runner.exec.v1.CancelKind
1986- (AnnotationLevel)(0), // 2: ermite.runner.exec.v1.AnnotationLevel2051+ (AnnotationLevel)(0), // 2: ermite.runner.exec.v1.AnnotationLevel
1987- (StepConclusion)(0), // 3: ermite.runner.exec.v1.StepConclusion2052+ (StepConclusion)(0), // 3: ermite.runner.exec.v1.StepConclusion
1988- (*Frame)(nil), // 4: ermite.runner.exec.v1.Frame2053+ (*Frame)(nil), // 4: ermite.runner.exec.v1.Frame
1989- (*NamedValue)(nil), // 5: ermite.runner.exec.v1.NamedValue2054+ (*NamedValue)(nil), // 5: ermite.runner.exec.v1.NamedValue
1990- (*Hello)(nil), // 6: ermite.runner.exec.v1.Hello2055+ (*Hello)(nil), // 6: ermite.runner.exec.v1.Hello
1991- (*Run)(nil), // 7: ermite.runner.exec.v1.Run2056+ (*Run)(nil), // 7: ermite.runner.exec.v1.Run
1992- (*JobSpec)(nil), // 8: ermite.runner.exec.v1.JobSpec2057+ (*JobSpec)(nil), // 8: ermite.runner.exec.v1.JobSpec
1993- (*Output)(nil), // 9: ermite.runner.exec.v1.Output2058+ (*TokenPermissions)(nil), // 9: ermite.runner.exec.v1.TokenPermissions
1994- (*Exit)(nil), // 10: ermite.runner.exec.v1.Exit2059+ (*Output)(nil), // 10: ermite.runner.exec.v1.Output
1995- (*Cancel)(nil), // 11: ermite.runner.exec.v1.Cancel2060+ (*Exit)(nil), // 11: ermite.runner.exec.v1.Exit
1996- (*StepStarted)(nil), // 12: ermite.runner.exec.v1.StepStarted2061+ (*Cancel)(nil), // 12: ermite.runner.exec.v1.Cancel
1997- (*StepCompleted)(nil), // 13: ermite.runner.exec.v1.StepCompleted2062+ (*StepStarted)(nil), // 13: ermite.runner.exec.v1.StepStarted
1998- (*Annotation)(nil), // 14: ermite.runner.exec.v1.Annotation2063+ (*StepCompleted)(nil), // 14: ermite.runner.exec.v1.StepCompleted
1999- (*StepAnnotations)(nil), // 15: ermite.runner.exec.v1.StepAnnotations2064+ (*Annotation)(nil), // 15: ermite.runner.exec.v1.Annotation
2000- (*StepSummary)(nil), // 16: ermite.runner.exec.v1.StepSummary2065+ (*StepAnnotations)(nil), // 16: ermite.runner.exec.v1.StepAnnotations
2001- (*Log)(nil), // 17: ermite.runner.exec.v1.Log2066+ (*StepSummary)(nil), // 17: ermite.runner.exec.v1.StepSummary
2002- (*JobResult)(nil), // 18: ermite.runner.exec.v1.JobResult2067+ (*Log)(nil), // 18: ermite.runner.exec.v1.Log
2003- (*ResolveRequest)(nil), // 19: ermite.runner.exec.v1.ResolveRequest2068+ (*JobResult)(nil), // 19: ermite.runner.exec.v1.JobResult
2004- (*ResolveResponse)(nil), // 20: ermite.runner.exec.v1.ResolveResponse2069+ (*ResolveRequest)(nil), // 20: ermite.runner.exec.v1.ResolveRequest
2005- (*ResolvedAction)(nil), // 21: ermite.runner.exec.v1.ResolvedAction2070+ (*ResolveResponse)(nil), // 21: ermite.runner.exec.v1.ResolveResponse
2006- nil, // 22: ermite.runner.exec.v1.Run.EnvEntry2071+ (*ResolvedAction)(nil), // 22: ermite.runner.exec.v1.ResolvedAction
2007- nil, // 23: ermite.runner.exec.v1.JobResult.OutputsEntry2072+ nil, // 23: ermite.runner.exec.v1.Run.EnvEntry
2073+ nil, // 24: ermite.runner.exec.v1.TokenPermissions.LevelsEntry
2074+ nil, // 25: ermite.runner.exec.v1.JobResult.OutputsEntry
2008 }2075 }
2009 var file_exec_proto_depIdxs = []int32{2076 var file_exec_proto_depIdxs = []int32{
2010 6, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello2077 6, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello
2011 7, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run2078 7, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run
2012- 9, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output2079+ 10, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output
2013- 10, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit2080+ 11, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit
2014- 11, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel2081+ 12, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel
2015- 12, // 5: ermite.runner.exec.v1.Frame.step_started:type_name -> ermite.runner.exec.v1.StepStarted2082+ 13, // 5: ermite.runner.exec.v1.Frame.step_started:type_name -> ermite.runner.exec.v1.StepStarted
2016- 13, // 6: ermite.runner.exec.v1.Frame.step_completed:type_name -> ermite.runner.exec.v1.StepCompleted2083+ 14, // 6: ermite.runner.exec.v1.Frame.step_completed:type_name -> ermite.runner.exec.v1.StepCompleted
2017- 17, // 7: ermite.runner.exec.v1.Frame.log:type_name -> ermite.runner.exec.v1.Log2084+ 18, // 7: ermite.runner.exec.v1.Frame.log:type_name -> ermite.runner.exec.v1.Log
2018- 18, // 8: ermite.runner.exec.v1.Frame.job_result:type_name -> ermite.runner.exec.v1.JobResult2085+ 19, // 8: ermite.runner.exec.v1.Frame.job_result:type_name -> ermite.runner.exec.v1.JobResult
2019- 15, // 9: ermite.runner.exec.v1.Frame.step_annotations:type_name -> ermite.runner.exec.v1.StepAnnotations2086+ 16, // 9: ermite.runner.exec.v1.Frame.step_annotations:type_name -> ermite.runner.exec.v1.StepAnnotations
2020- 16, // 10: ermite.runner.exec.v1.Frame.step_summary:type_name -> ermite.runner.exec.v1.StepSummary2087+ 17, // 10: ermite.runner.exec.v1.Frame.step_summary:type_name -> ermite.runner.exec.v1.StepSummary
2021- 19, // 11: ermite.runner.exec.v1.Frame.resolve_request:type_name -> ermite.runner.exec.v1.ResolveRequest2088+ 20, // 11: ermite.runner.exec.v1.Frame.resolve_request:type_name -> ermite.runner.exec.v1.ResolveRequest
2022- 20, // 12: ermite.runner.exec.v1.Frame.resolve_response:type_name -> ermite.runner.exec.v1.ResolveResponse2089+ 21, // 12: ermite.runner.exec.v1.Frame.resolve_response:type_name -> ermite.runner.exec.v1.ResolveResponse
2023 5, // 13: ermite.runner.exec.v1.Frame.secret:type_name -> ermite.runner.exec.v1.NamedValue2090 5, // 13: ermite.runner.exec.v1.Frame.secret:type_name -> ermite.runner.exec.v1.NamedValue
2024 5, // 14: ermite.runner.exec.v1.Frame.var:type_name -> ermite.runner.exec.v1.NamedValue2091 5, // 14: ermite.runner.exec.v1.Frame.var:type_name -> ermite.runner.exec.v1.NamedValue
2025- 22, // 15: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry2092+ 23, // 15: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry
2026 8, // 16: ermite.runner.exec.v1.Run.job:type_name -> ermite.runner.exec.v1.JobSpec2093 8, // 16: ermite.runner.exec.v1.Run.job:type_name -> ermite.runner.exec.v1.JobSpec
2027- 0, // 17: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind2094+ 9, // 17: ermite.runner.exec.v1.JobSpec.token_permissions:type_name -> ermite.runner.exec.v1.TokenPermissions
2028- 1, // 18: ermite.runner.exec.v1.Cancel.kind:type_name -> ermite.runner.exec.v1.CancelKind2095+ 24, // 18: ermite.runner.exec.v1.TokenPermissions.levels:type_name -> ermite.runner.exec.v1.TokenPermissions.LevelsEntry
2029- 3, // 19: ermite.runner.exec.v1.StepCompleted.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion2096+ 0, // 19: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind
2030- 3, // 20: ermite.runner.exec.v1.StepCompleted.outcome:type_name -> ermite.runner.exec.v1.StepConclusion2097+ 1, // 20: ermite.runner.exec.v1.Cancel.kind:type_name -> ermite.runner.exec.v1.CancelKind
2031- 2, // 21: ermite.runner.exec.v1.Annotation.level:type_name -> ermite.runner.exec.v1.AnnotationLevel2098+ 3, // 21: ermite.runner.exec.v1.StepCompleted.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
2032- 14, // 22: ermite.runner.exec.v1.StepAnnotations.annotations:type_name -> ermite.runner.exec.v1.Annotation2099+ 3, // 22: ermite.runner.exec.v1.StepCompleted.outcome:type_name -> ermite.runner.exec.v1.StepConclusion
2033- 3, // 23: ermite.runner.exec.v1.JobResult.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion2100+ 2, // 23: ermite.runner.exec.v1.Annotation.level:type_name -> ermite.runner.exec.v1.AnnotationLevel
2034- 23, // 24: ermite.runner.exec.v1.JobResult.outputs:type_name -> ermite.runner.exec.v1.JobResult.OutputsEntry2101+ 15, // 24: ermite.runner.exec.v1.StepAnnotations.annotations:type_name -> ermite.runner.exec.v1.Annotation
2035- 21, // 25: ermite.runner.exec.v1.ResolveResponse.actions:type_name -> ermite.runner.exec.v1.ResolvedAction2102+ 3, // 25: ermite.runner.exec.v1.JobResult.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
2036- 26, // [26:26] is the sub-list for method output_type2103+ 25, // 26: ermite.runner.exec.v1.JobResult.outputs:type_name -> ermite.runner.exec.v1.JobResult.OutputsEntry
2037- 26, // [26:26] is the sub-list for method input_type2104+ 22, // 27: ermite.runner.exec.v1.ResolveResponse.actions:type_name -> ermite.runner.exec.v1.ResolvedAction
2038- 26, // [26:26] is the sub-list for extension type_name2105+ 28, // [28:28] is the sub-list for method output_type
2039- 26, // [26:26] is the sub-list for extension extendee2106+ 28, // [28:28] is the sub-list for method input_type
2040- 0, // [0:26] is the sub-list for field type_name2107+ 28, // [28:28] is the sub-list for extension type_name
2108+ 28, // [28:28] is the sub-list for extension extendee
2109+ 0, // [0:28] is the sub-list for field type_name
2041 }2110 }
2042 2111
2043 func init() { file_exec_proto_init() }2112 func init() { file_exec_proto_init() }
@@ -2068,7 +2137,7 @@ func file_exec_proto_init() {
2068 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),2137 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
2069 RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),2138 RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),
2070 NumEnums: 4,2139 NumEnums: 4,
2071- NumMessages: 20,2140+ NumMessages: 22,
2072 NumExtensions: 0,2141 NumExtensions: 0,
2073 NumServices: 0,2142 NumServices: 0,
2074 },2143 },
M internal/executor/job.go +18 -5
@@ -85,6 +85,11 @@ type Config struct {
8585 // SecretsWithheld says that the server did not send secrets because the run
8686 // is not trusted: "Set up job" says so.
8787 SecretsWithheld bool
88+ // TokenPermissions are the permissions of GITHUB_TOKEN as the server sent
89+ // them (Job.permissions: key to "read", "write" or "none"), which "Set up
90+ // job" prints as they are. Nil means the server sent none: they are then
91+ // worked out from the workflow. An empty, non-nil map is "none at all".
92+ TokenPermissions map[string]string
8893 // Debug turns ##[debug] lines on (ACTIONS_STEP_DEBUG).
8994 Debug bool
9095 // Actions configures the actions of the job (resolution, downloads, Node.js,
@@ -729,13 +734,21 @@ const SecretsWithheldLine = "Secrets are not available to this job: the run was
729734 // and the cache mode.
730735 //
731736 // GitHub's runner prints what the server sends as
732-// system.github.token.permissions (JobExtension.cs:154-166); ermite's Job
733-// message does not carry it, so the group is worked out from the workflow
734-// with the rules recorded on github.com (tokenPermissionLines). It is always
735-// printed: Metadata is listed even for "permissions: {}".
737+// system.github.token.permissions (JobExtension.cs:154-166). ermite's server
738+// sends them in Job.permissions, already merged and lowered by its trust rule:
739+// they are printed as they are (serverPermissionLines). From a server that
740+// sends none the group is worked out from the workflow with the rules recorded
741+// on github.com (tokenPermissionLines). It is always printed: Metadata is listed
742+// even for "permissions: {}".
736743 func (j *job) printTokenInfo() {
737744 j.out(1, "##[group]GITHUB_TOKEN Permissions")
738- for _, p := range tokenPermissionLines(j.wf, j.jb) {
745+ var lines []permissionLine
746+ if j.cfg.TokenPermissions != nil {
747+ lines = serverPermissionLines(j.cfg.TokenPermissions)
748+ } else {
749+ lines = tokenPermissionLines(j.wf, j.jb)
750+ }
751+ for _, p := range lines {
739752 j.out(1, p.name+": "+p.level)
740753 }
741754 j.out(1, "##[endgroup]")
@@ -85,6 +85,11 @@ type Config struct {
85 // SecretsWithheld says that the server did not send secrets because the run85 // SecretsWithheld says that the server did not send secrets because the run
86 // is not trusted: "Set up job" says so.86 // is not trusted: "Set up job" says so.
87 SecretsWithheld bool87 SecretsWithheld bool
88+ // TokenPermissions are the permissions of GITHUB_TOKEN as the server sent
89+ // them (Job.permissions: key to "read", "write" or "none"), which "Set up
90+ // job" prints as they are. Nil means the server sent none: they are then
91+ // worked out from the workflow. An empty, non-nil map is "none at all".
92+ TokenPermissions map[string]string
88 // Debug turns ##[debug] lines on (ACTIONS_STEP_DEBUG).93 // Debug turns ##[debug] lines on (ACTIONS_STEP_DEBUG).
89 Debug bool94 Debug bool
90 // Actions configures the actions of the job (resolution, downloads, Node.js,95 // Actions configures the actions of the job (resolution, downloads, Node.js,
@@ -729,13 +734,21 @@ const SecretsWithheldLine = "Secrets are not available to this job: the run was
729 // and the cache mode.734 // and the cache mode.
730 //735 //
731 // GitHub's runner prints what the server sends as736 // GitHub's runner prints what the server sends as
732-// system.github.token.permissions (JobExtension.cs:154-166); ermite's Job737+// system.github.token.permissions (JobExtension.cs:154-166). ermite's server
733-// message does not carry it, so the group is worked out from the workflow738+// sends them in Job.permissions, already merged and lowered by its trust rule:
734-// with the rules recorded on github.com (tokenPermissionLines). It is always739+// they are printed as they are (serverPermissionLines). From a server that
735-// printed: Metadata is listed even for "permissions: {}".740+// sends none the group is worked out from the workflow with the rules recorded
741+// on github.com (tokenPermissionLines). It is always printed: Metadata is listed
742+// even for "permissions: {}".
736 func (j *job) printTokenInfo() {743 func (j *job) printTokenInfo() {
737 j.out(1, "##[group]GITHUB_TOKEN Permissions")744 j.out(1, "##[group]GITHUB_TOKEN Permissions")
738- for _, p := range tokenPermissionLines(j.wf, j.jb) {745+ var lines []permissionLine
746+ if j.cfg.TokenPermissions != nil {
747+ lines = serverPermissionLines(j.cfg.TokenPermissions)
748+ } else {
749+ lines = tokenPermissionLines(j.wf, j.jb)
750+ }
751+ for _, p := range lines {
739 j.out(1, p.name+": "+p.level)752 j.out(1, p.name+": "+p.level)
740 }753 }
741 j.out(1, "##[endgroup]")754 j.out(1, "##[endgroup]")
M internal/executor/secrets_test.go +36 -0
@@ -317,3 +317,39 @@ func TestOnlyDiagUsesTheDiagnosticsLogger(t *testing.T) {
317317 }
318318 }
319319 }
320+
321+// The group prints what the server sent in Job.permissions, as GitHub's runner
322+// prints system.github.token.permissions, and not what the workflow says.
323+func TestSetUpJobPrintsTheServersTokenPermissions(t *testing.T) {
324+ needTools(t, "bash")
325+ group := func(log string) string {
326+ _, rest, _ := strings.Cut(log, "##[group]GITHUB_TOKEN Permissions\n")
327+ g, _, _ := strings.Cut(rest, "\n##[endgroup]")
328+ return g
329+ }
330+ // The workflow asks for write-all; the server lowered it (an untrusted run).
331+ yaml := "on: push\npermissions: write-all\njobs:\n j:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"
332+ for _, tc := range []struct {
333+ name string
334+ levels map[string]string
335+ want string
336+ }{
337+ {"lowered by the server", map[string]string{"contents": "read", "pull-requests": "read", "issues": "read", "metadata": "read"},
338+ "Contents: read\nIssues: read\nMetadata: read\nPullRequests: read"},
339+ {"write kept", map[string]string{"contents": "write", "metadata": "read", "packages": "read"}, "Contents: write\nMetadata: read\nPackages: read"},
340+ {"none and id-token are not listed, metadata always is", map[string]string{"contents": "none", "id-token": "write", "actions": "read"},
341+ "Actions: read\nMetadata: read"},
342+ {"nothing at all is metadata only", map[string]string{}, "Metadata: read"},
343+ {"a key the runner does not know", map[string]string{"made-up-key": "read"}, "MadeUpKey: read\nMetadata: read"},
344+ } {
345+ got := group(setupLog(t, yaml, "j", func(c *Config) { c.TokenPermissions = tc.levels }))
346+ if got != tc.want {
347+ t.Errorf("%s:\n%s\nwant:\n%s", tc.name, got, tc.want)
348+ }
349+ }
350+ // A server that sends none: worked out from the workflow, as before.
351+ got := group(setupLog(t, yaml, "j", nil))
352+ if !strings.Contains(got, "Contents: write") || !strings.Contains(got, "Models: read") {
353+ t.Errorf("fallback to the workflow:\n%s", got)
354+ }
355+}
@@ -317,3 +317,39 @@ func TestOnlyDiagUsesTheDiagnosticsLogger(t *testing.T) {
317 }317 }
318 }318 }
319 }319 }
320+
321+// The group prints what the server sent in Job.permissions, as GitHub's runner
322+// prints system.github.token.permissions, and not what the workflow says.
323+func TestSetUpJobPrintsTheServersTokenPermissions(t *testing.T) {
324+ needTools(t, "bash")
325+ group := func(log string) string {
326+ _, rest, _ := strings.Cut(log, "##[group]GITHUB_TOKEN Permissions\n")
327+ g, _, _ := strings.Cut(rest, "\n##[endgroup]")
328+ return g
329+ }
330+ // The workflow asks for write-all; the server lowered it (an untrusted run).
331+ yaml := "on: push\npermissions: write-all\njobs:\n j:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"
332+ for _, tc := range []struct {
333+ name string
334+ levels map[string]string
335+ want string
336+ }{
337+ {"lowered by the server", map[string]string{"contents": "read", "pull-requests": "read", "issues": "read", "metadata": "read"},
338+ "Contents: read\nIssues: read\nMetadata: read\nPullRequests: read"},
339+ {"write kept", map[string]string{"contents": "write", "metadata": "read", "packages": "read"}, "Contents: write\nMetadata: read\nPackages: read"},
340+ {"none and id-token are not listed, metadata always is", map[string]string{"contents": "none", "id-token": "write", "actions": "read"},
341+ "Actions: read\nMetadata: read"},
342+ {"nothing at all is metadata only", map[string]string{}, "Metadata: read"},
343+ {"a key the runner does not know", map[string]string{"made-up-key": "read"}, "MadeUpKey: read\nMetadata: read"},
344+ } {
345+ got := group(setupLog(t, yaml, "j", func(c *Config) { c.TokenPermissions = tc.levels }))
346+ if got != tc.want {
347+ t.Errorf("%s:\n%s\nwant:\n%s", tc.name, got, tc.want)
348+ }
349+ }
350+ // A server that sends none: worked out from the workflow, as before.
351+ got := group(setupLog(t, yaml, "j", nil))
352+ if !strings.Contains(got, "Contents: write") || !strings.Contains(got, "Models: read") {
353+ t.Errorf("fallback to the workflow:\n%s", got)
354+ }
355+}
M internal/executor/token_permissions.go +35 -0
@@ -4,6 +4,7 @@ package executor
44
55 import (
66 "sort"
7+ "strings"
78
89 "bnema.dev/ermite-actions/workflow"
910 )
@@ -106,3 +107,37 @@ func tokenPermissionLines(wf *workflow.Workflow, jb *workflow.Job) []permissionL
106107 sort.Slice(lines, func(a, b int) bool { return lines[a].name < lines[b].name })
107108 return lines
108109 }
110+
111+// serverPermissionLines returns the lines of the group for the permissions the
112+// server sent (Job.permissions): one per key at "read" or "write", sorted by
113+// display name like GitHub's runner prints them. Metadata is always listed as
114+// read. A key at "none" or absent is not listed, nor is id-token (GitHub never
115+// lists it); a key the runner does not know is listed under its name in Pascal
116+// case, as the known ones are ("pull-requests" is "PullRequests").
117+func serverPermissionLines(levels map[string]string) []permissionLine {
118+ lines := []permissionLine{{"Metadata", "read"}}
119+ for key, level := range levels {
120+ if (level != "read" && level != "write") || key == "metadata" || key == "id-token" {
121+ continue
122+ }
123+ lines = append(lines, permissionLine{displayName(key), level})
124+ }
125+ sort.Slice(lines, func(a, b int) bool { return lines[a].name < lines[b].name })
126+ return lines
127+}
128+
129+// displayName is the name "Set up job" prints for a permission key.
130+func displayName(key string) string {
131+ for _, s := range tokenScopes {
132+ if s.key == key {
133+ return s.display
134+ }
135+ }
136+ var b strings.Builder
137+ for _, part := range strings.Split(key, "-") {
138+ if part != "" {
139+ b.WriteString(strings.ToUpper(part[:1]) + part[1:])
140+ }
141+ }
142+ return b.String()
143+}
@@ -4,6 +4,7 @@ package executor
4 4
5 import (5 import (
6 "sort"6 "sort"
7+ "strings"
7 8
8 "bnema.dev/ermite-actions/workflow"9 "bnema.dev/ermite-actions/workflow"
9 )10 )
@@ -106,3 +107,37 @@ func tokenPermissionLines(wf *workflow.Workflow, jb *workflow.Job) []permissionL
106 sort.Slice(lines, func(a, b int) bool { return lines[a].name < lines[b].name })107 sort.Slice(lines, func(a, b int) bool { return lines[a].name < lines[b].name })
107 return lines108 return lines
108 }109 }
110+
111+// serverPermissionLines returns the lines of the group for the permissions the
112+// server sent (Job.permissions): one per key at "read" or "write", sorted by
113+// display name like GitHub's runner prints them. Metadata is always listed as
114+// read. A key at "none" or absent is not listed, nor is id-token (GitHub never
115+// lists it); a key the runner does not know is listed under its name in Pascal
116+// case, as the known ones are ("pull-requests" is "PullRequests").
117+func serverPermissionLines(levels map[string]string) []permissionLine {
118+ lines := []permissionLine{{"Metadata", "read"}}
119+ for key, level := range levels {
120+ if (level != "read" && level != "write") || key == "metadata" || key == "id-token" {
121+ continue
122+ }
123+ lines = append(lines, permissionLine{displayName(key), level})
124+ }
125+ sort.Slice(lines, func(a, b int) bool { return lines[a].name < lines[b].name })
126+ return lines
127+}
128+
129+// displayName is the name "Set up job" prints for a permission key.
130+func displayName(key string) string {
131+ for _, s := range tokenScopes {
132+ if s.key == key {
133+ return s.display
134+ }
135+ }
136+ var b strings.Builder
137+ for _, part := range strings.Split(key, "-") {
138+ if part != "" {
139+ b.WriteString(strings.ToUpper(part[:1]) + part[1:])
140+ }
141+ }
142+ return b.String()
143+}
M internal/guest/jobexec.go +14 -1
@@ -259,7 +259,7 @@ func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *ex
259259 Home: su.Home, ToolCache: os.Getenv("RUNNER_TOOL_CACHE"), BaseEnv: stepEnv(os.Environ(), su), Credential: su.Credential,
260260 CacheURL: cacheURL(os.Getenv("ERMITE_GATEWAY"), spec.GetCachePort()), CacheToken: spec.GetCacheToken(),
261261 Debug: spec.GetDebug(), Reporter: rep, Actions: executor.DefaultActionsConfig(res.resolve), Log: zerowrap.FromCtx(ctx),
262- Secrets: vals.secrets, Vars: vals.vars, SecretsWithheld: spec.GetSecretsWithheld(),
262+ Secrets: vals.secrets, Vars: vals.vars, SecretsWithheld: spec.GetSecretsWithheld(), TokenPermissions: tokenPermissions(spec),
263263 SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(),
264264 TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay,
265265 })
@@ -290,3 +290,16 @@ func failSetup(conn *execstream.Conn, msg string) int {
290290 rep.JobCompleted(executor.Result{Conclusion: executor.ConclusionFailure, Error: msg})
291291 return 1
292292 }
293+
294+// tokenPermissions is the permissions of GITHUB_TOKEN the server sent, nil when
295+// it sent none (the executor then works them out from the workflow).
296+func tokenPermissions(spec *execpb.JobSpec) map[string]string {
297+ p := spec.GetTokenPermissions()
298+ if p == nil {
299+ return nil
300+ }
301+ if p.GetLevels() == nil {
302+ return map[string]string{}
303+ }
304+ return p.GetLevels()
305+}
@@ -259,7 +259,7 @@ func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *ex
259 Home: su.Home, ToolCache: os.Getenv("RUNNER_TOOL_CACHE"), BaseEnv: stepEnv(os.Environ(), su), Credential: su.Credential,259 Home: su.Home, ToolCache: os.Getenv("RUNNER_TOOL_CACHE"), BaseEnv: stepEnv(os.Environ(), su), Credential: su.Credential,
260 CacheURL: cacheURL(os.Getenv("ERMITE_GATEWAY"), spec.GetCachePort()), CacheToken: spec.GetCacheToken(),260 CacheURL: cacheURL(os.Getenv("ERMITE_GATEWAY"), spec.GetCachePort()), CacheToken: spec.GetCacheToken(),
261 Debug: spec.GetDebug(), Reporter: rep, Actions: executor.DefaultActionsConfig(res.resolve), Log: zerowrap.FromCtx(ctx),261 Debug: spec.GetDebug(), Reporter: rep, Actions: executor.DefaultActionsConfig(res.resolve), Log: zerowrap.FromCtx(ctx),
262- Secrets: vals.secrets, Vars: vals.vars, SecretsWithheld: spec.GetSecretsWithheld(),262+ Secrets: vals.secrets, Vars: vals.vars, SecretsWithheld: spec.GetSecretsWithheld(), TokenPermissions: tokenPermissions(spec),
263 SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(),263 SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(),
264 TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay,264 TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay,
265 })265 })
@@ -290,3 +290,16 @@ func failSetup(conn *execstream.Conn, msg string) int {
290 rep.JobCompleted(executor.Result{Conclusion: executor.ConclusionFailure, Error: msg})290 rep.JobCompleted(executor.Result{Conclusion: executor.ConclusionFailure, Error: msg})
291 return 1291 return 1
292 }292 }
293+
294+// tokenPermissions is the permissions of GITHUB_TOKEN the server sent, nil when
295+// it sent none (the executor then works them out from the workflow).
296+func tokenPermissions(spec *execpb.JobSpec) map[string]string {
297+ p := spec.GetTokenPermissions()
298+ if p == nil {
299+ return nil
300+ }
301+ if p.GetLevels() == nil {
302+ return map[string]string{}
303+ }
304+ return p.GetLevels()
305+}
M internal/guest/jobexec_test.go +85 -2
@@ -13,12 +13,20 @@ import (
1313 "testing"
1414 "time"
1515
16+ "google.golang.org/protobuf/proto"
17+
1618 "bnema.dev/ermite-runner/internal/execstream"
1719 "bnema.dev/ermite-runner/internal/execstream/execpb"
1820 )
1921
2022 // runJobSession starts Exec on a unix socket and drives it with a job.
2123 func runJobSession(t *testing.T, yaml string, cancelOn string, tweak func(*ExecConfig)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {
24+ t.Helper()
25+ return runJobSessionSpec(t, yaml, cancelOn, tweak, nil)
26+}
27+
28+// runJobSessionSpec is runJobSession with a change to the JobSpec sent.
29+func runJobSessionSpec(t *testing.T, yaml string, cancelOn string, tweak func(*ExecConfig), spec func(*execpb.JobSpec)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {
2230 t.Helper()
2331 sock := filepath.Join(t.TempDir(), "s")
2432 ln, err := net.Listen("unix", sock)
@@ -53,10 +61,14 @@ func runJobSession(t *testing.T, yaml string, cancelOn string, tweak func(*ExecC
5361 }
5462 var cancelOnce sync.Once
5563 var mu sync.Mutex
56- _, exit, err = execstream.DriveJob(conn, &execpb.Run{Job: &execpb.JobSpec{
64+ js := &execpb.JobSpec{
5765 WorkflowPath: ".github/workflows/ci.yml", WorkflowYaml: yaml, JobKey: "j", Repo: "hello", RunnerName: "r", RunnerVersion: "v",
5866 Contexts: `{"github":{"repository":"octo/hello","event_name":"push","job":"j"}}`, Matrix: "{}",
59- }}, cancel, execstream.JobHandler{
67+ }
68+ if spec != nil {
69+ spec(js)
70+ }
71+ _, exit, err = execstream.DriveJob(conn, &execpb.Run{Job: js}, cancel, execstream.JobHandler{
6072 StepStarted: func(s *execpb.StepStarted) {
6173 mu.Lock()
6274 steps = append(steps, s.Name)
@@ -331,3 +343,74 @@ func TestExecSendsResultsFrames(t *testing.T) {
331343 }
332344 }
333345 }
346+
347+// The permissions the server sent reach "Set up job" through the Run frame as
348+// the guest reads them: absent keeps the workflow's, present but empty is
349+// "Metadata only", populated is printed as sent. The frame is marshalled and
350+// parsed on the way, where a nil and an empty map would look alike.
351+func TestExecJobPrintsTheServersTokenPermissions(t *testing.T) {
352+ requireBash(t)
353+ yaml := "on: push\npermissions: write-all\njobs:\n j:\n runs-on: x\n steps:\n - run: echo hi\n"
354+ group := func(logs []*execpb.Log) string {
355+ var lines []string
356+ in := false
357+ for _, l := range logs {
358+ switch {
359+ case l.Text == "##[group]GITHUB_TOKEN Permissions":
360+ in = true
361+ case l.Text == "##[endgroup]":
362+ in = false
363+ case in:
364+ lines = append(lines, l.Text)
365+ }
366+ }
367+ return strings.Join(lines, "\n")
368+ }
369+ for _, tc := range []struct {
370+ name string
371+ set func(*execpb.JobSpec)
372+ want string
373+ }{
374+ {"absent: from the workflow", nil, "Actions: write\nArtifactMetadata: write\nAttestations: write\nChecks: write\nCodeQuality: write\nContents: write\n" +
375+ "CopilotRequests: write\nDeployments: write\nDiscussions: write\nDrives: write\nIssues: write\nMetadata: read\nModels: read\nPackages: write\n" +
376+ "Pages: write\nPullRequests: write\nRepositoryProjects: write\nSecurityEvents: write\nStatuses: write\nVulnerabilityAlerts: read"},
377+ {"present and empty: metadata only", func(s *execpb.JobSpec) { s.TokenPermissions = &execpb.TokenPermissions{} }, "Metadata: read"},
378+ {"populated: as sent", func(s *execpb.JobSpec) {
379+ s.TokenPermissions = &execpb.TokenPermissions{Levels: map[string]string{"contents": "read", "issues": "none"}}
380+ }, "Contents: read\nMetadata: read"},
381+ } {
382+ _, logs, res, _ := runJobSessionSpec(t, yaml, "", nil, tc.set)
383+ if res == nil || res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS {
384+ t.Fatalf("%s: %+v", tc.name, res)
385+ }
386+ if got := group(logs); got != tc.want {
387+ t.Errorf("%s:\n%s\nwant:\n%s", tc.name, got, tc.want)
388+ }
389+ }
390+}
391+
392+func TestTokenPermissionsNilVersusEmpty(t *testing.T) {
393+ for name, tc := range map[string]struct {
394+ spec *execpb.JobSpec
395+ wantNil bool
396+ want int
397+ }{
398+ "absent": {&execpb.JobSpec{}, true, 0},
399+ "empty": {&execpb.JobSpec{TokenPermissions: &execpb.TokenPermissions{}}, false, 0},
400+ "populated": {&execpb.JobSpec{TokenPermissions: &execpb.TokenPermissions{Levels: map[string]string{"contents": "read"}}}, false, 1},
401+ } {
402+ // After the trip a frame makes.
403+ raw, err := proto.Marshal(tc.spec)
404+ if err != nil {
405+ t.Fatal(err)
406+ }
407+ var got execpb.JobSpec
408+ if err := proto.Unmarshal(raw, &got); err != nil {
409+ t.Fatal(err)
410+ }
411+ m := tokenPermissions(&got)
412+ if (m == nil) != tc.wantNil || len(m) != tc.want {
413+ t.Errorf("%s: %#v", name, m)
414+ }
415+ }
416+}
@@ -13,12 +13,20 @@ import (
13 "testing"13 "testing"
14 "time"14 "time"
15 15
16+ "google.golang.org/protobuf/proto"
17+
16 "bnema.dev/ermite-runner/internal/execstream"18 "bnema.dev/ermite-runner/internal/execstream"
17 "bnema.dev/ermite-runner/internal/execstream/execpb"19 "bnema.dev/ermite-runner/internal/execstream/execpb"
18 )20 )
19 21
20 // runJobSession starts Exec on a unix socket and drives it with a job.22 // runJobSession starts Exec on a unix socket and drives it with a job.
21 func runJobSession(t *testing.T, yaml string, cancelOn string, tweak func(*ExecConfig)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {23 func runJobSession(t *testing.T, yaml string, cancelOn string, tweak func(*ExecConfig)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {
24+ t.Helper()
25+ return runJobSessionSpec(t, yaml, cancelOn, tweak, nil)
26+}
27+
28+// runJobSessionSpec is runJobSession with a change to the JobSpec sent.
29+func runJobSessionSpec(t *testing.T, yaml string, cancelOn string, tweak func(*ExecConfig), spec func(*execpb.JobSpec)) (steps []string, logs []*execpb.Log, res *execpb.JobResult, exit *execpb.Exit) {
22 t.Helper()30 t.Helper()
23 sock := filepath.Join(t.TempDir(), "s")31 sock := filepath.Join(t.TempDir(), "s")
24 ln, err := net.Listen("unix", sock)32 ln, err := net.Listen("unix", sock)
@@ -53,10 +61,14 @@ func runJobSession(t *testing.T, yaml string, cancelOn string, tweak func(*ExecC
53 }61 }
54 var cancelOnce sync.Once62 var cancelOnce sync.Once
55 var mu sync.Mutex63 var mu sync.Mutex
56- _, exit, err = execstream.DriveJob(conn, &execpb.Run{Job: &execpb.JobSpec{64+ js := &execpb.JobSpec{
57 WorkflowPath: ".github/workflows/ci.yml", WorkflowYaml: yaml, JobKey: "j", Repo: "hello", RunnerName: "r", RunnerVersion: "v",65 WorkflowPath: ".github/workflows/ci.yml", WorkflowYaml: yaml, JobKey: "j", Repo: "hello", RunnerName: "r", RunnerVersion: "v",
58 Contexts: `{"github":{"repository":"octo/hello","event_name":"push","job":"j"}}`, Matrix: "{}",66 Contexts: `{"github":{"repository":"octo/hello","event_name":"push","job":"j"}}`, Matrix: "{}",
59- }}, cancel, execstream.JobHandler{67+ }
68+ if spec != nil {
69+ spec(js)
70+ }
71+ _, exit, err = execstream.DriveJob(conn, &execpb.Run{Job: js}, cancel, execstream.JobHandler{
60 StepStarted: func(s *execpb.StepStarted) {72 StepStarted: func(s *execpb.StepStarted) {
61 mu.Lock()73 mu.Lock()
62 steps = append(steps, s.Name)74 steps = append(steps, s.Name)
@@ -331,3 +343,74 @@ func TestExecSendsResultsFrames(t *testing.T) {
331 }343 }
332 }344 }
333 }345 }
346+
347+// The permissions the server sent reach "Set up job" through the Run frame as
348+// the guest reads them: absent keeps the workflow's, present but empty is
349+// "Metadata only", populated is printed as sent. The frame is marshalled and
350+// parsed on the way, where a nil and an empty map would look alike.
351+func TestExecJobPrintsTheServersTokenPermissions(t *testing.T) {
352+ requireBash(t)
353+ yaml := "on: push\npermissions: write-all\njobs:\n j:\n runs-on: x\n steps:\n - run: echo hi\n"
354+ group := func(logs []*execpb.Log) string {
355+ var lines []string
356+ in := false
357+ for _, l := range logs {
358+ switch {
359+ case l.Text == "##[group]GITHUB_TOKEN Permissions":
360+ in = true
361+ case l.Text == "##[endgroup]":
362+ in = false
363+ case in:
364+ lines = append(lines, l.Text)
365+ }
366+ }
367+ return strings.Join(lines, "\n")
368+ }
369+ for _, tc := range []struct {
370+ name string
371+ set func(*execpb.JobSpec)
372+ want string
373+ }{
374+ {"absent: from the workflow", nil, "Actions: write\nArtifactMetadata: write\nAttestations: write\nChecks: write\nCodeQuality: write\nContents: write\n" +
375+ "CopilotRequests: write\nDeployments: write\nDiscussions: write\nDrives: write\nIssues: write\nMetadata: read\nModels: read\nPackages: write\n" +
376+ "Pages: write\nPullRequests: write\nRepositoryProjects: write\nSecurityEvents: write\nStatuses: write\nVulnerabilityAlerts: read"},
377+ {"present and empty: metadata only", func(s *execpb.JobSpec) { s.TokenPermissions = &execpb.TokenPermissions{} }, "Metadata: read"},
378+ {"populated: as sent", func(s *execpb.JobSpec) {
379+ s.TokenPermissions = &execpb.TokenPermissions{Levels: map[string]string{"contents": "read", "issues": "none"}}
380+ }, "Contents: read\nMetadata: read"},
381+ } {
382+ _, logs, res, _ := runJobSessionSpec(t, yaml, "", nil, tc.set)
383+ if res == nil || res.Conclusion != execpb.StepConclusion_STEP_CONCLUSION_SUCCESS {
384+ t.Fatalf("%s: %+v", tc.name, res)
385+ }
386+ if got := group(logs); got != tc.want {
387+ t.Errorf("%s:\n%s\nwant:\n%s", tc.name, got, tc.want)
388+ }
389+ }
390+}
391+
392+func TestTokenPermissionsNilVersusEmpty(t *testing.T) {
393+ for name, tc := range map[string]struct {
394+ spec *execpb.JobSpec
395+ wantNil bool
396+ want int
397+ }{
398+ "absent": {&execpb.JobSpec{}, true, 0},
399+ "empty": {&execpb.JobSpec{TokenPermissions: &execpb.TokenPermissions{}}, false, 0},
400+ "populated": {&execpb.JobSpec{TokenPermissions: &execpb.TokenPermissions{Levels: map[string]string{"contents": "read"}}}, false, 1},
401+ } {
402+ // After the trip a frame makes.
403+ raw, err := proto.Marshal(tc.spec)
404+ if err != nil {
405+ t.Fatal(err)
406+ }
407+ var got execpb.JobSpec
408+ if err := proto.Unmarshal(raw, &got); err != nil {
409+ t.Fatal(err)
410+ }
411+ m := tokenPermissions(&got)
412+ if (m == nil) != tc.wantNil || len(m) != tc.want {
413+ t.Errorf("%s: %#v", name, m)
414+ }
415+ }
416+}
M internal/host/job.go +3 -0
@@ -697,6 +697,9 @@ func (j *jobRun) drive(conn sandbox.Conn, rt *Runner) (*execpb.Exit, error) {
697697 // only when the server has ci.secrets.
698698 SecretsWithheld: j.features.secrets && !j.job.Trusted,
699699 }}
700+ if p := j.job.Permissions; p != nil && j.features.secrets {
701+ run.Job.TokenPermissions = &execpb.TokenPermissions{Levels: p.GetLevels()}
702+ }
700703 run.Job.CachePort, run.Job.CacheToken = j.cacheFor(rt)
701704 _, exit, err := execstream.DriveJob(conn, run, j.cancelCh, execstream.JobHandler{
702705 StepStarted: j.stepStarted, StepCompleted: j.stepCompleted,
@@ -697,6 +697,9 @@ func (j *jobRun) drive(conn sandbox.Conn, rt *Runner) (*execpb.Exit, error) {
697 // only when the server has ci.secrets.697 // only when the server has ci.secrets.
698 SecretsWithheld: j.features.secrets && !j.job.Trusted,698 SecretsWithheld: j.features.secrets && !j.job.Trusted,
699 }}699 }}
700+ if p := j.job.Permissions; p != nil && j.features.secrets {
701+ run.Job.TokenPermissions = &execpb.TokenPermissions{Levels: p.GetLevels()}
702+ }
700 run.Job.CachePort, run.Job.CacheToken = j.cacheFor(rt)703 run.Job.CachePort, run.Job.CacheToken = j.cacheFor(rt)
701 _, exit, err := execstream.DriveJob(conn, run, j.cancelCh, execstream.JobHandler{704 _, exit, err := execstream.DriveJob(conn, run, j.cancelCh, execstream.JobHandler{
702 StepStarted: j.stepStarted, StepCompleted: j.stepCompleted,705 StepStarted: j.stepStarted, StepCompleted: j.stepCompleted,
M internal/host/secrets_test.go +47 -0
@@ -9,6 +9,7 @@ import (
99 "go/format"
1010 "go/parser"
1111 "go/token"
12+ "maps"
1213 "os"
1314 "path/filepath"
1415 "regexp"
@@ -17,6 +18,8 @@ import (
1718
1819 "google.golang.org/protobuf/proto"
1920
21+ ermite "bnema.dev/ermite-api/gen/ermite"
22+
2023 "bnema.dev/ermite-runner/internal/execstream/execpb"
2124 )
2225
@@ -300,3 +303,47 @@ func TestPrintLeaksHeuristic(t *testing.T) {
300303 }
301304 }
302305 }
306+
307+// Job.permissions reaches the executor in the Run frame when the server has
308+// ci.secrets (it sent them); an old server's absent field stays unset, so that
309+// the executor works them out from the workflow.
310+func TestTokenPermissionsReachTheExecutor(t *testing.T) {
311+ for _, tc := range []struct {
312+ name string
313+ features []string
314+ perms *ermite.JobPermissions
315+ want map[string]string // nil: unset in the frame
316+ }{
317+ {"sent", []string{FeatureSecrets}, &ermite.JobPermissions{Levels: map[string]string{"contents": "write", "metadata": "read"}},
318+ map[string]string{"contents": "write", "metadata": "read"}},
319+ {"sent and empty", []string{FeatureSecrets}, &ermite.JobPermissions{}, map[string]string{}},
320+ {"not sent", []string{FeatureSecrets}, nil, nil},
321+ {"old server", []string{FeatureAnnotations}, &ermite.JobPermissions{Levels: map[string]string{"contents": "write"}}, nil},
322+ } {
323+ t.Run(tc.name, func(t *testing.T) {
324+ seenRun.Store(nil)
325+ h := newHarness(t, resultsScript(nil))
326+ h.runner.Cfg.StateDir = t.TempDir()
327+ h.runner.Features = func(context.Context) ([]string, error) { return tc.features, nil }
328+ j := testJob("j1")
329+ j.Permissions = tc.perms
330+ h.srv.jobs = []*ermite.Job{j}
331+ stop := h.run(t)
332+ eventually(t, "CompleteJob", func() bool { return h.srv.completeCount() == 1 })
333+ if err := stop(); err != nil {
334+ t.Fatal(err)
335+ }
336+ run := seenRun.Load()
337+ if run == nil {
338+ t.Fatal("no Run frame")
339+ }
340+ got := run.Job.GetTokenPermissions()
341+ switch {
342+ case tc.want == nil && got != nil:
343+ t.Errorf("unexpected permissions %v", got.GetLevels())
344+ case tc.want != nil && (got == nil || !maps.Equal(got.GetLevels(), tc.want)):
345+ t.Errorf("permissions %v, want %v", got.GetLevels(), tc.want)
346+ }
347+ })
348+ }
349+}
@@ -9,6 +9,7 @@ import (
9 "go/format"9 "go/format"
10 "go/parser"10 "go/parser"
11 "go/token"11 "go/token"
12+ "maps"
12 "os"13 "os"
13 "path/filepath"14 "path/filepath"
14 "regexp"15 "regexp"
@@ -17,6 +18,8 @@ import (
17 18
18 "google.golang.org/protobuf/proto"19 "google.golang.org/protobuf/proto"
19 20
21+ ermite "bnema.dev/ermite-api/gen/ermite"
22+
20 "bnema.dev/ermite-runner/internal/execstream/execpb"23 "bnema.dev/ermite-runner/internal/execstream/execpb"
21 )24 )
22 25
@@ -300,3 +303,47 @@ func TestPrintLeaksHeuristic(t *testing.T) {
300 }303 }
301 }304 }
302 }305 }
306+
307+// Job.permissions reaches the executor in the Run frame when the server has
308+// ci.secrets (it sent them); an old server's absent field stays unset, so that
309+// the executor works them out from the workflow.
310+func TestTokenPermissionsReachTheExecutor(t *testing.T) {
311+ for _, tc := range []struct {
312+ name string
313+ features []string
314+ perms *ermite.JobPermissions
315+ want map[string]string // nil: unset in the frame
316+ }{
317+ {"sent", []string{FeatureSecrets}, &ermite.JobPermissions{Levels: map[string]string{"contents": "write", "metadata": "read"}},
318+ map[string]string{"contents": "write", "metadata": "read"}},
319+ {"sent and empty", []string{FeatureSecrets}, &ermite.JobPermissions{}, map[string]string{}},
320+ {"not sent", []string{FeatureSecrets}, nil, nil},
321+ {"old server", []string{FeatureAnnotations}, &ermite.JobPermissions{Levels: map[string]string{"contents": "write"}}, nil},
322+ } {
323+ t.Run(tc.name, func(t *testing.T) {
324+ seenRun.Store(nil)
325+ h := newHarness(t, resultsScript(nil))
326+ h.runner.Cfg.StateDir = t.TempDir()
327+ h.runner.Features = func(context.Context) ([]string, error) { return tc.features, nil }
328+ j := testJob("j1")
329+ j.Permissions = tc.perms
330+ h.srv.jobs = []*ermite.Job{j}
331+ stop := h.run(t)
332+ eventually(t, "CompleteJob", func() bool { return h.srv.completeCount() == 1 })
333+ if err := stop(); err != nil {
334+ t.Fatal(err)
335+ }
336+ run := seenRun.Load()
337+ if run == nil {
338+ t.Fatal("no Run frame")
339+ }
340+ got := run.Job.GetTokenPermissions()
341+ switch {
342+ case tc.want == nil && got != nil:
343+ t.Errorf("unexpected permissions %v", got.GetLevels())
344+ case tc.want != nil && (got == nil || !maps.Equal(got.GetLevels(), tc.want)):
345+ t.Errorf("permissions %v, want %v", got.GetLevels(), tc.want)
346+ }
347+ })
348+ }
349+}

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