detached c936897

feat(executor): secrets, vars and masking from the job (#25)

commit
c93689733dd6ba4beb0fbc9568f2b42099bb9b05
author
brice · · 2026-10-14 22:08 UTC
committer
bnema · · 2026-10-14 22:08 UTC
parent
49c3309
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
15 files · +1168 -145
M internal/execstream/exec.proto +22 -0
@@ -29,9 +29,21 @@ message Frame {
2929 StepSummary step_summary = 11; // executor -> host, before the StepCompleted of the step
3030 ResolveRequest resolve_request = 12; // executor -> host
3131 ResolveResponse resolve_response = 13; // host -> executor, answers a ResolveRequest
32+ NamedValue secret = 14; // host -> executor, one per secret, before Run
33+ NamedValue var = 15; // host -> executor, one per variable, before Run
3234 }
3335 }
3436
37+// NamedValue is one secret (Job.secrets) or one variable (Job.vars) of the job.
38+// They travel in frames of their own, before Run, rather than in JobSpec: a
39+// frame is at most 2 MiB (execstream.MaxFrame) and ermite allows many secrets of
40+// 48 KiB, so a map in Run could not carry them. The executor keeps them until
41+// Run arrives. Secrets are never logged nor written to disk.
42+message NamedValue {
43+ string name = 1;
44+ string value = 2;
45+}
46+
3547 message Hello {
3648 string version = 1; // runner version of the executor
3749 string sandbox = 2; // "vm" or "container"
@@ -85,6 +97,16 @@ message JobSpec {
8597 // job's repository and refs.
8698 uint32 cache_port = 15;
8799 string cache_token = 16;
100+ // Job.secrets and Job.vars are not here: they come in NamedValue frames
101+ // before Run (see NamedValue). The executor takes the vars of those frames
102+ // for the "vars" context when contexts has none.
103+ reserved 17, 18;
104+ reserved "secrets", "vars";
105+ // secrets_withheld is set by the host when the server sends Job.trusted and
106+ // the job is not trusted (feature ci.secrets): "Set up job" then says that
107+ // secrets are not available. Never set for servers without the feature, for
108+ // which an absent Job.trusted only means "not sent".
109+ bool secrets_withheld = 19;
88110 }
89111
90112 enum StreamKind {
@@ -29,9 +29,21 @@ message Frame {
29 StepSummary step_summary = 11; // executor -> host, before the StepCompleted of the step29 StepSummary step_summary = 11; // executor -> host, before the StepCompleted of the step
30 ResolveRequest resolve_request = 12; // executor -> host30 ResolveRequest resolve_request = 12; // executor -> host
31 ResolveResponse resolve_response = 13; // host -> executor, answers a ResolveRequest31 ResolveResponse resolve_response = 13; // host -> executor, answers a ResolveRequest
32+ NamedValue secret = 14; // host -> executor, one per secret, before Run
33+ NamedValue var = 15; // host -> executor, one per variable, before Run
32 }34 }
33 }35 }
34 36
37+// NamedValue is one secret (Job.secrets) or one variable (Job.vars) of the job.
38+// They travel in frames of their own, before Run, rather than in JobSpec: a
39+// frame is at most 2 MiB (execstream.MaxFrame) and ermite allows many secrets of
40+// 48 KiB, so a map in Run could not carry them. The executor keeps them until
41+// Run arrives. Secrets are never logged nor written to disk.
42+message NamedValue {
43+ string name = 1;
44+ string value = 2;
45+}
46+
35 message Hello {47 message Hello {
36 string version = 1; // runner version of the executor48 string version = 1; // runner version of the executor
37 string sandbox = 2; // "vm" or "container"49 string sandbox = 2; // "vm" or "container"
@@ -85,6 +97,16 @@ message JobSpec {
85 // job's repository and refs.97 // job's repository and refs.
86 uint32 cache_port = 15;98 uint32 cache_port = 15;
87 string cache_token = 16;99 string cache_token = 16;
100+ // Job.secrets and Job.vars are not here: they come in NamedValue frames
101+ // before Run (see NamedValue). The executor takes the vars of those frames
102+ // for the "vars" context when contexts has none.
103+ reserved 17, 18;
104+ reserved "secrets", "vars";
105+ // secrets_withheld is set by the host when the server sends Job.trusted and
106+ // the job is not trusted (feature ci.secrets): "Set up job" then says that
107+ // secrets are not available. Never set for servers without the feature, for
108+ // which an absent Job.trusted only means "not sent".
109+ bool secrets_withheld = 19;
88 }110 }
89 111
90 enum StreamKind {112 enum StreamKind {
M internal/execstream/execpb/exec.pb.go +219 -106
@@ -259,6 +259,8 @@ type Frame struct {
259259 // *Frame_StepSummary
260260 // *Frame_ResolveRequest
261261 // *Frame_ResolveResponse
262+ // *Frame_Secret
263+ // *Frame_Var
262264 Body isFrame_Body `protobuf_oneof:"body"`
263265 unknownFields protoimpl.UnknownFields
264266 sizeCache protoimpl.SizeCache
@@ -418,6 +420,24 @@ func (x *Frame) GetResolveResponse() *ResolveResponse {
418420 return nil
419421 }
420422
423+func (x *Frame) GetSecret() *NamedValue {
424+ if x != nil {
425+ if x, ok := x.Body.(*Frame_Secret); ok {
426+ return x.Secret
427+ }
428+ }
429+ return nil
430+}
431+
432+func (x *Frame) GetVar() *NamedValue {
433+ if x != nil {
434+ if x, ok := x.Body.(*Frame_Var); ok {
435+ return x.Var
436+ }
437+ }
438+ return nil
439+}
440+
421441 type isFrame_Body interface {
422442 isFrame_Body()
423443 }
@@ -474,6 +494,14 @@ type Frame_ResolveResponse struct {
474494 ResolveResponse *ResolveResponse `protobuf:"bytes,13,opt,name=resolve_response,json=resolveResponse,proto3,oneof"` // host -> executor, answers a ResolveRequest
475495 }
476496
497+type Frame_Secret struct {
498+ Secret *NamedValue `protobuf:"bytes,14,opt,name=secret,proto3,oneof"` // host -> executor, one per secret, before Run
499+}
500+
501+type Frame_Var struct {
502+ Var *NamedValue `protobuf:"bytes,15,opt,name=var,proto3,oneof"` // host -> executor, one per variable, before Run
503+}
504+
477505 func (*Frame_Hello) isFrame_Body() {}
478506
479507 func (*Frame_Run) isFrame_Body() {}
@@ -500,6 +528,67 @@ func (*Frame_ResolveRequest) isFrame_Body() {}
500528
501529 func (*Frame_ResolveResponse) isFrame_Body() {}
502530
531+func (*Frame_Secret) isFrame_Body() {}
532+
533+func (*Frame_Var) isFrame_Body() {}
534+
535+// NamedValue is one secret (Job.secrets) or one variable (Job.vars) of the job.
536+// They travel in frames of their own, before Run, rather than in JobSpec: a
537+// frame is at most 2 MiB (execstream.MaxFrame) and ermite allows many secrets of
538+// 48 KiB, so a map in Run could not carry them. The executor keeps them until
539+// Run arrives. Secrets are never logged nor written to disk.
540+type NamedValue struct {
541+ state protoimpl.MessageState `protogen:"open.v1"`
542+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
543+ Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
544+ unknownFields protoimpl.UnknownFields
545+ sizeCache protoimpl.SizeCache
546+}
547+
548+func (x *NamedValue) Reset() {
549+ *x = NamedValue{}
550+ mi := &file_exec_proto_msgTypes[1]
551+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
552+ ms.StoreMessageInfo(mi)
553+}
554+
555+func (x *NamedValue) String() string {
556+ return protoimpl.X.MessageStringOf(x)
557+}
558+
559+func (*NamedValue) ProtoMessage() {}
560+
561+func (x *NamedValue) ProtoReflect() protoreflect.Message {
562+ mi := &file_exec_proto_msgTypes[1]
563+ if x != nil {
564+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
565+ if ms.LoadMessageInfo() == nil {
566+ ms.StoreMessageInfo(mi)
567+ }
568+ return ms
569+ }
570+ return mi.MessageOf(x)
571+}
572+
573+// Deprecated: Use NamedValue.ProtoReflect.Descriptor instead.
574+func (*NamedValue) Descriptor() ([]byte, []int) {
575+ return file_exec_proto_rawDescGZIP(), []int{1}
576+}
577+
578+func (x *NamedValue) GetName() string {
579+ if x != nil {
580+ return x.Name
581+ }
582+ return ""
583+}
584+
585+func (x *NamedValue) GetValue() string {
586+ if x != nil {
587+ return x.Value
588+ }
589+ return ""
590+}
591+
503592 type Hello struct {
504593 state protoimpl.MessageState `protogen:"open.v1"`
505594 Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` // runner version of the executor
@@ -511,7 +600,7 @@ type Hello struct {
511600
512601 func (x *Hello) Reset() {
513602 *x = Hello{}
514- mi := &file_exec_proto_msgTypes[1]
603+ mi := &file_exec_proto_msgTypes[2]
515604 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
516605 ms.StoreMessageInfo(mi)
517606 }
@@ -523,7 +612,7 @@ func (x *Hello) String() string {
523612 func (*Hello) ProtoMessage() {}
524613
525614 func (x *Hello) ProtoReflect() protoreflect.Message {
526- mi := &file_exec_proto_msgTypes[1]
615+ mi := &file_exec_proto_msgTypes[2]
527616 if x != nil {
528617 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
529618 if ms.LoadMessageInfo() == nil {
@@ -536,7 +625,7 @@ func (x *Hello) ProtoReflect() protoreflect.Message {
536625
537626 // Deprecated: Use Hello.ProtoReflect.Descriptor instead.
538627 func (*Hello) Descriptor() ([]byte, []int) {
539- return file_exec_proto_rawDescGZIP(), []int{1}
628+ return file_exec_proto_rawDescGZIP(), []int{2}
540629 }
541630
542631 func (x *Hello) GetVersion() string {
@@ -576,7 +665,7 @@ type Run struct {
576665
577666 func (x *Run) Reset() {
578667 *x = Run{}
579- mi := &file_exec_proto_msgTypes[2]
668+ mi := &file_exec_proto_msgTypes[3]
580669 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
581670 ms.StoreMessageInfo(mi)
582671 }
@@ -588,7 +677,7 @@ func (x *Run) String() string {
588677 func (*Run) ProtoMessage() {}
589678
590679 func (x *Run) ProtoReflect() protoreflect.Message {
591- mi := &file_exec_proto_msgTypes[2]
680+ mi := &file_exec_proto_msgTypes[3]
592681 if x != nil {
593682 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
594683 if ms.LoadMessageInfo() == nil {
@@ -601,7 +690,7 @@ func (x *Run) ProtoReflect() protoreflect.Message {
601690
602691 // Deprecated: Use Run.ProtoReflect.Descriptor instead.
603692 func (*Run) Descriptor() ([]byte, []int) {
604- return file_exec_proto_rawDescGZIP(), []int{2}
693+ return file_exec_proto_rawDescGZIP(), []int{3}
605694 }
606695
607696 func (x *Run) GetScript() string {
@@ -660,15 +749,20 @@ type JobSpec struct {
660749 // runner does, and masks the token. The token is not an ermite credential: it
661750 // is an HMAC that only the runner's cache service accepts, scoped to this
662751 // job's repository and refs.
663- CachePort uint32 `protobuf:"varint,15,opt,name=cache_port,json=cachePort,proto3" json:"cache_port,omitempty"`
664- CacheToken string `protobuf:"bytes,16,opt,name=cache_token,json=cacheToken,proto3" json:"cache_token,omitempty"`
665- unknownFields protoimpl.UnknownFields
666- sizeCache protoimpl.SizeCache
752+ CachePort uint32 `protobuf:"varint,15,opt,name=cache_port,json=cachePort,proto3" json:"cache_port,omitempty"`
753+ CacheToken string `protobuf:"bytes,16,opt,name=cache_token,json=cacheToken,proto3" json:"cache_token,omitempty"`
754+ // secrets_withheld is set by the host when the server sends Job.trusted and
755+ // the job is not trusted (feature ci.secrets): "Set up job" then says that
756+ // secrets are not available. Never set for servers without the feature, for
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"`
759+ unknownFields protoimpl.UnknownFields
760+ sizeCache protoimpl.SizeCache
667761 }
668762
669763 func (x *JobSpec) Reset() {
670764 *x = JobSpec{}
671- mi := &file_exec_proto_msgTypes[3]
765+ mi := &file_exec_proto_msgTypes[4]
672766 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
673767 ms.StoreMessageInfo(mi)
674768 }
@@ -680,7 +774,7 @@ func (x *JobSpec) String() string {
680774 func (*JobSpec) ProtoMessage() {}
681775
682776 func (x *JobSpec) ProtoReflect() protoreflect.Message {
683- mi := &file_exec_proto_msgTypes[3]
777+ mi := &file_exec_proto_msgTypes[4]
684778 if x != nil {
685779 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
686780 if ms.LoadMessageInfo() == nil {
@@ -693,7 +787,7 @@ func (x *JobSpec) ProtoReflect() protoreflect.Message {
693787
694788 // Deprecated: Use JobSpec.ProtoReflect.Descriptor instead.
695789 func (*JobSpec) Descriptor() ([]byte, []int) {
696- return file_exec_proto_rawDescGZIP(), []int{3}
790+ return file_exec_proto_rawDescGZIP(), []int{4}
697791 }
698792
699793 func (x *JobSpec) GetWorkflowPath() string {
@@ -808,6 +902,13 @@ func (x *JobSpec) GetCacheToken() string {
808902 return ""
809903 }
810904
905+func (x *JobSpec) GetSecretsWithheld() bool {
906+ if x != nil {
907+ return x.SecretsWithheld
908+ }
909+ return false
910+}
911+
811912 type Output struct {
812913 state protoimpl.MessageState `protogen:"open.v1"`
813914 Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`
@@ -819,7 +920,7 @@ type Output struct {
819920
820921 func (x *Output) Reset() {
821922 *x = Output{}
822- mi := &file_exec_proto_msgTypes[4]
923+ mi := &file_exec_proto_msgTypes[5]
823924 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
824925 ms.StoreMessageInfo(mi)
825926 }
@@ -831,7 +932,7 @@ func (x *Output) String() string {
831932 func (*Output) ProtoMessage() {}
832933
833934 func (x *Output) ProtoReflect() protoreflect.Message {
834- mi := &file_exec_proto_msgTypes[4]
935+ mi := &file_exec_proto_msgTypes[5]
835936 if x != nil {
836937 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
837938 if ms.LoadMessageInfo() == nil {
@@ -844,7 +945,7 @@ func (x *Output) ProtoReflect() protoreflect.Message {
844945
845946 // Deprecated: Use Output.ProtoReflect.Descriptor instead.
846947 func (*Output) Descriptor() ([]byte, []int) {
847- return file_exec_proto_rawDescGZIP(), []int{4}
948+ return file_exec_proto_rawDescGZIP(), []int{5}
848949 }
849950
850951 func (x *Output) GetStream() StreamKind {
@@ -878,7 +979,7 @@ type Exit struct {
878979
879980 func (x *Exit) Reset() {
880981 *x = Exit{}
881- mi := &file_exec_proto_msgTypes[5]
982+ mi := &file_exec_proto_msgTypes[6]
882983 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
883984 ms.StoreMessageInfo(mi)
884985 }
@@ -890,7 +991,7 @@ func (x *Exit) String() string {
890991 func (*Exit) ProtoMessage() {}
891992
892993 func (x *Exit) ProtoReflect() protoreflect.Message {
893- mi := &file_exec_proto_msgTypes[5]
994+ mi := &file_exec_proto_msgTypes[6]
894995 if x != nil {
895996 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
896997 if ms.LoadMessageInfo() == nil {
@@ -903,7 +1004,7 @@ func (x *Exit) ProtoReflect() protoreflect.Message {
9031004
9041005 // Deprecated: Use Exit.ProtoReflect.Descriptor instead.
9051006 func (*Exit) Descriptor() ([]byte, []int) {
906- return file_exec_proto_rawDescGZIP(), []int{5}
1007+ return file_exec_proto_rawDescGZIP(), []int{6}
9071008 }
9081009
9091010 func (x *Exit) GetCode() int32 {
@@ -937,7 +1038,7 @@ type Cancel struct {
9371038
9381039 func (x *Cancel) Reset() {
9391040 *x = Cancel{}
940- mi := &file_exec_proto_msgTypes[6]
1041+ mi := &file_exec_proto_msgTypes[7]
9411042 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9421043 ms.StoreMessageInfo(mi)
9431044 }
@@ -949,7 +1050,7 @@ func (x *Cancel) String() string {
9491050 func (*Cancel) ProtoMessage() {}
9501051
9511052 func (x *Cancel) ProtoReflect() protoreflect.Message {
952- mi := &file_exec_proto_msgTypes[6]
1053+ mi := &file_exec_proto_msgTypes[7]
9531054 if x != nil {
9541055 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9551056 if ms.LoadMessageInfo() == nil {
@@ -962,7 +1063,7 @@ func (x *Cancel) ProtoReflect() protoreflect.Message {
9621063
9631064 // Deprecated: Use Cancel.ProtoReflect.Descriptor instead.
9641065 func (*Cancel) Descriptor() ([]byte, []int) {
965- return file_exec_proto_rawDescGZIP(), []int{6}
1066+ return file_exec_proto_rawDescGZIP(), []int{7}
9661067 }
9671068
9681069 func (x *Cancel) GetReason() string {
@@ -992,7 +1093,7 @@ type StepStarted struct {
9921093
9931094 func (x *StepStarted) Reset() {
9941095 *x = StepStarted{}
995- mi := &file_exec_proto_msgTypes[7]
1096+ mi := &file_exec_proto_msgTypes[8]
9961097 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
9971098 ms.StoreMessageInfo(mi)
9981099 }
@@ -1004,7 +1105,7 @@ func (x *StepStarted) String() string {
10041105 func (*StepStarted) ProtoMessage() {}
10051106
10061107 func (x *StepStarted) ProtoReflect() protoreflect.Message {
1007- mi := &file_exec_proto_msgTypes[7]
1108+ mi := &file_exec_proto_msgTypes[8]
10081109 if x != nil {
10091110 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10101111 if ms.LoadMessageInfo() == nil {
@@ -1017,7 +1118,7 @@ func (x *StepStarted) ProtoReflect() protoreflect.Message {
10171118
10181119 // Deprecated: Use StepStarted.ProtoReflect.Descriptor instead.
10191120 func (*StepStarted) Descriptor() ([]byte, []int) {
1020- return file_exec_proto_rawDescGZIP(), []int{7}
1121+ return file_exec_proto_rawDescGZIP(), []int{8}
10211122 }
10221123
10231124 func (x *StepStarted) GetNumber() int32 {
@@ -1057,7 +1158,7 @@ type StepCompleted struct {
10571158
10581159 func (x *StepCompleted) Reset() {
10591160 *x = StepCompleted{}
1060- mi := &file_exec_proto_msgTypes[8]
1161+ mi := &file_exec_proto_msgTypes[9]
10611162 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10621163 ms.StoreMessageInfo(mi)
10631164 }
@@ -1069,7 +1170,7 @@ func (x *StepCompleted) String() string {
10691170 func (*StepCompleted) ProtoMessage() {}
10701171
10711172 func (x *StepCompleted) ProtoReflect() protoreflect.Message {
1072- mi := &file_exec_proto_msgTypes[8]
1173+ mi := &file_exec_proto_msgTypes[9]
10731174 if x != nil {
10741175 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10751176 if ms.LoadMessageInfo() == nil {
@@ -1082,7 +1183,7 @@ func (x *StepCompleted) ProtoReflect() protoreflect.Message {
10821183
10831184 // Deprecated: Use StepCompleted.ProtoReflect.Descriptor instead.
10841185 func (*StepCompleted) Descriptor() ([]byte, []int) {
1085- return file_exec_proto_rawDescGZIP(), []int{8}
1186+ return file_exec_proto_rawDescGZIP(), []int{9}
10861187 }
10871188
10881189 func (x *StepCompleted) GetNumber() int32 {
@@ -1138,7 +1239,7 @@ type Annotation struct {
11381239
11391240 func (x *Annotation) Reset() {
11401241 *x = Annotation{}
1141- mi := &file_exec_proto_msgTypes[9]
1242+ mi := &file_exec_proto_msgTypes[10]
11421243 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11431244 ms.StoreMessageInfo(mi)
11441245 }
@@ -1150,7 +1251,7 @@ func (x *Annotation) String() string {
11501251 func (*Annotation) ProtoMessage() {}
11511252
11521253 func (x *Annotation) ProtoReflect() protoreflect.Message {
1153- mi := &file_exec_proto_msgTypes[9]
1254+ mi := &file_exec_proto_msgTypes[10]
11541255 if x != nil {
11551256 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11561257 if ms.LoadMessageInfo() == nil {
@@ -1163,7 +1264,7 @@ func (x *Annotation) ProtoReflect() protoreflect.Message {
11631264
11641265 // Deprecated: Use Annotation.ProtoReflect.Descriptor instead.
11651266 func (*Annotation) Descriptor() ([]byte, []int) {
1166- return file_exec_proto_rawDescGZIP(), []int{9}
1267+ return file_exec_proto_rawDescGZIP(), []int{10}
11671268 }
11681269
11691270 func (x *Annotation) GetLevel() AnnotationLevel {
@@ -1236,7 +1337,7 @@ type StepAnnotations struct {
12361337
12371338 func (x *StepAnnotations) Reset() {
12381339 *x = StepAnnotations{}
1239- mi := &file_exec_proto_msgTypes[10]
1340+ mi := &file_exec_proto_msgTypes[11]
12401341 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12411342 ms.StoreMessageInfo(mi)
12421343 }
@@ -1248,7 +1349,7 @@ func (x *StepAnnotations) String() string {
12481349 func (*StepAnnotations) ProtoMessage() {}
12491350
12501351 func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
1251- mi := &file_exec_proto_msgTypes[10]
1352+ mi := &file_exec_proto_msgTypes[11]
12521353 if x != nil {
12531354 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12541355 if ms.LoadMessageInfo() == nil {
@@ -1261,7 +1362,7 @@ func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
12611362
12621363 // Deprecated: Use StepAnnotations.ProtoReflect.Descriptor instead.
12631364 func (*StepAnnotations) Descriptor() ([]byte, []int) {
1264- return file_exec_proto_rawDescGZIP(), []int{10}
1365+ return file_exec_proto_rawDescGZIP(), []int{11}
12651366 }
12661367
12671368 func (x *StepAnnotations) GetStep() int32 {
@@ -1290,7 +1391,7 @@ type StepSummary struct {
12901391
12911392 func (x *StepSummary) Reset() {
12921393 *x = StepSummary{}
1293- mi := &file_exec_proto_msgTypes[11]
1394+ mi := &file_exec_proto_msgTypes[12]
12941395 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12951396 ms.StoreMessageInfo(mi)
12961397 }
@@ -1302,7 +1403,7 @@ func (x *StepSummary) String() string {
13021403 func (*StepSummary) ProtoMessage() {}
13031404
13041405 func (x *StepSummary) ProtoReflect() protoreflect.Message {
1305- mi := &file_exec_proto_msgTypes[11]
1406+ mi := &file_exec_proto_msgTypes[12]
13061407 if x != nil {
13071408 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13081409 if ms.LoadMessageInfo() == nil {
@@ -1315,7 +1416,7 @@ func (x *StepSummary) ProtoReflect() protoreflect.Message {
13151416
13161417 // Deprecated: Use StepSummary.ProtoReflect.Descriptor instead.
13171418 func (*StepSummary) Descriptor() ([]byte, []int) {
1318- return file_exec_proto_rawDescGZIP(), []int{11}
1419+ return file_exec_proto_rawDescGZIP(), []int{12}
13191420 }
13201421
13211422 func (x *StepSummary) GetStep() int32 {
@@ -1347,7 +1448,7 @@ type Log struct {
13471448
13481449 func (x *Log) Reset() {
13491450 *x = Log{}
1350- mi := &file_exec_proto_msgTypes[12]
1451+ mi := &file_exec_proto_msgTypes[13]
13511452 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13521453 ms.StoreMessageInfo(mi)
13531454 }
@@ -1359,7 +1460,7 @@ func (x *Log) String() string {
13591460 func (*Log) ProtoMessage() {}
13601461
13611462 func (x *Log) ProtoReflect() protoreflect.Message {
1362- mi := &file_exec_proto_msgTypes[12]
1463+ mi := &file_exec_proto_msgTypes[13]
13631464 if x != nil {
13641465 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13651466 if ms.LoadMessageInfo() == nil {
@@ -1372,7 +1473,7 @@ func (x *Log) ProtoReflect() protoreflect.Message {
13721473
13731474 // Deprecated: Use Log.ProtoReflect.Descriptor instead.
13741475 func (*Log) Descriptor() ([]byte, []int) {
1375- return file_exec_proto_rawDescGZIP(), []int{12}
1476+ return file_exec_proto_rawDescGZIP(), []int{13}
13761477 }
13771478
13781479 func (x *Log) GetStep() int32 {
@@ -1416,7 +1517,7 @@ type JobResult struct {
14161517
14171518 func (x *JobResult) Reset() {
14181519 *x = JobResult{}
1419- mi := &file_exec_proto_msgTypes[13]
1520+ mi := &file_exec_proto_msgTypes[14]
14201521 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14211522 ms.StoreMessageInfo(mi)
14221523 }
@@ -1428,7 +1529,7 @@ func (x *JobResult) String() string {
14281529 func (*JobResult) ProtoMessage() {}
14291530
14301531 func (x *JobResult) ProtoReflect() protoreflect.Message {
1431- mi := &file_exec_proto_msgTypes[13]
1532+ mi := &file_exec_proto_msgTypes[14]
14321533 if x != nil {
14331534 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14341535 if ms.LoadMessageInfo() == nil {
@@ -1441,7 +1542,7 @@ func (x *JobResult) ProtoReflect() protoreflect.Message {
14411542
14421543 // Deprecated: Use JobResult.ProtoReflect.Descriptor instead.
14431544 func (*JobResult) Descriptor() ([]byte, []int) {
1444- return file_exec_proto_rawDescGZIP(), []int{13}
1545+ return file_exec_proto_rawDescGZIP(), []int{14}
14451546 }
14461547
14471548 func (x *JobResult) GetConclusion() StepConclusion {
@@ -1486,7 +1587,7 @@ type ResolveRequest struct {
14861587
14871588 func (x *ResolveRequest) Reset() {
14881589 *x = ResolveRequest{}
1489- mi := &file_exec_proto_msgTypes[14]
1590+ mi := &file_exec_proto_msgTypes[15]
14901591 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
14911592 ms.StoreMessageInfo(mi)
14921593 }
@@ -1498,7 +1599,7 @@ func (x *ResolveRequest) String() string {
14981599 func (*ResolveRequest) ProtoMessage() {}
14991600
15001601 func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
1501- mi := &file_exec_proto_msgTypes[14]
1602+ mi := &file_exec_proto_msgTypes[15]
15021603 if x != nil {
15031604 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15041605 if ms.LoadMessageInfo() == nil {
@@ -1511,7 +1612,7 @@ func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
15111612
15121613 // Deprecated: Use ResolveRequest.ProtoReflect.Descriptor instead.
15131614 func (*ResolveRequest) Descriptor() ([]byte, []int) {
1514- return file_exec_proto_rawDescGZIP(), []int{14}
1615+ return file_exec_proto_rawDescGZIP(), []int{15}
15151616 }
15161617
15171618 func (x *ResolveRequest) GetId() int64 {
@@ -1541,7 +1642,7 @@ type ResolveResponse struct {
15411642
15421643 func (x *ResolveResponse) Reset() {
15431644 *x = ResolveResponse{}
1544- mi := &file_exec_proto_msgTypes[15]
1645+ mi := &file_exec_proto_msgTypes[16]
15451646 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15461647 ms.StoreMessageInfo(mi)
15471648 }
@@ -1553,7 +1654,7 @@ func (x *ResolveResponse) String() string {
15531654 func (*ResolveResponse) ProtoMessage() {}
15541655
15551656 func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
1556- mi := &file_exec_proto_msgTypes[15]
1657+ mi := &file_exec_proto_msgTypes[16]
15571658 if x != nil {
15581659 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
15591660 if ms.LoadMessageInfo() == nil {
@@ -1566,7 +1667,7 @@ func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
15661667
15671668 // Deprecated: Use ResolveResponse.ProtoReflect.Descriptor instead.
15681669 func (*ResolveResponse) Descriptor() ([]byte, []int) {
1569- return file_exec_proto_rawDescGZIP(), []int{15}
1670+ return file_exec_proto_rawDescGZIP(), []int{16}
15701671 }
15711672
15721673 func (x *ResolveResponse) GetId() int64 {
@@ -1613,7 +1714,7 @@ type ResolvedAction struct {
16131714
16141715 func (x *ResolvedAction) Reset() {
16151716 *x = ResolvedAction{}
1616- mi := &file_exec_proto_msgTypes[16]
1717+ mi := &file_exec_proto_msgTypes[17]
16171718 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16181719 ms.StoreMessageInfo(mi)
16191720 }
@@ -1625,7 +1726,7 @@ func (x *ResolvedAction) String() string {
16251726 func (*ResolvedAction) ProtoMessage() {}
16261727
16271728 func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
1628- mi := &file_exec_proto_msgTypes[16]
1729+ mi := &file_exec_proto_msgTypes[17]
16291730 if x != nil {
16301731 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
16311732 if ms.LoadMessageInfo() == nil {
@@ -1638,7 +1739,7 @@ func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
16381739
16391740 // Deprecated: Use ResolvedAction.ProtoReflect.Descriptor instead.
16401741 func (*ResolvedAction) Descriptor() ([]byte, []int) {
1641- return file_exec_proto_rawDescGZIP(), []int{16}
1742+ return file_exec_proto_rawDescGZIP(), []int{17}
16421743 }
16431744
16441745 func (x *ResolvedAction) GetRef() string {
@@ -1709,7 +1810,7 @@ var File_exec_proto protoreflect.FileDescriptor
17091810 const file_exec_proto_rawDesc = "" +
17101811 "\n" +
17111812 "\n" +
1712- "exec.proto\x12\x15ermite.runner.exec.v1\"\xea\x06\n" +
1813+ "exec.proto\x12\x15ermite.runner.exec.v1\"\xde\a\n" +
17131814 "\x05Frame\x124\n" +
17141815 "\x05hello\x18\x01 \x01(\v2\x1c.ermite.runner.exec.v1.HelloH\x00R\x05hello\x12.\n" +
17151816 "\x03run\x18\x02 \x01(\v2\x1a.ermite.runner.exec.v1.RunH\x00R\x03run\x127\n" +
@@ -1725,8 +1826,14 @@ const file_exec_proto_rawDesc = "" +
17251826 " \x01(\v2&.ermite.runner.exec.v1.StepAnnotationsH\x00R\x0fstepAnnotations\x12G\n" +
17261827 "\fstep_summary\x18\v \x01(\v2\".ermite.runner.exec.v1.StepSummaryH\x00R\vstepSummary\x12P\n" +
17271828 "\x0fresolve_request\x18\f \x01(\v2%.ermite.runner.exec.v1.ResolveRequestH\x00R\x0eresolveRequest\x12S\n" +
1728- "\x10resolve_response\x18\r \x01(\v2&.ermite.runner.exec.v1.ResolveResponseH\x00R\x0fresolveResponseB\x06\n" +
1729- "\x04body\"W\n" +
1829+ "\x10resolve_response\x18\r \x01(\v2&.ermite.runner.exec.v1.ResolveResponseH\x00R\x0fresolveResponse\x12;\n" +
1830+ "\x06secret\x18\x0e \x01(\v2!.ermite.runner.exec.v1.NamedValueH\x00R\x06secret\x125\n" +
1831+ "\x03var\x18\x0f \x01(\v2!.ermite.runner.exec.v1.NamedValueH\x00R\x03varB\x06\n" +
1832+ "\x04body\"6\n" +
1833+ "\n" +
1834+ "NamedValue\x12\x12\n" +
1835+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
1836+ "\x05value\x18\x02 \x01(\tR\x05value\"W\n" +
17301837 "\x05Hello\x12\x18\n" +
17311838 "\aversion\x18\x01 \x01(\tR\aversion\x12\x18\n" +
17321839 "\asandbox\x18\x02 \x01(\tR\asandbox\x12\x1a\n" +
@@ -1737,7 +1844,7 @@ const file_exec_proto_rawDesc = "" +
17371844 "\x03job\x18\x03 \x01(\v2\x1e.ermite.runner.exec.v1.JobSpecR\x03job\x1a6\n" +
17381845 "\bEnvEntry\x12\x10\n" +
17391846 "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1740- "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x9d\x04\n" +
1847+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe3\x04\n" +
17411848 "\aJobSpec\x12#\n" +
17421849 "\rworkflow_path\x18\x01 \x01(\tR\fworkflowPath\x12#\n" +
17431850 "\rworkflow_yaml\x18\x02 \x01(\tR\fworkflowYaml\x12\x17\n" +
@@ -1759,7 +1866,8 @@ const file_exec_proto_rawDesc = "" +
17591866 "\n" +
17601867 "cache_port\x18\x0f \x01(\rR\tcachePort\x12\x1f\n" +
17611868 "\vcache_token\x18\x10 \x01(\tR\n" +
1762- "cacheToken\"t\n" +
1869+ "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" +
17631871 "\x06Output\x129\n" +
17641872 "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +
17651873 "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +
@@ -1871,62 +1979,65 @@ func file_exec_proto_rawDescGZIP() []byte {
18711979 }
18721980
18731981 var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
1874-var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
1982+var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
18751983 var file_exec_proto_goTypes = []any{
18761984 (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind
18771985 (CancelKind)(0), // 1: ermite.runner.exec.v1.CancelKind
18781986 (AnnotationLevel)(0), // 2: ermite.runner.exec.v1.AnnotationLevel
18791987 (StepConclusion)(0), // 3: ermite.runner.exec.v1.StepConclusion
18801988 (*Frame)(nil), // 4: ermite.runner.exec.v1.Frame
1881- (*Hello)(nil), // 5: ermite.runner.exec.v1.Hello
1882- (*Run)(nil), // 6: ermite.runner.exec.v1.Run
1883- (*JobSpec)(nil), // 7: ermite.runner.exec.v1.JobSpec
1884- (*Output)(nil), // 8: ermite.runner.exec.v1.Output
1885- (*Exit)(nil), // 9: ermite.runner.exec.v1.Exit
1886- (*Cancel)(nil), // 10: ermite.runner.exec.v1.Cancel
1887- (*StepStarted)(nil), // 11: ermite.runner.exec.v1.StepStarted
1888- (*StepCompleted)(nil), // 12: ermite.runner.exec.v1.StepCompleted
1889- (*Annotation)(nil), // 13: ermite.runner.exec.v1.Annotation
1890- (*StepAnnotations)(nil), // 14: ermite.runner.exec.v1.StepAnnotations
1891- (*StepSummary)(nil), // 15: ermite.runner.exec.v1.StepSummary
1892- (*Log)(nil), // 16: ermite.runner.exec.v1.Log
1893- (*JobResult)(nil), // 17: ermite.runner.exec.v1.JobResult
1894- (*ResolveRequest)(nil), // 18: ermite.runner.exec.v1.ResolveRequest
1895- (*ResolveResponse)(nil), // 19: ermite.runner.exec.v1.ResolveResponse
1896- (*ResolvedAction)(nil), // 20: ermite.runner.exec.v1.ResolvedAction
1897- nil, // 21: ermite.runner.exec.v1.Run.EnvEntry
1898- nil, // 22: ermite.runner.exec.v1.JobResult.OutputsEntry
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
18992008 }
19002009 var file_exec_proto_depIdxs = []int32{
1901- 5, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello
1902- 6, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run
1903- 8, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output
1904- 9, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit
1905- 10, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel
1906- 11, // 5: ermite.runner.exec.v1.Frame.step_started:type_name -> ermite.runner.exec.v1.StepStarted
1907- 12, // 6: ermite.runner.exec.v1.Frame.step_completed:type_name -> ermite.runner.exec.v1.StepCompleted
1908- 16, // 7: ermite.runner.exec.v1.Frame.log:type_name -> ermite.runner.exec.v1.Log
1909- 17, // 8: ermite.runner.exec.v1.Frame.job_result:type_name -> ermite.runner.exec.v1.JobResult
1910- 14, // 9: ermite.runner.exec.v1.Frame.step_annotations:type_name -> ermite.runner.exec.v1.StepAnnotations
1911- 15, // 10: ermite.runner.exec.v1.Frame.step_summary:type_name -> ermite.runner.exec.v1.StepSummary
1912- 18, // 11: ermite.runner.exec.v1.Frame.resolve_request:type_name -> ermite.runner.exec.v1.ResolveRequest
1913- 19, // 12: ermite.runner.exec.v1.Frame.resolve_response:type_name -> ermite.runner.exec.v1.ResolveResponse
1914- 21, // 13: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry
1915- 7, // 14: ermite.runner.exec.v1.Run.job:type_name -> ermite.runner.exec.v1.JobSpec
1916- 0, // 15: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind
1917- 1, // 16: ermite.runner.exec.v1.Cancel.kind:type_name -> ermite.runner.exec.v1.CancelKind
1918- 3, // 17: ermite.runner.exec.v1.StepCompleted.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
1919- 3, // 18: ermite.runner.exec.v1.StepCompleted.outcome:type_name -> ermite.runner.exec.v1.StepConclusion
1920- 2, // 19: ermite.runner.exec.v1.Annotation.level:type_name -> ermite.runner.exec.v1.AnnotationLevel
1921- 13, // 20: ermite.runner.exec.v1.StepAnnotations.annotations:type_name -> ermite.runner.exec.v1.Annotation
1922- 3, // 21: ermite.runner.exec.v1.JobResult.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
1923- 22, // 22: ermite.runner.exec.v1.JobResult.outputs:type_name -> ermite.runner.exec.v1.JobResult.OutputsEntry
1924- 20, // 23: ermite.runner.exec.v1.ResolveResponse.actions:type_name -> ermite.runner.exec.v1.ResolvedAction
1925- 24, // [24:24] is the sub-list for method output_type
1926- 24, // [24:24] is the sub-list for method input_type
1927- 24, // [24:24] is the sub-list for extension type_name
1928- 24, // [24:24] is the sub-list for extension extendee
1929- 0, // [0:24] is the sub-list for field type_name
2010+ 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.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
2023+ 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.NamedValue
2025+ 22, // 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.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
19302041 }
19312042
19322043 func init() { file_exec_proto_init() }
@@ -1948,6 +2059,8 @@ func file_exec_proto_init() {
19482059 (*Frame_StepSummary)(nil),
19492060 (*Frame_ResolveRequest)(nil),
19502061 (*Frame_ResolveResponse)(nil),
2062+ (*Frame_Secret)(nil),
2063+ (*Frame_Var)(nil),
19512064 }
19522065 type x struct{}
19532066 out := protoimpl.TypeBuilder{
@@ -1955,7 +2068,7 @@ func file_exec_proto_init() {
19552068 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
19562069 RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),
19572070 NumEnums: 4,
1958- NumMessages: 19,
2071+ NumMessages: 20,
19592072 NumExtensions: 0,
19602073 NumServices: 0,
19612074 },
@@ -259,6 +259,8 @@ type Frame struct {
259 // *Frame_StepSummary259 // *Frame_StepSummary
260 // *Frame_ResolveRequest260 // *Frame_ResolveRequest
261 // *Frame_ResolveResponse261 // *Frame_ResolveResponse
262+ // *Frame_Secret
263+ // *Frame_Var
262 Body isFrame_Body `protobuf_oneof:"body"`264 Body isFrame_Body `protobuf_oneof:"body"`
263 unknownFields protoimpl.UnknownFields265 unknownFields protoimpl.UnknownFields
264 sizeCache protoimpl.SizeCache266 sizeCache protoimpl.SizeCache
@@ -418,6 +420,24 @@ func (x *Frame) GetResolveResponse() *ResolveResponse {
418 return nil420 return nil
419 }421 }
420 422
423+func (x *Frame) GetSecret() *NamedValue {
424+ if x != nil {
425+ if x, ok := x.Body.(*Frame_Secret); ok {
426+ return x.Secret
427+ }
428+ }
429+ return nil
430+}
431+
432+func (x *Frame) GetVar() *NamedValue {
433+ if x != nil {
434+ if x, ok := x.Body.(*Frame_Var); ok {
435+ return x.Var
436+ }
437+ }
438+ return nil
439+}
440+
421 type isFrame_Body interface {441 type isFrame_Body interface {
422 isFrame_Body()442 isFrame_Body()
423 }443 }
@@ -474,6 +494,14 @@ type Frame_ResolveResponse struct {
474 ResolveResponse *ResolveResponse `protobuf:"bytes,13,opt,name=resolve_response,json=resolveResponse,proto3,oneof"` // host -> executor, answers a ResolveRequest494 ResolveResponse *ResolveResponse `protobuf:"bytes,13,opt,name=resolve_response,json=resolveResponse,proto3,oneof"` // host -> executor, answers a ResolveRequest
475 }495 }
476 496
497+type Frame_Secret struct {
498+ Secret *NamedValue `protobuf:"bytes,14,opt,name=secret,proto3,oneof"` // host -> executor, one per secret, before Run
499+}
500+
501+type Frame_Var struct {
502+ Var *NamedValue `protobuf:"bytes,15,opt,name=var,proto3,oneof"` // host -> executor, one per variable, before Run
503+}
504+
477 func (*Frame_Hello) isFrame_Body() {}505 func (*Frame_Hello) isFrame_Body() {}
478 506
479 func (*Frame_Run) isFrame_Body() {}507 func (*Frame_Run) isFrame_Body() {}
@@ -500,6 +528,67 @@ func (*Frame_ResolveRequest) isFrame_Body() {}
500 528
501 func (*Frame_ResolveResponse) isFrame_Body() {}529 func (*Frame_ResolveResponse) isFrame_Body() {}
502 530
531+func (*Frame_Secret) isFrame_Body() {}
532+
533+func (*Frame_Var) isFrame_Body() {}
534+
535+// NamedValue is one secret (Job.secrets) or one variable (Job.vars) of the job.
536+// They travel in frames of their own, before Run, rather than in JobSpec: a
537+// frame is at most 2 MiB (execstream.MaxFrame) and ermite allows many secrets of
538+// 48 KiB, so a map in Run could not carry them. The executor keeps them until
539+// Run arrives. Secrets are never logged nor written to disk.
540+type NamedValue struct {
541+ state protoimpl.MessageState `protogen:"open.v1"`
542+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
543+ Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
544+ unknownFields protoimpl.UnknownFields
545+ sizeCache protoimpl.SizeCache
546+}
547+
548+func (x *NamedValue) Reset() {
549+ *x = NamedValue{}
550+ mi := &file_exec_proto_msgTypes[1]
551+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
552+ ms.StoreMessageInfo(mi)
553+}
554+
555+func (x *NamedValue) String() string {
556+ return protoimpl.X.MessageStringOf(x)
557+}
558+
559+func (*NamedValue) ProtoMessage() {}
560+
561+func (x *NamedValue) ProtoReflect() protoreflect.Message {
562+ mi := &file_exec_proto_msgTypes[1]
563+ if x != nil {
564+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
565+ if ms.LoadMessageInfo() == nil {
566+ ms.StoreMessageInfo(mi)
567+ }
568+ return ms
569+ }
570+ return mi.MessageOf(x)
571+}
572+
573+// Deprecated: Use NamedValue.ProtoReflect.Descriptor instead.
574+func (*NamedValue) Descriptor() ([]byte, []int) {
575+ return file_exec_proto_rawDescGZIP(), []int{1}
576+}
577+
578+func (x *NamedValue) GetName() string {
579+ if x != nil {
580+ return x.Name
581+ }
582+ return ""
583+}
584+
585+func (x *NamedValue) GetValue() string {
586+ if x != nil {
587+ return x.Value
588+ }
589+ return ""
590+}
591+
503 type Hello struct {592 type Hello struct {
504 state protoimpl.MessageState `protogen:"open.v1"`593 state protoimpl.MessageState `protogen:"open.v1"`
505 Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` // runner version of the executor594 Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` // runner version of the executor
@@ -511,7 +600,7 @@ type Hello struct {
511 600
512 func (x *Hello) Reset() {601 func (x *Hello) Reset() {
513 *x = Hello{}602 *x = Hello{}
514- mi := &file_exec_proto_msgTypes[1]603+ mi := &file_exec_proto_msgTypes[2]
515 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))604 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
516 ms.StoreMessageInfo(mi)605 ms.StoreMessageInfo(mi)
517 }606 }
@@ -523,7 +612,7 @@ func (x *Hello) String() string {
523 func (*Hello) ProtoMessage() {}612 func (*Hello) ProtoMessage() {}
524 613
525 func (x *Hello) ProtoReflect() protoreflect.Message {614 func (x *Hello) ProtoReflect() protoreflect.Message {
526- mi := &file_exec_proto_msgTypes[1]615+ mi := &file_exec_proto_msgTypes[2]
527 if x != nil {616 if x != nil {
528 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))617 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
529 if ms.LoadMessageInfo() == nil {618 if ms.LoadMessageInfo() == nil {
@@ -536,7 +625,7 @@ func (x *Hello) ProtoReflect() protoreflect.Message {
536 625
537 // Deprecated: Use Hello.ProtoReflect.Descriptor instead.626 // Deprecated: Use Hello.ProtoReflect.Descriptor instead.
538 func (*Hello) Descriptor() ([]byte, []int) {627 func (*Hello) Descriptor() ([]byte, []int) {
539- return file_exec_proto_rawDescGZIP(), []int{1}628+ return file_exec_proto_rawDescGZIP(), []int{2}
540 }629 }
541 630
542 func (x *Hello) GetVersion() string {631 func (x *Hello) GetVersion() string {
@@ -576,7 +665,7 @@ type Run struct {
576 665
577 func (x *Run) Reset() {666 func (x *Run) Reset() {
578 *x = Run{}667 *x = Run{}
579- mi := &file_exec_proto_msgTypes[2]668+ mi := &file_exec_proto_msgTypes[3]
580 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))669 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
581 ms.StoreMessageInfo(mi)670 ms.StoreMessageInfo(mi)
582 }671 }
@@ -588,7 +677,7 @@ func (x *Run) String() string {
588 func (*Run) ProtoMessage() {}677 func (*Run) ProtoMessage() {}
589 678
590 func (x *Run) ProtoReflect() protoreflect.Message {679 func (x *Run) ProtoReflect() protoreflect.Message {
591- mi := &file_exec_proto_msgTypes[2]680+ mi := &file_exec_proto_msgTypes[3]
592 if x != nil {681 if x != nil {
593 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))682 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
594 if ms.LoadMessageInfo() == nil {683 if ms.LoadMessageInfo() == nil {
@@ -601,7 +690,7 @@ func (x *Run) ProtoReflect() protoreflect.Message {
601 690
602 // Deprecated: Use Run.ProtoReflect.Descriptor instead.691 // Deprecated: Use Run.ProtoReflect.Descriptor instead.
603 func (*Run) Descriptor() ([]byte, []int) {692 func (*Run) Descriptor() ([]byte, []int) {
604- return file_exec_proto_rawDescGZIP(), []int{2}693+ return file_exec_proto_rawDescGZIP(), []int{3}
605 }694 }
606 695
607 func (x *Run) GetScript() string {696 func (x *Run) GetScript() string {
@@ -660,15 +749,20 @@ type JobSpec struct {
660 // runner does, and masks the token. The token is not an ermite credential: it749 // runner does, and masks the token. The token is not an ermite credential: it
661 // is an HMAC that only the runner's cache service accepts, scoped to this750 // is an HMAC that only the runner's cache service accepts, scoped to this
662 // job's repository and refs.751 // job's repository and refs.
663- CachePort uint32 `protobuf:"varint,15,opt,name=cache_port,json=cachePort,proto3" json:"cache_port,omitempty"`752+ CachePort uint32 `protobuf:"varint,15,opt,name=cache_port,json=cachePort,proto3" json:"cache_port,omitempty"`
664- CacheToken string `protobuf:"bytes,16,opt,name=cache_token,json=cacheToken,proto3" json:"cache_token,omitempty"`753+ CacheToken string `protobuf:"bytes,16,opt,name=cache_token,json=cacheToken,proto3" json:"cache_token,omitempty"`
665- unknownFields protoimpl.UnknownFields754+ // secrets_withheld is set by the host when the server sends Job.trusted and
666- sizeCache protoimpl.SizeCache755+ // the job is not trusted (feature ci.secrets): "Set up job" then says that
756+ // secrets are not available. Never set for servers without the feature, for
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"`
759+ unknownFields protoimpl.UnknownFields
760+ sizeCache protoimpl.SizeCache
667 }761 }
668 762
669 func (x *JobSpec) Reset() {763 func (x *JobSpec) Reset() {
670 *x = JobSpec{}764 *x = JobSpec{}
671- mi := &file_exec_proto_msgTypes[3]765+ mi := &file_exec_proto_msgTypes[4]
672 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))766 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
673 ms.StoreMessageInfo(mi)767 ms.StoreMessageInfo(mi)
674 }768 }
@@ -680,7 +774,7 @@ func (x *JobSpec) String() string {
680 func (*JobSpec) ProtoMessage() {}774 func (*JobSpec) ProtoMessage() {}
681 775
682 func (x *JobSpec) ProtoReflect() protoreflect.Message {776 func (x *JobSpec) ProtoReflect() protoreflect.Message {
683- mi := &file_exec_proto_msgTypes[3]777+ mi := &file_exec_proto_msgTypes[4]
684 if x != nil {778 if x != nil {
685 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))779 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
686 if ms.LoadMessageInfo() == nil {780 if ms.LoadMessageInfo() == nil {
@@ -693,7 +787,7 @@ func (x *JobSpec) ProtoReflect() protoreflect.Message {
693 787
694 // Deprecated: Use JobSpec.ProtoReflect.Descriptor instead.788 // Deprecated: Use JobSpec.ProtoReflect.Descriptor instead.
695 func (*JobSpec) Descriptor() ([]byte, []int) {789 func (*JobSpec) Descriptor() ([]byte, []int) {
696- return file_exec_proto_rawDescGZIP(), []int{3}790+ return file_exec_proto_rawDescGZIP(), []int{4}
697 }791 }
698 792
699 func (x *JobSpec) GetWorkflowPath() string {793 func (x *JobSpec) GetWorkflowPath() string {
@@ -808,6 +902,13 @@ func (x *JobSpec) GetCacheToken() string {
808 return ""902 return ""
809 }903 }
810 904
905+func (x *JobSpec) GetSecretsWithheld() bool {
906+ if x != nil {
907+ return x.SecretsWithheld
908+ }
909+ return false
910+}
911+
811 type Output struct {912 type Output struct {
812 state protoimpl.MessageState `protogen:"open.v1"`913 state protoimpl.MessageState `protogen:"open.v1"`
813 Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`914 Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`
@@ -819,7 +920,7 @@ type Output struct {
819 920
820 func (x *Output) Reset() {921 func (x *Output) Reset() {
821 *x = Output{}922 *x = Output{}
822- mi := &file_exec_proto_msgTypes[4]923+ mi := &file_exec_proto_msgTypes[5]
823 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))924 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
824 ms.StoreMessageInfo(mi)925 ms.StoreMessageInfo(mi)
825 }926 }
@@ -831,7 +932,7 @@ func (x *Output) String() string {
831 func (*Output) ProtoMessage() {}932 func (*Output) ProtoMessage() {}
832 933
833 func (x *Output) ProtoReflect() protoreflect.Message {934 func (x *Output) ProtoReflect() protoreflect.Message {
834- mi := &file_exec_proto_msgTypes[4]935+ mi := &file_exec_proto_msgTypes[5]
835 if x != nil {936 if x != nil {
836 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))937 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
837 if ms.LoadMessageInfo() == nil {938 if ms.LoadMessageInfo() == nil {
@@ -844,7 +945,7 @@ func (x *Output) ProtoReflect() protoreflect.Message {
844 945
845 // Deprecated: Use Output.ProtoReflect.Descriptor instead.946 // Deprecated: Use Output.ProtoReflect.Descriptor instead.
846 func (*Output) Descriptor() ([]byte, []int) {947 func (*Output) Descriptor() ([]byte, []int) {
847- return file_exec_proto_rawDescGZIP(), []int{4}948+ return file_exec_proto_rawDescGZIP(), []int{5}
848 }949 }
849 950
850 func (x *Output) GetStream() StreamKind {951 func (x *Output) GetStream() StreamKind {
@@ -878,7 +979,7 @@ type Exit struct {
878 979
879 func (x *Exit) Reset() {980 func (x *Exit) Reset() {
880 *x = Exit{}981 *x = Exit{}
881- mi := &file_exec_proto_msgTypes[5]982+ mi := &file_exec_proto_msgTypes[6]
882 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))983 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
883 ms.StoreMessageInfo(mi)984 ms.StoreMessageInfo(mi)
884 }985 }
@@ -890,7 +991,7 @@ func (x *Exit) String() string {
890 func (*Exit) ProtoMessage() {}991 func (*Exit) ProtoMessage() {}
891 992
892 func (x *Exit) ProtoReflect() protoreflect.Message {993 func (x *Exit) ProtoReflect() protoreflect.Message {
893- mi := &file_exec_proto_msgTypes[5]994+ mi := &file_exec_proto_msgTypes[6]
894 if x != nil {995 if x != nil {
895 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))996 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
896 if ms.LoadMessageInfo() == nil {997 if ms.LoadMessageInfo() == nil {
@@ -903,7 +1004,7 @@ func (x *Exit) ProtoReflect() protoreflect.Message {
903 1004
904 // Deprecated: Use Exit.ProtoReflect.Descriptor instead.1005 // Deprecated: Use Exit.ProtoReflect.Descriptor instead.
905 func (*Exit) Descriptor() ([]byte, []int) {1006 func (*Exit) Descriptor() ([]byte, []int) {
906- return file_exec_proto_rawDescGZIP(), []int{5}1007+ return file_exec_proto_rawDescGZIP(), []int{6}
907 }1008 }
908 1009
909 func (x *Exit) GetCode() int32 {1010 func (x *Exit) GetCode() int32 {
@@ -937,7 +1038,7 @@ type Cancel struct {
937 1038
938 func (x *Cancel) Reset() {1039 func (x *Cancel) Reset() {
939 *x = Cancel{}1040 *x = Cancel{}
940- mi := &file_exec_proto_msgTypes[6]1041+ mi := &file_exec_proto_msgTypes[7]
941 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1042 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
942 ms.StoreMessageInfo(mi)1043 ms.StoreMessageInfo(mi)
943 }1044 }
@@ -949,7 +1050,7 @@ func (x *Cancel) String() string {
949 func (*Cancel) ProtoMessage() {}1050 func (*Cancel) ProtoMessage() {}
950 1051
951 func (x *Cancel) ProtoReflect() protoreflect.Message {1052 func (x *Cancel) ProtoReflect() protoreflect.Message {
952- mi := &file_exec_proto_msgTypes[6]1053+ mi := &file_exec_proto_msgTypes[7]
953 if x != nil {1054 if x != nil {
954 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1055 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
955 if ms.LoadMessageInfo() == nil {1056 if ms.LoadMessageInfo() == nil {
@@ -962,7 +1063,7 @@ func (x *Cancel) ProtoReflect() protoreflect.Message {
962 1063
963 // Deprecated: Use Cancel.ProtoReflect.Descriptor instead.1064 // Deprecated: Use Cancel.ProtoReflect.Descriptor instead.
964 func (*Cancel) Descriptor() ([]byte, []int) {1065 func (*Cancel) Descriptor() ([]byte, []int) {
965- return file_exec_proto_rawDescGZIP(), []int{6}1066+ return file_exec_proto_rawDescGZIP(), []int{7}
966 }1067 }
967 1068
968 func (x *Cancel) GetReason() string {1069 func (x *Cancel) GetReason() string {
@@ -992,7 +1093,7 @@ type StepStarted struct {
992 1093
993 func (x *StepStarted) Reset() {1094 func (x *StepStarted) Reset() {
994 *x = StepStarted{}1095 *x = StepStarted{}
995- mi := &file_exec_proto_msgTypes[7]1096+ mi := &file_exec_proto_msgTypes[8]
996 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1097 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
997 ms.StoreMessageInfo(mi)1098 ms.StoreMessageInfo(mi)
998 }1099 }
@@ -1004,7 +1105,7 @@ func (x *StepStarted) String() string {
1004 func (*StepStarted) ProtoMessage() {}1105 func (*StepStarted) ProtoMessage() {}
1005 1106
1006 func (x *StepStarted) ProtoReflect() protoreflect.Message {1107 func (x *StepStarted) ProtoReflect() protoreflect.Message {
1007- mi := &file_exec_proto_msgTypes[7]1108+ mi := &file_exec_proto_msgTypes[8]
1008 if x != nil {1109 if x != nil {
1009 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1110 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1010 if ms.LoadMessageInfo() == nil {1111 if ms.LoadMessageInfo() == nil {
@@ -1017,7 +1118,7 @@ func (x *StepStarted) ProtoReflect() protoreflect.Message {
1017 1118
1018 // Deprecated: Use StepStarted.ProtoReflect.Descriptor instead.1119 // Deprecated: Use StepStarted.ProtoReflect.Descriptor instead.
1019 func (*StepStarted) Descriptor() ([]byte, []int) {1120 func (*StepStarted) Descriptor() ([]byte, []int) {
1020- return file_exec_proto_rawDescGZIP(), []int{7}1121+ return file_exec_proto_rawDescGZIP(), []int{8}
1021 }1122 }
1022 1123
1023 func (x *StepStarted) GetNumber() int32 {1124 func (x *StepStarted) GetNumber() int32 {
@@ -1057,7 +1158,7 @@ type StepCompleted struct {
1057 1158
1058 func (x *StepCompleted) Reset() {1159 func (x *StepCompleted) Reset() {
1059 *x = StepCompleted{}1160 *x = StepCompleted{}
1060- mi := &file_exec_proto_msgTypes[8]1161+ mi := &file_exec_proto_msgTypes[9]
1061 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1162 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1062 ms.StoreMessageInfo(mi)1163 ms.StoreMessageInfo(mi)
1063 }1164 }
@@ -1069,7 +1170,7 @@ func (x *StepCompleted) String() string {
1069 func (*StepCompleted) ProtoMessage() {}1170 func (*StepCompleted) ProtoMessage() {}
1070 1171
1071 func (x *StepCompleted) ProtoReflect() protoreflect.Message {1172 func (x *StepCompleted) ProtoReflect() protoreflect.Message {
1072- mi := &file_exec_proto_msgTypes[8]1173+ mi := &file_exec_proto_msgTypes[9]
1073 if x != nil {1174 if x != nil {
1074 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1175 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1075 if ms.LoadMessageInfo() == nil {1176 if ms.LoadMessageInfo() == nil {
@@ -1082,7 +1183,7 @@ func (x *StepCompleted) ProtoReflect() protoreflect.Message {
1082 1183
1083 // Deprecated: Use StepCompleted.ProtoReflect.Descriptor instead.1184 // Deprecated: Use StepCompleted.ProtoReflect.Descriptor instead.
1084 func (*StepCompleted) Descriptor() ([]byte, []int) {1185 func (*StepCompleted) Descriptor() ([]byte, []int) {
1085- return file_exec_proto_rawDescGZIP(), []int{8}1186+ return file_exec_proto_rawDescGZIP(), []int{9}
1086 }1187 }
1087 1188
1088 func (x *StepCompleted) GetNumber() int32 {1189 func (x *StepCompleted) GetNumber() int32 {
@@ -1138,7 +1239,7 @@ type Annotation struct {
1138 1239
1139 func (x *Annotation) Reset() {1240 func (x *Annotation) Reset() {
1140 *x = Annotation{}1241 *x = Annotation{}
1141- mi := &file_exec_proto_msgTypes[9]1242+ mi := &file_exec_proto_msgTypes[10]
1142 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1243 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1143 ms.StoreMessageInfo(mi)1244 ms.StoreMessageInfo(mi)
1144 }1245 }
@@ -1150,7 +1251,7 @@ func (x *Annotation) String() string {
1150 func (*Annotation) ProtoMessage() {}1251 func (*Annotation) ProtoMessage() {}
1151 1252
1152 func (x *Annotation) ProtoReflect() protoreflect.Message {1253 func (x *Annotation) ProtoReflect() protoreflect.Message {
1153- mi := &file_exec_proto_msgTypes[9]1254+ mi := &file_exec_proto_msgTypes[10]
1154 if x != nil {1255 if x != nil {
1155 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1256 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1156 if ms.LoadMessageInfo() == nil {1257 if ms.LoadMessageInfo() == nil {
@@ -1163,7 +1264,7 @@ func (x *Annotation) ProtoReflect() protoreflect.Message {
1163 1264
1164 // Deprecated: Use Annotation.ProtoReflect.Descriptor instead.1265 // Deprecated: Use Annotation.ProtoReflect.Descriptor instead.
1165 func (*Annotation) Descriptor() ([]byte, []int) {1266 func (*Annotation) Descriptor() ([]byte, []int) {
1166- return file_exec_proto_rawDescGZIP(), []int{9}1267+ return file_exec_proto_rawDescGZIP(), []int{10}
1167 }1268 }
1168 1269
1169 func (x *Annotation) GetLevel() AnnotationLevel {1270 func (x *Annotation) GetLevel() AnnotationLevel {
@@ -1236,7 +1337,7 @@ type StepAnnotations struct {
1236 1337
1237 func (x *StepAnnotations) Reset() {1338 func (x *StepAnnotations) Reset() {
1238 *x = StepAnnotations{}1339 *x = StepAnnotations{}
1239- mi := &file_exec_proto_msgTypes[10]1340+ mi := &file_exec_proto_msgTypes[11]
1240 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1341 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1241 ms.StoreMessageInfo(mi)1342 ms.StoreMessageInfo(mi)
1242 }1343 }
@@ -1248,7 +1349,7 @@ func (x *StepAnnotations) String() string {
1248 func (*StepAnnotations) ProtoMessage() {}1349 func (*StepAnnotations) ProtoMessage() {}
1249 1350
1250 func (x *StepAnnotations) ProtoReflect() protoreflect.Message {1351 func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
1251- mi := &file_exec_proto_msgTypes[10]1352+ mi := &file_exec_proto_msgTypes[11]
1252 if x != nil {1353 if x != nil {
1253 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1354 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1254 if ms.LoadMessageInfo() == nil {1355 if ms.LoadMessageInfo() == nil {
@@ -1261,7 +1362,7 @@ func (x *StepAnnotations) ProtoReflect() protoreflect.Message {
1261 1362
1262 // Deprecated: Use StepAnnotations.ProtoReflect.Descriptor instead.1363 // Deprecated: Use StepAnnotations.ProtoReflect.Descriptor instead.
1263 func (*StepAnnotations) Descriptor() ([]byte, []int) {1364 func (*StepAnnotations) Descriptor() ([]byte, []int) {
1264- return file_exec_proto_rawDescGZIP(), []int{10}1365+ return file_exec_proto_rawDescGZIP(), []int{11}
1265 }1366 }
1266 1367
1267 func (x *StepAnnotations) GetStep() int32 {1368 func (x *StepAnnotations) GetStep() int32 {
@@ -1290,7 +1391,7 @@ type StepSummary struct {
1290 1391
1291 func (x *StepSummary) Reset() {1392 func (x *StepSummary) Reset() {
1292 *x = StepSummary{}1393 *x = StepSummary{}
1293- mi := &file_exec_proto_msgTypes[11]1394+ mi := &file_exec_proto_msgTypes[12]
1294 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1395 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1295 ms.StoreMessageInfo(mi)1396 ms.StoreMessageInfo(mi)
1296 }1397 }
@@ -1302,7 +1403,7 @@ func (x *StepSummary) String() string {
1302 func (*StepSummary) ProtoMessage() {}1403 func (*StepSummary) ProtoMessage() {}
1303 1404
1304 func (x *StepSummary) ProtoReflect() protoreflect.Message {1405 func (x *StepSummary) ProtoReflect() protoreflect.Message {
1305- mi := &file_exec_proto_msgTypes[11]1406+ mi := &file_exec_proto_msgTypes[12]
1306 if x != nil {1407 if x != nil {
1307 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1408 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1308 if ms.LoadMessageInfo() == nil {1409 if ms.LoadMessageInfo() == nil {
@@ -1315,7 +1416,7 @@ func (x *StepSummary) ProtoReflect() protoreflect.Message {
1315 1416
1316 // Deprecated: Use StepSummary.ProtoReflect.Descriptor instead.1417 // Deprecated: Use StepSummary.ProtoReflect.Descriptor instead.
1317 func (*StepSummary) Descriptor() ([]byte, []int) {1418 func (*StepSummary) Descriptor() ([]byte, []int) {
1318- return file_exec_proto_rawDescGZIP(), []int{11}1419+ return file_exec_proto_rawDescGZIP(), []int{12}
1319 }1420 }
1320 1421
1321 func (x *StepSummary) GetStep() int32 {1422 func (x *StepSummary) GetStep() int32 {
@@ -1347,7 +1448,7 @@ type Log struct {
1347 1448
1348 func (x *Log) Reset() {1449 func (x *Log) Reset() {
1349 *x = Log{}1450 *x = Log{}
1350- mi := &file_exec_proto_msgTypes[12]1451+ mi := &file_exec_proto_msgTypes[13]
1351 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1452 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1352 ms.StoreMessageInfo(mi)1453 ms.StoreMessageInfo(mi)
1353 }1454 }
@@ -1359,7 +1460,7 @@ func (x *Log) String() string {
1359 func (*Log) ProtoMessage() {}1460 func (*Log) ProtoMessage() {}
1360 1461
1361 func (x *Log) ProtoReflect() protoreflect.Message {1462 func (x *Log) ProtoReflect() protoreflect.Message {
1362- mi := &file_exec_proto_msgTypes[12]1463+ mi := &file_exec_proto_msgTypes[13]
1363 if x != nil {1464 if x != nil {
1364 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1465 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1365 if ms.LoadMessageInfo() == nil {1466 if ms.LoadMessageInfo() == nil {
@@ -1372,7 +1473,7 @@ func (x *Log) ProtoReflect() protoreflect.Message {
1372 1473
1373 // Deprecated: Use Log.ProtoReflect.Descriptor instead.1474 // Deprecated: Use Log.ProtoReflect.Descriptor instead.
1374 func (*Log) Descriptor() ([]byte, []int) {1475 func (*Log) Descriptor() ([]byte, []int) {
1375- return file_exec_proto_rawDescGZIP(), []int{12}1476+ return file_exec_proto_rawDescGZIP(), []int{13}
1376 }1477 }
1377 1478
1378 func (x *Log) GetStep() int32 {1479 func (x *Log) GetStep() int32 {
@@ -1416,7 +1517,7 @@ type JobResult struct {
1416 1517
1417 func (x *JobResult) Reset() {1518 func (x *JobResult) Reset() {
1418 *x = JobResult{}1519 *x = JobResult{}
1419- mi := &file_exec_proto_msgTypes[13]1520+ mi := &file_exec_proto_msgTypes[14]
1420 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1521 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1421 ms.StoreMessageInfo(mi)1522 ms.StoreMessageInfo(mi)
1422 }1523 }
@@ -1428,7 +1529,7 @@ func (x *JobResult) String() string {
1428 func (*JobResult) ProtoMessage() {}1529 func (*JobResult) ProtoMessage() {}
1429 1530
1430 func (x *JobResult) ProtoReflect() protoreflect.Message {1531 func (x *JobResult) ProtoReflect() protoreflect.Message {
1431- mi := &file_exec_proto_msgTypes[13]1532+ mi := &file_exec_proto_msgTypes[14]
1432 if x != nil {1533 if x != nil {
1433 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1534 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1434 if ms.LoadMessageInfo() == nil {1535 if ms.LoadMessageInfo() == nil {
@@ -1441,7 +1542,7 @@ func (x *JobResult) ProtoReflect() protoreflect.Message {
1441 1542
1442 // Deprecated: Use JobResult.ProtoReflect.Descriptor instead.1543 // Deprecated: Use JobResult.ProtoReflect.Descriptor instead.
1443 func (*JobResult) Descriptor() ([]byte, []int) {1544 func (*JobResult) Descriptor() ([]byte, []int) {
1444- return file_exec_proto_rawDescGZIP(), []int{13}1545+ return file_exec_proto_rawDescGZIP(), []int{14}
1445 }1546 }
1446 1547
1447 func (x *JobResult) GetConclusion() StepConclusion {1548 func (x *JobResult) GetConclusion() StepConclusion {
@@ -1486,7 +1587,7 @@ type ResolveRequest struct {
1486 1587
1487 func (x *ResolveRequest) Reset() {1588 func (x *ResolveRequest) Reset() {
1488 *x = ResolveRequest{}1589 *x = ResolveRequest{}
1489- mi := &file_exec_proto_msgTypes[14]1590+ mi := &file_exec_proto_msgTypes[15]
1490 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1591 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1491 ms.StoreMessageInfo(mi)1592 ms.StoreMessageInfo(mi)
1492 }1593 }
@@ -1498,7 +1599,7 @@ func (x *ResolveRequest) String() string {
1498 func (*ResolveRequest) ProtoMessage() {}1599 func (*ResolveRequest) ProtoMessage() {}
1499 1600
1500 func (x *ResolveRequest) ProtoReflect() protoreflect.Message {1601 func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
1501- mi := &file_exec_proto_msgTypes[14]1602+ mi := &file_exec_proto_msgTypes[15]
1502 if x != nil {1603 if x != nil {
1503 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1604 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1504 if ms.LoadMessageInfo() == nil {1605 if ms.LoadMessageInfo() == nil {
@@ -1511,7 +1612,7 @@ func (x *ResolveRequest) ProtoReflect() protoreflect.Message {
1511 1612
1512 // Deprecated: Use ResolveRequest.ProtoReflect.Descriptor instead.1613 // Deprecated: Use ResolveRequest.ProtoReflect.Descriptor instead.
1513 func (*ResolveRequest) Descriptor() ([]byte, []int) {1614 func (*ResolveRequest) Descriptor() ([]byte, []int) {
1514- return file_exec_proto_rawDescGZIP(), []int{14}1615+ return file_exec_proto_rawDescGZIP(), []int{15}
1515 }1616 }
1516 1617
1517 func (x *ResolveRequest) GetId() int64 {1618 func (x *ResolveRequest) GetId() int64 {
@@ -1541,7 +1642,7 @@ type ResolveResponse struct {
1541 1642
1542 func (x *ResolveResponse) Reset() {1643 func (x *ResolveResponse) Reset() {
1543 *x = ResolveResponse{}1644 *x = ResolveResponse{}
1544- mi := &file_exec_proto_msgTypes[15]1645+ mi := &file_exec_proto_msgTypes[16]
1545 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1646 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1546 ms.StoreMessageInfo(mi)1647 ms.StoreMessageInfo(mi)
1547 }1648 }
@@ -1553,7 +1654,7 @@ func (x *ResolveResponse) String() string {
1553 func (*ResolveResponse) ProtoMessage() {}1654 func (*ResolveResponse) ProtoMessage() {}
1554 1655
1555 func (x *ResolveResponse) ProtoReflect() protoreflect.Message {1656 func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
1556- mi := &file_exec_proto_msgTypes[15]1657+ mi := &file_exec_proto_msgTypes[16]
1557 if x != nil {1658 if x != nil {
1558 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1659 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1559 if ms.LoadMessageInfo() == nil {1660 if ms.LoadMessageInfo() == nil {
@@ -1566,7 +1667,7 @@ func (x *ResolveResponse) ProtoReflect() protoreflect.Message {
1566 1667
1567 // Deprecated: Use ResolveResponse.ProtoReflect.Descriptor instead.1668 // Deprecated: Use ResolveResponse.ProtoReflect.Descriptor instead.
1568 func (*ResolveResponse) Descriptor() ([]byte, []int) {1669 func (*ResolveResponse) Descriptor() ([]byte, []int) {
1569- return file_exec_proto_rawDescGZIP(), []int{15}1670+ return file_exec_proto_rawDescGZIP(), []int{16}
1570 }1671 }
1571 1672
1572 func (x *ResolveResponse) GetId() int64 {1673 func (x *ResolveResponse) GetId() int64 {
@@ -1613,7 +1714,7 @@ type ResolvedAction struct {
1613 1714
1614 func (x *ResolvedAction) Reset() {1715 func (x *ResolvedAction) Reset() {
1615 *x = ResolvedAction{}1716 *x = ResolvedAction{}
1616- mi := &file_exec_proto_msgTypes[16]1717+ mi := &file_exec_proto_msgTypes[17]
1617 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1718 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1618 ms.StoreMessageInfo(mi)1719 ms.StoreMessageInfo(mi)
1619 }1720 }
@@ -1625,7 +1726,7 @@ func (x *ResolvedAction) String() string {
1625 func (*ResolvedAction) ProtoMessage() {}1726 func (*ResolvedAction) ProtoMessage() {}
1626 1727
1627 func (x *ResolvedAction) ProtoReflect() protoreflect.Message {1728 func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
1628- mi := &file_exec_proto_msgTypes[16]1729+ mi := &file_exec_proto_msgTypes[17]
1629 if x != nil {1730 if x != nil {
1630 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1731 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1631 if ms.LoadMessageInfo() == nil {1732 if ms.LoadMessageInfo() == nil {
@@ -1638,7 +1739,7 @@ func (x *ResolvedAction) ProtoReflect() protoreflect.Message {
1638 1739
1639 // Deprecated: Use ResolvedAction.ProtoReflect.Descriptor instead.1740 // Deprecated: Use ResolvedAction.ProtoReflect.Descriptor instead.
1640 func (*ResolvedAction) Descriptor() ([]byte, []int) {1741 func (*ResolvedAction) Descriptor() ([]byte, []int) {
1641- return file_exec_proto_rawDescGZIP(), []int{16}1742+ return file_exec_proto_rawDescGZIP(), []int{17}
1642 }1743 }
1643 1744
1644 func (x *ResolvedAction) GetRef() string {1745 func (x *ResolvedAction) GetRef() string {
@@ -1709,7 +1810,7 @@ var File_exec_proto protoreflect.FileDescriptor
1709 const file_exec_proto_rawDesc = "" +1810 const file_exec_proto_rawDesc = "" +
1710 "\n" +1811 "\n" +
1711 "\n" +1812 "\n" +
1712- "exec.proto\x12\x15ermite.runner.exec.v1\"\xea\x06\n" +1813+ "exec.proto\x12\x15ermite.runner.exec.v1\"\xde\a\n" +
1713 "\x05Frame\x124\n" +1814 "\x05Frame\x124\n" +
1714 "\x05hello\x18\x01 \x01(\v2\x1c.ermite.runner.exec.v1.HelloH\x00R\x05hello\x12.\n" +1815 "\x05hello\x18\x01 \x01(\v2\x1c.ermite.runner.exec.v1.HelloH\x00R\x05hello\x12.\n" +
1715 "\x03run\x18\x02 \x01(\v2\x1a.ermite.runner.exec.v1.RunH\x00R\x03run\x127\n" +1816 "\x03run\x18\x02 \x01(\v2\x1a.ermite.runner.exec.v1.RunH\x00R\x03run\x127\n" +
@@ -1725,8 +1826,14 @@ const file_exec_proto_rawDesc = "" +
1725 " \x01(\v2&.ermite.runner.exec.v1.StepAnnotationsH\x00R\x0fstepAnnotations\x12G\n" +1826 " \x01(\v2&.ermite.runner.exec.v1.StepAnnotationsH\x00R\x0fstepAnnotations\x12G\n" +
1726 "\fstep_summary\x18\v \x01(\v2\".ermite.runner.exec.v1.StepSummaryH\x00R\vstepSummary\x12P\n" +1827 "\fstep_summary\x18\v \x01(\v2\".ermite.runner.exec.v1.StepSummaryH\x00R\vstepSummary\x12P\n" +
1727 "\x0fresolve_request\x18\f \x01(\v2%.ermite.runner.exec.v1.ResolveRequestH\x00R\x0eresolveRequest\x12S\n" +1828 "\x0fresolve_request\x18\f \x01(\v2%.ermite.runner.exec.v1.ResolveRequestH\x00R\x0eresolveRequest\x12S\n" +
1728- "\x10resolve_response\x18\r \x01(\v2&.ermite.runner.exec.v1.ResolveResponseH\x00R\x0fresolveResponseB\x06\n" +1829+ "\x10resolve_response\x18\r \x01(\v2&.ermite.runner.exec.v1.ResolveResponseH\x00R\x0fresolveResponse\x12;\n" +
1729- "\x04body\"W\n" +1830+ "\x06secret\x18\x0e \x01(\v2!.ermite.runner.exec.v1.NamedValueH\x00R\x06secret\x125\n" +
1831+ "\x03var\x18\x0f \x01(\v2!.ermite.runner.exec.v1.NamedValueH\x00R\x03varB\x06\n" +
1832+ "\x04body\"6\n" +
1833+ "\n" +
1834+ "NamedValue\x12\x12\n" +
1835+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
1836+ "\x05value\x18\x02 \x01(\tR\x05value\"W\n" +
1730 "\x05Hello\x12\x18\n" +1837 "\x05Hello\x12\x18\n" +
1731 "\aversion\x18\x01 \x01(\tR\aversion\x12\x18\n" +1838 "\aversion\x18\x01 \x01(\tR\aversion\x12\x18\n" +
1732 "\asandbox\x18\x02 \x01(\tR\asandbox\x12\x1a\n" +1839 "\asandbox\x18\x02 \x01(\tR\asandbox\x12\x1a\n" +
@@ -1737,7 +1844,7 @@ const file_exec_proto_rawDesc = "" +
1737 "\x03job\x18\x03 \x01(\v2\x1e.ermite.runner.exec.v1.JobSpecR\x03job\x1a6\n" +1844 "\x03job\x18\x03 \x01(\v2\x1e.ermite.runner.exec.v1.JobSpecR\x03job\x1a6\n" +
1738 "\bEnvEntry\x12\x10\n" +1845 "\bEnvEntry\x12\x10\n" +
1739 "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +1846 "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1740- "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x9d\x04\n" +1847+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe3\x04\n" +
1741 "\aJobSpec\x12#\n" +1848 "\aJobSpec\x12#\n" +
1742 "\rworkflow_path\x18\x01 \x01(\tR\fworkflowPath\x12#\n" +1849 "\rworkflow_path\x18\x01 \x01(\tR\fworkflowPath\x12#\n" +
1743 "\rworkflow_yaml\x18\x02 \x01(\tR\fworkflowYaml\x12\x17\n" +1850 "\rworkflow_yaml\x18\x02 \x01(\tR\fworkflowYaml\x12\x17\n" +
@@ -1759,7 +1866,8 @@ const file_exec_proto_rawDesc = "" +
1759 "\n" +1866 "\n" +
1760 "cache_port\x18\x0f \x01(\rR\tcachePort\x12\x1f\n" +1867 "cache_port\x18\x0f \x01(\rR\tcachePort\x12\x1f\n" +
1761 "\vcache_token\x18\x10 \x01(\tR\n" +1868 "\vcache_token\x18\x10 \x01(\tR\n" +
1762- "cacheToken\"t\n" +1869+ "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" +
1763 "\x06Output\x129\n" +1871 "\x06Output\x129\n" +
1764 "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +1872 "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +
1765 "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +1873 "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +
@@ -1871,62 +1979,65 @@ func file_exec_proto_rawDescGZIP() []byte {
1871 }1979 }
1872 1980
1873 var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 4)1981 var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
1874-var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 19)1982+var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
1875 var file_exec_proto_goTypes = []any{1983 var file_exec_proto_goTypes = []any{
1876 (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind1984 (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind
1877 (CancelKind)(0), // 1: ermite.runner.exec.v1.CancelKind1985 (CancelKind)(0), // 1: ermite.runner.exec.v1.CancelKind
1878 (AnnotationLevel)(0), // 2: ermite.runner.exec.v1.AnnotationLevel1986 (AnnotationLevel)(0), // 2: ermite.runner.exec.v1.AnnotationLevel
1879 (StepConclusion)(0), // 3: ermite.runner.exec.v1.StepConclusion1987 (StepConclusion)(0), // 3: ermite.runner.exec.v1.StepConclusion
1880 (*Frame)(nil), // 4: ermite.runner.exec.v1.Frame1988 (*Frame)(nil), // 4: ermite.runner.exec.v1.Frame
1881- (*Hello)(nil), // 5: ermite.runner.exec.v1.Hello1989+ (*NamedValue)(nil), // 5: ermite.runner.exec.v1.NamedValue
1882- (*Run)(nil), // 6: ermite.runner.exec.v1.Run1990+ (*Hello)(nil), // 6: ermite.runner.exec.v1.Hello
1883- (*JobSpec)(nil), // 7: ermite.runner.exec.v1.JobSpec1991+ (*Run)(nil), // 7: ermite.runner.exec.v1.Run
1884- (*Output)(nil), // 8: ermite.runner.exec.v1.Output1992+ (*JobSpec)(nil), // 8: ermite.runner.exec.v1.JobSpec
1885- (*Exit)(nil), // 9: ermite.runner.exec.v1.Exit1993+ (*Output)(nil), // 9: ermite.runner.exec.v1.Output
1886- (*Cancel)(nil), // 10: ermite.runner.exec.v1.Cancel1994+ (*Exit)(nil), // 10: ermite.runner.exec.v1.Exit
1887- (*StepStarted)(nil), // 11: ermite.runner.exec.v1.StepStarted1995+ (*Cancel)(nil), // 11: ermite.runner.exec.v1.Cancel
1888- (*StepCompleted)(nil), // 12: ermite.runner.exec.v1.StepCompleted1996+ (*StepStarted)(nil), // 12: ermite.runner.exec.v1.StepStarted
1889- (*Annotation)(nil), // 13: ermite.runner.exec.v1.Annotation1997+ (*StepCompleted)(nil), // 13: ermite.runner.exec.v1.StepCompleted
1890- (*StepAnnotations)(nil), // 14: ermite.runner.exec.v1.StepAnnotations1998+ (*Annotation)(nil), // 14: ermite.runner.exec.v1.Annotation
1891- (*StepSummary)(nil), // 15: ermite.runner.exec.v1.StepSummary1999+ (*StepAnnotations)(nil), // 15: ermite.runner.exec.v1.StepAnnotations
1892- (*Log)(nil), // 16: ermite.runner.exec.v1.Log2000+ (*StepSummary)(nil), // 16: ermite.runner.exec.v1.StepSummary
1893- (*JobResult)(nil), // 17: ermite.runner.exec.v1.JobResult2001+ (*Log)(nil), // 17: ermite.runner.exec.v1.Log
1894- (*ResolveRequest)(nil), // 18: ermite.runner.exec.v1.ResolveRequest2002+ (*JobResult)(nil), // 18: ermite.runner.exec.v1.JobResult
1895- (*ResolveResponse)(nil), // 19: ermite.runner.exec.v1.ResolveResponse2003+ (*ResolveRequest)(nil), // 19: ermite.runner.exec.v1.ResolveRequest
1896- (*ResolvedAction)(nil), // 20: ermite.runner.exec.v1.ResolvedAction2004+ (*ResolveResponse)(nil), // 20: ermite.runner.exec.v1.ResolveResponse
1897- nil, // 21: ermite.runner.exec.v1.Run.EnvEntry2005+ (*ResolvedAction)(nil), // 21: ermite.runner.exec.v1.ResolvedAction
1898- nil, // 22: ermite.runner.exec.v1.JobResult.OutputsEntry2006+ nil, // 22: ermite.runner.exec.v1.Run.EnvEntry
2007+ nil, // 23: ermite.runner.exec.v1.JobResult.OutputsEntry
1899 }2008 }
1900 var file_exec_proto_depIdxs = []int32{2009 var file_exec_proto_depIdxs = []int32{
1901- 5, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello2010+ 6, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello
1902- 6, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run2011+ 7, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run
1903- 8, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output2012+ 9, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output
1904- 9, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit2013+ 10, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit
1905- 10, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel2014+ 11, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel
1906- 11, // 5: ermite.runner.exec.v1.Frame.step_started:type_name -> ermite.runner.exec.v1.StepStarted2015+ 12, // 5: ermite.runner.exec.v1.Frame.step_started:type_name -> ermite.runner.exec.v1.StepStarted
1907- 12, // 6: ermite.runner.exec.v1.Frame.step_completed:type_name -> ermite.runner.exec.v1.StepCompleted2016+ 13, // 6: ermite.runner.exec.v1.Frame.step_completed:type_name -> ermite.runner.exec.v1.StepCompleted
1908- 16, // 7: ermite.runner.exec.v1.Frame.log:type_name -> ermite.runner.exec.v1.Log2017+ 17, // 7: ermite.runner.exec.v1.Frame.log:type_name -> ermite.runner.exec.v1.Log
1909- 17, // 8: ermite.runner.exec.v1.Frame.job_result:type_name -> ermite.runner.exec.v1.JobResult2018+ 18, // 8: ermite.runner.exec.v1.Frame.job_result:type_name -> ermite.runner.exec.v1.JobResult
1910- 14, // 9: ermite.runner.exec.v1.Frame.step_annotations:type_name -> ermite.runner.exec.v1.StepAnnotations2019+ 15, // 9: ermite.runner.exec.v1.Frame.step_annotations:type_name -> ermite.runner.exec.v1.StepAnnotations
1911- 15, // 10: ermite.runner.exec.v1.Frame.step_summary:type_name -> ermite.runner.exec.v1.StepSummary2020+ 16, // 10: ermite.runner.exec.v1.Frame.step_summary:type_name -> ermite.runner.exec.v1.StepSummary
1912- 18, // 11: ermite.runner.exec.v1.Frame.resolve_request:type_name -> ermite.runner.exec.v1.ResolveRequest2021+ 19, // 11: ermite.runner.exec.v1.Frame.resolve_request:type_name -> ermite.runner.exec.v1.ResolveRequest
1913- 19, // 12: ermite.runner.exec.v1.Frame.resolve_response:type_name -> ermite.runner.exec.v1.ResolveResponse2022+ 20, // 12: ermite.runner.exec.v1.Frame.resolve_response:type_name -> ermite.runner.exec.v1.ResolveResponse
1914- 21, // 13: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry2023+ 5, // 13: ermite.runner.exec.v1.Frame.secret:type_name -> ermite.runner.exec.v1.NamedValue
1915- 7, // 14: ermite.runner.exec.v1.Run.job:type_name -> ermite.runner.exec.v1.JobSpec2024+ 5, // 14: ermite.runner.exec.v1.Frame.var:type_name -> ermite.runner.exec.v1.NamedValue
1916- 0, // 15: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind2025+ 22, // 15: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry
1917- 1, // 16: ermite.runner.exec.v1.Cancel.kind:type_name -> ermite.runner.exec.v1.CancelKind2026+ 8, // 16: ermite.runner.exec.v1.Run.job:type_name -> ermite.runner.exec.v1.JobSpec
1918- 3, // 17: ermite.runner.exec.v1.StepCompleted.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion2027+ 0, // 17: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind
1919- 3, // 18: ermite.runner.exec.v1.StepCompleted.outcome:type_name -> ermite.runner.exec.v1.StepConclusion2028+ 1, // 18: ermite.runner.exec.v1.Cancel.kind:type_name -> ermite.runner.exec.v1.CancelKind
1920- 2, // 19: ermite.runner.exec.v1.Annotation.level:type_name -> ermite.runner.exec.v1.AnnotationLevel2029+ 3, // 19: ermite.runner.exec.v1.StepCompleted.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
1921- 13, // 20: ermite.runner.exec.v1.StepAnnotations.annotations:type_name -> ermite.runner.exec.v1.Annotation2030+ 3, // 20: ermite.runner.exec.v1.StepCompleted.outcome:type_name -> ermite.runner.exec.v1.StepConclusion
1922- 3, // 21: ermite.runner.exec.v1.JobResult.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion2031+ 2, // 21: ermite.runner.exec.v1.Annotation.level:type_name -> ermite.runner.exec.v1.AnnotationLevel
1923- 22, // 22: ermite.runner.exec.v1.JobResult.outputs:type_name -> ermite.runner.exec.v1.JobResult.OutputsEntry2032+ 14, // 22: ermite.runner.exec.v1.StepAnnotations.annotations:type_name -> ermite.runner.exec.v1.Annotation
1924- 20, // 23: ermite.runner.exec.v1.ResolveResponse.actions:type_name -> ermite.runner.exec.v1.ResolvedAction2033+ 3, // 23: ermite.runner.exec.v1.JobResult.conclusion:type_name -> ermite.runner.exec.v1.StepConclusion
1925- 24, // [24:24] is the sub-list for method output_type2034+ 23, // 24: ermite.runner.exec.v1.JobResult.outputs:type_name -> ermite.runner.exec.v1.JobResult.OutputsEntry
1926- 24, // [24:24] is the sub-list for method input_type2035+ 21, // 25: ermite.runner.exec.v1.ResolveResponse.actions:type_name -> ermite.runner.exec.v1.ResolvedAction
1927- 24, // [24:24] is the sub-list for extension type_name2036+ 26, // [26:26] is the sub-list for method output_type
1928- 24, // [24:24] is the sub-list for extension extendee2037+ 26, // [26:26] is the sub-list for method input_type
1929- 0, // [0:24] is the sub-list for field type_name2038+ 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
1930 }2041 }
1931 2042
1932 func init() { file_exec_proto_init() }2043 func init() { file_exec_proto_init() }
@@ -1948,6 +2059,8 @@ func file_exec_proto_init() {
1948 (*Frame_StepSummary)(nil),2059 (*Frame_StepSummary)(nil),
1949 (*Frame_ResolveRequest)(nil),2060 (*Frame_ResolveRequest)(nil),
1950 (*Frame_ResolveResponse)(nil),2061 (*Frame_ResolveResponse)(nil),
2062+ (*Frame_Secret)(nil),
2063+ (*Frame_Var)(nil),
1951 }2064 }
1952 type x struct{}2065 type x struct{}
1953 out := protoimpl.TypeBuilder{2066 out := protoimpl.TypeBuilder{
@@ -1955,7 +2068,7 @@ func file_exec_proto_init() {
1955 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),2068 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
1956 RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),2069 RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),
1957 NumEnums: 4,2070 NumEnums: 4,
1958- NumMessages: 19,2071+ NumMessages: 20,
1959 NumExtensions: 0,2072 NumExtensions: 0,
1960 NumServices: 0,2073 NumServices: 0,
1961 },2074 },
M internal/execstream/frame.go +27 -0
@@ -18,6 +18,8 @@ import (
1818 "errors"
1919 "fmt"
2020 "io"
21+ "maps"
22+ "slices"
2123 "sync"
2224 "time"
2325
@@ -167,6 +169,10 @@ type JobHandler struct {
167169 // step.
168170 StepAnnotations func(*execpb.StepAnnotations)
169171 StepSummary func(*execpb.StepSummary)
172+ // Secrets and Vars (Job.secrets, Job.vars) are sent before the Run frame,
173+ // one NamedValue frame each, so that their total size is not bound by the
174+ // size of one frame (MaxFrame). A single value must fit in a frame.
175+ Secrets, Vars map[string]string
170176 }
171177
172178 // DriveJob is Drive for a session that carries a job: besides Output frames it
@@ -184,6 +190,9 @@ func DriveJob(rw io.ReadWriter, run *execpb.Run, cancel <-chan *execpb.Cancel, h
184190 if h.Hello != nil {
185191 h.Hello(hello)
186192 }
193+ if err := sendValues(c, h); err != nil {
194+ return hello, nil, err
195+ }
187196 if err := c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: run}}); err != nil {
188197 return hello, nil, fmt.Errorf("execstream: sending run: %w", err)
189198 }
@@ -279,3 +288,21 @@ func DriveJob(rw io.ReadWriter, run *execpb.Run, cancel <-chan *execpb.Cancel, h
279288 }
280289 }
281290 }
291+
292+// sendValues sends the secrets, then the variables of the job, in name order,
293+// one frame each. The error never holds a value.
294+func sendValues(c *Conn, h JobHandler) error {
295+ for _, k := range slices.Sorted(maps.Keys(h.Secrets)) {
296+ f := &execpb.Frame{Body: &execpb.Frame_Secret{Secret: &execpb.NamedValue{Name: k, Value: h.Secrets[k]}}}
297+ if err := c.Send(f); err != nil {
298+ return fmt.Errorf("execstream: sending secret %q: %w", k, err)
299+ }
300+ }
301+ for _, k := range slices.Sorted(maps.Keys(h.Vars)) {
302+ f := &execpb.Frame{Body: &execpb.Frame_Var{Var: &execpb.NamedValue{Name: k, Value: h.Vars[k]}}}
303+ if err := c.Send(f); err != nil {
304+ return fmt.Errorf("execstream: sending variable %q: %w", k, err)
305+ }
306+ }
307+ return nil
308+}
@@ -18,6 +18,8 @@ import (
18 "errors"18 "errors"
19 "fmt"19 "fmt"
20 "io"20 "io"
21+ "maps"
22+ "slices"
21 "sync"23 "sync"
22 "time"24 "time"
23 25
@@ -167,6 +169,10 @@ type JobHandler struct {
167 // step.169 // step.
168 StepAnnotations func(*execpb.StepAnnotations)170 StepAnnotations func(*execpb.StepAnnotations)
169 StepSummary func(*execpb.StepSummary)171 StepSummary func(*execpb.StepSummary)
172+ // Secrets and Vars (Job.secrets, Job.vars) are sent before the Run frame,
173+ // one NamedValue frame each, so that their total size is not bound by the
174+ // size of one frame (MaxFrame). A single value must fit in a frame.
175+ Secrets, Vars map[string]string
170 }176 }
171 177
172 // DriveJob is Drive for a session that carries a job: besides Output frames it178 // DriveJob is Drive for a session that carries a job: besides Output frames it
@@ -184,6 +190,9 @@ func DriveJob(rw io.ReadWriter, run *execpb.Run, cancel <-chan *execpb.Cancel, h
184 if h.Hello != nil {190 if h.Hello != nil {
185 h.Hello(hello)191 h.Hello(hello)
186 }192 }
193+ if err := sendValues(c, h); err != nil {
194+ return hello, nil, err
195+ }
187 if err := c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: run}}); err != nil {196 if err := c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: run}}); err != nil {
188 return hello, nil, fmt.Errorf("execstream: sending run: %w", err)197 return hello, nil, fmt.Errorf("execstream: sending run: %w", err)
189 }198 }
@@ -279,3 +288,21 @@ func DriveJob(rw io.ReadWriter, run *execpb.Run, cancel <-chan *execpb.Cancel, h
279 }288 }
280 }289 }
281 }290 }
291+
292+// sendValues sends the secrets, then the variables of the job, in name order,
293+// one frame each. The error never holds a value.
294+func sendValues(c *Conn, h JobHandler) error {
295+ for _, k := range slices.Sorted(maps.Keys(h.Secrets)) {
296+ f := &execpb.Frame{Body: &execpb.Frame_Secret{Secret: &execpb.NamedValue{Name: k, Value: h.Secrets[k]}}}
297+ if err := c.Send(f); err != nil {
298+ return fmt.Errorf("execstream: sending secret %q: %w", k, err)
299+ }
300+ }
301+ for _, k := range slices.Sorted(maps.Keys(h.Vars)) {
302+ f := &execpb.Frame{Body: &execpb.Frame_Var{Var: &execpb.NamedValue{Name: k, Value: h.Vars[k]}}}
303+ if err := c.Send(f); err != nil {
304+ return fmt.Errorf("execstream: sending variable %q: %w", k, err)
305+ }
306+ }
307+ return nil
308+}
M internal/executor/containers_stop.go +2 -2
@@ -121,7 +121,7 @@ func (j *job) fallbackCleanup() {
121121 return
122122 }
123123 for _, e := range j.cleanupContainers(nil) {
124- j.cfg.Log.Warn().Err(e).Msg("fallback cleanup of the job containers")
124+ j.diagWarn(e, "fallback cleanup of the job containers")
125125 }
126126 }
127127
@@ -273,7 +273,7 @@ func (j *job) warnTo(s *stepRun, msg string) {
273273 j.tagged(s.num, "##[warning]", msg)
274274 return
275275 }
276- j.cfg.Log.Warn().Msg(msg)
276+ j.diagWarn(nil, msg)
277277 }
278278
279279 // absent reports whether docker's answer to a removal says the object does not
@@ -121,7 +121,7 @@ func (j *job) fallbackCleanup() {
121 return121 return
122 }122 }
123 for _, e := range j.cleanupContainers(nil) {123 for _, e := range j.cleanupContainers(nil) {
124- j.cfg.Log.Warn().Err(e).Msg("fallback cleanup of the job containers")124+ j.diagWarn(e, "fallback cleanup of the job containers")
125 }125 }
126 }126 }
127 127
@@ -273,7 +273,7 @@ func (j *job) warnTo(s *stepRun, msg string) {
273 j.tagged(s.num, "##[warning]", msg)273 j.tagged(s.num, "##[warning]", msg)
274 return274 return
275 }275 }
276- j.cfg.Log.Warn().Msg(msg)276+ j.diagWarn(nil, msg)
277 }277 }
278 278
279 // absent reports whether docker's answer to a removal says the object does not279 // absent reports whether docker's answer to a removal says the object does not
M internal/executor/contexts.go +54 -11
@@ -5,6 +5,7 @@ package executor
55 import (
66 "encoding/base64"
77 "fmt"
8+ "maps"
89 "path/filepath"
910 "runtime"
1011 "slices"
@@ -228,6 +229,23 @@ type State struct {
228229 prepend []string
229230 // status is job.status: "success", "failure" or "cancelled".
230231 status string
232+ // jobSecrets are Job.secrets: the secrets context besides GITHUB_TOKEN.
233+ // They live in memory only (never in a file, a log or an error).
234+ jobSecrets map[string]string
235+}
236+
237+// WithJobData gives the state the secrets and variables of the job message
238+// (Job.secrets and Job.vars). The vars context keeps what the server wrote in
239+// the contexts JSON; the variables are its fallback when the JSON has none.
240+func (s *State) WithJobData(secrets, vars map[string]string) {
241+ s.jobSecrets = secrets
242+ if d, ok := s.other["vars"].(*expr.Dictionary); (!ok || d.Len() == 0) && len(vars) > 0 {
243+ nd := expr.NewDictionary()
244+ for _, k := range slices.Sorted(maps.Keys(vars)) {
245+ nd.Add(k, expr.String(vars[k]))
246+ }
247+ s.other["vars"] = nd
248+ }
231249 }
232250
233251 // serverContexts are the keys of the contexts JSON that the runner reads.
@@ -493,32 +511,38 @@ func (s *State) Values(env *ordered) map[string]expr.Value {
493511 return m
494512 }
495513
496-// secrets is the secrets context. Today it holds one entry, GITHUB_TOKEN, as
497-// on GitHub (Variables.ToSecretsContext lists the secret variables but the
498-// access token; GitHub's message carries the token as the secret GITHUB_TOKEN,
499-// recorded: ctx-inputs-vars, secrets.GITHUB_TOKEN == github.token).
514+// secrets is the secrets context: GITHUB_TOKEN and the secrets of the job
515+// (Job.secrets), as on GitHub where the message carries the token as the secret
516+// GITHUB_TOKEN (recorded: ctx-inputs-vars, secrets.GITHUB_TOKEN == github.token).
517+// An unknown secret evaluates to "" like on GitHub.
500518 //
501-// Where the value comes from: the "token" key of the github context that the
519+// GITHUB_TOKEN comes from the "token" key of the github context that the
502520 // server sends in Job.contexts, the same value as github.token. It must be a
503521 // token of its own, scoped to the job (the permissions "Set up job" prints) and
504522 // short lived. It must never be the "ermitejob_" job token: that one
505523 // authenticates the runner's calls to ermite, stays in the runner host process
506-// and must not reach the sandbox (runner.proto, "Secrets and trust"). ermite
507-// does not send a token yet (planned for CI-3, with the secrets of the
508-// repository); until it does, github.token is empty, so is secrets, and
509-// a test of secrets.GITHUB_TOKEN against the empty string is false. Without a
510-// token nothing is masked.
524+// and must not reach the sandbox (runner.proto, "Secrets and trust"). Without a
525+// token there is no GITHUB_TOKEN entry, and a test of secrets.GITHUB_TOKEN
526+// against the empty string is false. GITHUB_TOKEN is reserved: an entry of
527+// that name in Job.secrets is ignored.
511528 func (s *State) secrets() *expr.Dictionary {
512529 d := expr.NewDictionary()
513530 if t := s.GitHub("token"); t != "" {
514531 d.Add("GITHUB_TOKEN", expr.String(t))
515532 }
533+ for _, k := range slices.Sorted(maps.Keys(s.jobSecrets)) {
534+ if strings.EqualFold(k, "GITHUB_TOKEN") {
535+ continue
536+ }
537+ d.Add(k, expr.String(s.jobSecrets[k]))
538+ }
516539 return d
517540 }
518541
519542 // Secrets returns the values that must never leave the job: the token and the
520543 // base64 of "x-access-token:<token>" (ExecutionContext.cs:1026-1028); newMasker
521-// adds their other encodings.
544+// adds their other encodings. The secrets of the job are registered with
545+// registerSecrets.
522546 func (s *State) Secrets() []string {
523547 t := s.GitHub("token")
524548 if t == "" {
@@ -527,6 +551,25 @@ func (s *State) Secrets() []string {
527551 return []string{t, base64.StdEncoding.EncodeToString([]byte("x-access-token:" + t))}
528552 }
529553
554+// registerSecrets registers every secret of the job as the runner does before
555+// the first step (Worker.cs:158-171): the whole value trimmed, which may span
556+// several lines because individual trace info can contain line breaks, then
557+// each non-empty trimmed line of it, split on CR and LF, because child
558+// processes are processed line by line. add ignores empty values and adds the
559+// encoded forms. GITHUB_TOKEN is not a job secret (see State.secrets).
560+func registerSecrets(m *masker, secrets map[string]string) {
561+ for _, k := range slices.Sorted(maps.Keys(secrets)) {
562+ if strings.EqualFold(k, "GITHUB_TOKEN") {
563+ continue
564+ }
565+ v := strings.TrimSpace(secrets[k])
566+ m.add(v)
567+ for _, line := range strings.FieldsFunc(v, func(r rune) bool { return r == '\r' || r == '\n' }) {
568+ m.add(strings.TrimSpace(line))
569+ }
570+ }
571+}
572+
530573 // ServerContexts returns the contexts that the server has when it evaluates
531574 // the name of a job: github as received (without the keys the runner adds),
532575 // needs, strategy, matrix, vars and inputs.
@@ -5,6 +5,7 @@ package executor
5 import (5 import (
6 "encoding/base64"6 "encoding/base64"
7 "fmt"7 "fmt"
8+ "maps"
8 "path/filepath"9 "path/filepath"
9 "runtime"10 "runtime"
10 "slices"11 "slices"
@@ -228,6 +229,23 @@ type State struct {
228 prepend []string229 prepend []string
229 // status is job.status: "success", "failure" or "cancelled".230 // status is job.status: "success", "failure" or "cancelled".
230 status string231 status string
232+ // jobSecrets are Job.secrets: the secrets context besides GITHUB_TOKEN.
233+ // They live in memory only (never in a file, a log or an error).
234+ jobSecrets map[string]string
235+}
236+
237+// WithJobData gives the state the secrets and variables of the job message
238+// (Job.secrets and Job.vars). The vars context keeps what the server wrote in
239+// the contexts JSON; the variables are its fallback when the JSON has none.
240+func (s *State) WithJobData(secrets, vars map[string]string) {
241+ s.jobSecrets = secrets
242+ if d, ok := s.other["vars"].(*expr.Dictionary); (!ok || d.Len() == 0) && len(vars) > 0 {
243+ nd := expr.NewDictionary()
244+ for _, k := range slices.Sorted(maps.Keys(vars)) {
245+ nd.Add(k, expr.String(vars[k]))
246+ }
247+ s.other["vars"] = nd
248+ }
231 }249 }
232 250
233 // serverContexts are the keys of the contexts JSON that the runner reads.251 // serverContexts are the keys of the contexts JSON that the runner reads.
@@ -493,32 +511,38 @@ func (s *State) Values(env *ordered) map[string]expr.Value {
493 return m511 return m
494 }512 }
495 513
496-// secrets is the secrets context. Today it holds one entry, GITHUB_TOKEN, as514+// secrets is the secrets context: GITHUB_TOKEN and the secrets of the job
497-// on GitHub (Variables.ToSecretsContext lists the secret variables but the515+// (Job.secrets), as on GitHub where the message carries the token as the secret
498-// access token; GitHub's message carries the token as the secret GITHUB_TOKEN,516+// GITHUB_TOKEN (recorded: ctx-inputs-vars, secrets.GITHUB_TOKEN == github.token).
499-// recorded: ctx-inputs-vars, secrets.GITHUB_TOKEN == github.token).517+// An unknown secret evaluates to "" like on GitHub.
500 //518 //
501-// Where the value comes from: the "token" key of the github context that the519+// GITHUB_TOKEN comes from the "token" key of the github context that the
502 // server sends in Job.contexts, the same value as github.token. It must be a520 // server sends in Job.contexts, the same value as github.token. It must be a
503 // token of its own, scoped to the job (the permissions "Set up job" prints) and521 // token of its own, scoped to the job (the permissions "Set up job" prints) and
504 // short lived. It must never be the "ermitejob_" job token: that one522 // short lived. It must never be the "ermitejob_" job token: that one
505 // authenticates the runner's calls to ermite, stays in the runner host process523 // authenticates the runner's calls to ermite, stays in the runner host process
506-// and must not reach the sandbox (runner.proto, "Secrets and trust"). ermite524+// and must not reach the sandbox (runner.proto, "Secrets and trust"). Without a
507-// does not send a token yet (planned for CI-3, with the secrets of the525+// token there is no GITHUB_TOKEN entry, and a test of secrets.GITHUB_TOKEN
508-// repository); until it does, github.token is empty, so is secrets, and526+// against the empty string is false. GITHUB_TOKEN is reserved: an entry of
509-// a test of secrets.GITHUB_TOKEN against the empty string is false. Without a527+// that name in Job.secrets is ignored.
510-// token nothing is masked.
511 func (s *State) secrets() *expr.Dictionary {528 func (s *State) secrets() *expr.Dictionary {
512 d := expr.NewDictionary()529 d := expr.NewDictionary()
513 if t := s.GitHub("token"); t != "" {530 if t := s.GitHub("token"); t != "" {
514 d.Add("GITHUB_TOKEN", expr.String(t))531 d.Add("GITHUB_TOKEN", expr.String(t))
515 }532 }
533+ for _, k := range slices.Sorted(maps.Keys(s.jobSecrets)) {
534+ if strings.EqualFold(k, "GITHUB_TOKEN") {
535+ continue
536+ }
537+ d.Add(k, expr.String(s.jobSecrets[k]))
538+ }
516 return d539 return d
517 }540 }
518 541
519 // Secrets returns the values that must never leave the job: the token and the542 // Secrets returns the values that must never leave the job: the token and the
520 // base64 of "x-access-token:<token>" (ExecutionContext.cs:1026-1028); newMasker543 // base64 of "x-access-token:<token>" (ExecutionContext.cs:1026-1028); newMasker
521-// adds their other encodings.544+// adds their other encodings. The secrets of the job are registered with
545+// registerSecrets.
522 func (s *State) Secrets() []string {546 func (s *State) Secrets() []string {
523 t := s.GitHub("token")547 t := s.GitHub("token")
524 if t == "" {548 if t == "" {
@@ -527,6 +551,25 @@ func (s *State) Secrets() []string {
527 return []string{t, base64.StdEncoding.EncodeToString([]byte("x-access-token:" + t))}551 return []string{t, base64.StdEncoding.EncodeToString([]byte("x-access-token:" + t))}
528 }552 }
529 553
554+// registerSecrets registers every secret of the job as the runner does before
555+// the first step (Worker.cs:158-171): the whole value trimmed, which may span
556+// several lines because individual trace info can contain line breaks, then
557+// each non-empty trimmed line of it, split on CR and LF, because child
558+// processes are processed line by line. add ignores empty values and adds the
559+// encoded forms. GITHUB_TOKEN is not a job secret (see State.secrets).
560+func registerSecrets(m *masker, secrets map[string]string) {
561+ for _, k := range slices.Sorted(maps.Keys(secrets)) {
562+ if strings.EqualFold(k, "GITHUB_TOKEN") {
563+ continue
564+ }
565+ v := strings.TrimSpace(secrets[k])
566+ m.add(v)
567+ for _, line := range strings.FieldsFunc(v, func(r rune) bool { return r == '\r' || r == '\n' }) {
568+ m.add(strings.TrimSpace(line))
569+ }
570+ }
571+}
572+
530 // ServerContexts returns the contexts that the server has when it evaluates573 // ServerContexts returns the contexts that the server has when it evaluates
531 // the name of a job: github as received (without the keys the runner adds),574 // the name of a job: github as received (without the keys the runner adds),
532 // needs, strategy, matrix, vars and inputs.575 // needs, strategy, matrix, vars and inputs.
M internal/executor/hashfiles.go +1 -1
@@ -68,7 +68,7 @@ func (j *job) hashFilesFunc() expr.Func {
6868 //nolint:staticcheck // GitHub's text, with its final period
6969 return nil, fmt.Errorf("hashFiles('%s') couldn't finish within %d seconds.", desc, int(hashFilesTimeout/time.Second))
7070 case err != nil:
71- j.cfg.Log.Debug().Err(err).Msg("hashFiles failed")
71+ j.diagDebug(err, "hashFiles failed")
7272 return nil, fmt.Errorf("hashFiles('%s') failed. Fail to hash files under directory '%s'", desc, ws)
7373 }
7474 return expr.String(sum), nil
@@ -68,7 +68,7 @@ func (j *job) hashFilesFunc() expr.Func {
68 //nolint:staticcheck // GitHub's text, with its final period68 //nolint:staticcheck // GitHub's text, with its final period
69 return nil, fmt.Errorf("hashFiles('%s') couldn't finish within %d seconds.", desc, int(hashFilesTimeout/time.Second))69 return nil, fmt.Errorf("hashFiles('%s') couldn't finish within %d seconds.", desc, int(hashFilesTimeout/time.Second))
70 case err != nil:70 case err != nil:
71- j.cfg.Log.Debug().Err(err).Msg("hashFiles failed")71+ j.diagDebug(err, "hashFiles failed")
72 return nil, fmt.Errorf("hashFiles('%s') failed. Fail to hash files under directory '%s'", desc, ws)72 return nil, fmt.Errorf("hashFiles('%s') failed. Fail to hash files under directory '%s'", desc, ws)
73 }73 }
74 return expr.String(sum), nil74 return expr.String(sum), nil
M internal/executor/job.go +59 -4
@@ -78,6 +78,13 @@ type Config struct {
7878 // ACTIONS_RUNTIME_TOKEN of the node and docker actions of the job: the cache
7979 // service of the runner. The token is masked in the log.
8080 CacheURL, CacheToken string
81+ // Secrets and Vars are Job.secrets and Job.vars. Secrets are registered in
82+ // the masker before the first step and form the secrets context; they are
83+ // never logged. Vars are the fallback of the vars context.
84+ Secrets, Vars map[string]string
85+ // SecretsWithheld says that the server did not send secrets because the run
86+ // is not trusted: "Set up job" says so.
87+ SecretsWithheld bool
8188 // Debug turns ##[debug] lines on (ACTIONS_STEP_DEBUG).
8289 Debug bool
8390 // Actions configures the actions of the job (resolution, downloads, Node.js,
@@ -163,8 +170,8 @@ type job struct {
163170 baseEnv []string
164171 lookupPath string
165172 before map[string]bool
166- // masker hides the secrets in everything reported; it is set once the
167- // contexts are built, before any step runs.
173+ // masker hides the secrets in everything reported; Run sets it, with the
174+ // secrets of the job, before the first report.
168175 masker atomic.Pointer[masker]
169176 // actions holds what the pre-job phase loaded (see prepareActions).
170177 actions *actionState
@@ -188,6 +195,23 @@ type job struct {
188195
189196 func (j *job) mask(s string) string { return j.masker.Load().mask(s) }
190197
198+// The runner's own diagnostics (Config.Log) bypass the reporter, hence the
199+// masker: a path, a docker message or an error can hold a secret. Every
200+// diagnostic of the executor goes through these helpers, which mask the
201+// rendered error, message and fields (TestDiagnosticsAreMasked checks that no
202+// other code calls Config.Log).
203+
204+// diagWarn logs a warning with an optional error and string fields (key,
205+// value, ...).
206+func (j *job) diagWarn(err error, msg string, kv ...string) {
207+ j.diag(false, err, msg, kv)
208+}
209+
210+// diagDebug is diagWarn at debug level.
211+func (j *job) diagDebug(err error, msg string, kv ...string) {
212+ j.diag(true, err, msg, kv)
213+}
214+
191215 func (j *job) now() time.Time {
192216 if j.cfg.Now != nil {
193217 return j.cfg.Now()
@@ -202,6 +226,13 @@ func Run(cancel *Canceller, cfg Config) Result {
202226 cancel = NewCanceller()
203227 }
204228 j := &job{cfg: cfg, cancel: cancel, trackID: "github_" + uuid.NewString(), matchers: &matcherRoot{}}
229+ // The masker exists before anything is reported, with the secrets of the job
230+ // registered (Worker.cs:158-171 does it before the job starts); the token and
231+ // the cache token join it once the contexts are built. GitHub's SecretMasker
232+ // has no minimum length (SecretMasker.AddValue ignores only null or empty
233+ // values), so a one-character secret is masked too.
234+ j.masker.Store(newMasker())
235+ registerSecrets(j.masker.Load(), cfg.Secrets)
205236 j.cfg.Reporter = &maskReporter{j: j, next: cfg.Reporter}
206237 defer func() {
207238 if j.fc != nil {
@@ -450,8 +481,11 @@ func (j *job) setUp() ([]qstep, error) {
450481 layout = NewLayout(home, filepath.Base(layout.Workspace), cfg.ToolCache)
451482 state.WithLayout(layout)
452483 }
484+ state.WithJobData(cfg.Secrets, cfg.Vars)
453485 j.state = state
454- j.masker.Store(newMasker(state.Secrets()...))
486+ for _, v := range state.Secrets() {
487+ j.masker.Load().add(v)
488+ }
455489 j.masker.Load().add(cfg.CacheToken)
456490
457491 // Features that arrive in later phases fail the job with a clear message.
@@ -687,6 +721,10 @@ const (
687721 defaultCacheMode = "write"
688722 )
689723
724+// SecretsWithheldLine is the line of "Set up job" for a run whose secrets the
725+// server withheld (an ermite text: GitHub has none for fork pull requests).
726+const SecretsWithheldLine = "Secrets are not available to this job: the run was started by a limited token."
727+
690728 // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source
691729 // and the cache mode.
692730 //
@@ -709,6 +747,9 @@ func (j *job) printTokenInfo() {
709747 j.state.SetGitHub("secret_source", src)
710748 }
711749 j.out(1, "Secret source: "+src)
750+ if j.cfg.SecretsWithheld {
751+ j.out(1, SecretsWithheldLine)
752+ }
712753 j.out(1, "Cache mode: "+defaultCacheMode)
713754 }
714755
@@ -723,7 +764,7 @@ func (j *job) printTokenInfo() {
723764 func (j *job) jobDisplayName() string {
724765 ctx := context.Background()
725766 fallback := func(why string, err error) string {
726- j.cfg.Log.Warn().Err(err).Str("job_key", j.cfg.JobKey).Msg("job display name: " + why + ", using the job key")
767+ j.diagWarn(err, "job display name: "+why+", using the job key", "job_key", j.cfg.JobKey)
727768 return j.cfg.JobKey
728769 }
729770 j.mu.Lock()
@@ -796,3 +837,17 @@ func sameMatrix(a, b *expr.Dictionary) bool {
796837 }
797838 return true
798839 }
840+
841+func (j *job) diag(debug bool, err error, msg string, kv []string) {
842+ ev := j.cfg.Log.Warn()
843+ if debug {
844+ ev = j.cfg.Log.Debug()
845+ }
846+ if err != nil {
847+ ev = ev.Str("error", j.mask(err.Error()))
848+ }
849+ for i := 0; i+1 < len(kv); i += 2 {
850+ ev = ev.Str(kv[i], j.mask(kv[i+1]))
851+ }
852+ ev.Msg(j.mask(msg))
853+}
@@ -78,6 +78,13 @@ type Config struct {
78 // ACTIONS_RUNTIME_TOKEN of the node and docker actions of the job: the cache78 // ACTIONS_RUNTIME_TOKEN of the node and docker actions of the job: the cache
79 // service of the runner. The token is masked in the log.79 // service of the runner. The token is masked in the log.
80 CacheURL, CacheToken string80 CacheURL, CacheToken string
81+ // Secrets and Vars are Job.secrets and Job.vars. Secrets are registered in
82+ // the masker before the first step and form the secrets context; they are
83+ // never logged. Vars are the fallback of the vars context.
84+ Secrets, Vars map[string]string
85+ // SecretsWithheld says that the server did not send secrets because the run
86+ // is not trusted: "Set up job" says so.
87+ SecretsWithheld bool
81 // Debug turns ##[debug] lines on (ACTIONS_STEP_DEBUG).88 // Debug turns ##[debug] lines on (ACTIONS_STEP_DEBUG).
82 Debug bool89 Debug bool
83 // Actions configures the actions of the job (resolution, downloads, Node.js,90 // Actions configures the actions of the job (resolution, downloads, Node.js,
@@ -163,8 +170,8 @@ type job struct {
163 baseEnv []string170 baseEnv []string
164 lookupPath string171 lookupPath string
165 before map[string]bool172 before map[string]bool
166- // masker hides the secrets in everything reported; it is set once the173+ // masker hides the secrets in everything reported; Run sets it, with the
167- // contexts are built, before any step runs.174+ // secrets of the job, before the first report.
168 masker atomic.Pointer[masker]175 masker atomic.Pointer[masker]
169 // actions holds what the pre-job phase loaded (see prepareActions).176 // actions holds what the pre-job phase loaded (see prepareActions).
170 actions *actionState177 actions *actionState
@@ -188,6 +195,23 @@ type job struct {
188 195
189 func (j *job) mask(s string) string { return j.masker.Load().mask(s) }196 func (j *job) mask(s string) string { return j.masker.Load().mask(s) }
190 197
198+// The runner's own diagnostics (Config.Log) bypass the reporter, hence the
199+// masker: a path, a docker message or an error can hold a secret. Every
200+// diagnostic of the executor goes through these helpers, which mask the
201+// rendered error, message and fields (TestDiagnosticsAreMasked checks that no
202+// other code calls Config.Log).
203+
204+// diagWarn logs a warning with an optional error and string fields (key,
205+// value, ...).
206+func (j *job) diagWarn(err error, msg string, kv ...string) {
207+ j.diag(false, err, msg, kv)
208+}
209+
210+// diagDebug is diagWarn at debug level.
211+func (j *job) diagDebug(err error, msg string, kv ...string) {
212+ j.diag(true, err, msg, kv)
213+}
214+
191 func (j *job) now() time.Time {215 func (j *job) now() time.Time {
192 if j.cfg.Now != nil {216 if j.cfg.Now != nil {
193 return j.cfg.Now()217 return j.cfg.Now()
@@ -202,6 +226,13 @@ func Run(cancel *Canceller, cfg Config) Result {
202 cancel = NewCanceller()226 cancel = NewCanceller()
203 }227 }
204 j := &job{cfg: cfg, cancel: cancel, trackID: "github_" + uuid.NewString(), matchers: &matcherRoot{}}228 j := &job{cfg: cfg, cancel: cancel, trackID: "github_" + uuid.NewString(), matchers: &matcherRoot{}}
229+ // The masker exists before anything is reported, with the secrets of the job
230+ // registered (Worker.cs:158-171 does it before the job starts); the token and
231+ // the cache token join it once the contexts are built. GitHub's SecretMasker
232+ // has no minimum length (SecretMasker.AddValue ignores only null or empty
233+ // values), so a one-character secret is masked too.
234+ j.masker.Store(newMasker())
235+ registerSecrets(j.masker.Load(), cfg.Secrets)
205 j.cfg.Reporter = &maskReporter{j: j, next: cfg.Reporter}236 j.cfg.Reporter = &maskReporter{j: j, next: cfg.Reporter}
206 defer func() {237 defer func() {
207 if j.fc != nil {238 if j.fc != nil {
@@ -450,8 +481,11 @@ func (j *job) setUp() ([]qstep, error) {
450 layout = NewLayout(home, filepath.Base(layout.Workspace), cfg.ToolCache)481 layout = NewLayout(home, filepath.Base(layout.Workspace), cfg.ToolCache)
451 state.WithLayout(layout)482 state.WithLayout(layout)
452 }483 }
484+ state.WithJobData(cfg.Secrets, cfg.Vars)
453 j.state = state485 j.state = state
454- j.masker.Store(newMasker(state.Secrets()...))486+ for _, v := range state.Secrets() {
487+ j.masker.Load().add(v)
488+ }
455 j.masker.Load().add(cfg.CacheToken)489 j.masker.Load().add(cfg.CacheToken)
456 490
457 // Features that arrive in later phases fail the job with a clear message.491 // Features that arrive in later phases fail the job with a clear message.
@@ -687,6 +721,10 @@ const (
687 defaultCacheMode = "write"721 defaultCacheMode = "write"
688 )722 )
689 723
724+// SecretsWithheldLine is the line of "Set up job" for a run whose secrets the
725+// server withheld (an ermite text: GitHub has none for fork pull requests).
726+const SecretsWithheldLine = "Secrets are not available to this job: the run was started by a limited token."
727+
690 // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source728 // printTokenInfo prints the GITHUB_TOKEN permissions group, the secret source
691 // and the cache mode.729 // and the cache mode.
692 //730 //
@@ -709,6 +747,9 @@ func (j *job) printTokenInfo() {
709 j.state.SetGitHub("secret_source", src)747 j.state.SetGitHub("secret_source", src)
710 }748 }
711 j.out(1, "Secret source: "+src)749 j.out(1, "Secret source: "+src)
750+ if j.cfg.SecretsWithheld {
751+ j.out(1, SecretsWithheldLine)
752+ }
712 j.out(1, "Cache mode: "+defaultCacheMode)753 j.out(1, "Cache mode: "+defaultCacheMode)
713 }754 }
714 755
@@ -723,7 +764,7 @@ func (j *job) printTokenInfo() {
723 func (j *job) jobDisplayName() string {764 func (j *job) jobDisplayName() string {
724 ctx := context.Background()765 ctx := context.Background()
725 fallback := func(why string, err error) string {766 fallback := func(why string, err error) string {
726- j.cfg.Log.Warn().Err(err).Str("job_key", j.cfg.JobKey).Msg("job display name: " + why + ", using the job key")767+ j.diagWarn(err, "job display name: "+why+", using the job key", "job_key", j.cfg.JobKey)
727 return j.cfg.JobKey768 return j.cfg.JobKey
728 }769 }
729 j.mu.Lock()770 j.mu.Lock()
@@ -796,3 +837,17 @@ func sameMatrix(a, b *expr.Dictionary) bool {
796 }837 }
797 return true838 return true
798 }839 }
840+
841+func (j *job) diag(debug bool, err error, msg string, kv []string) {
842+ ev := j.cfg.Log.Warn()
843+ if debug {
844+ ev = j.cfg.Log.Debug()
845+ }
846+ if err != nil {
847+ ev = ev.Str("error", j.mask(err.Error()))
848+ }
849+ for i := 0; i+1 < len(kv); i += 2 {
850+ ev = ev.Str(kv[i], j.mask(kv[i+1]))
851+ }
852+ ev.Msg(j.mask(msg))
853+}
A internal/executor/secrets_test.go +319 -0
@@ -0,0 +1,319 @@
1+// SPDX-License-Identifier: MIT
2+
3+package executor
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "os"
9+ "path/filepath"
10+
11+ "bnema.dev/ermite-actions/expr"
12+ "encoding/base64"
13+ "encoding/json"
14+ "github.com/bnema/zerowrap"
15+ "net/url"
16+ "strings"
17+ "testing"
18+)
19+
20+// jobSecretMasker is the masker of a job holding these secrets, built the way
21+// Run does: the token first, then the secrets of the job.
22+func jobSecretMasker(t *testing.T, secrets map[string]string) *masker {
23+ t.Helper()
24+ s, _, err := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ s.WithJobData(secrets, nil)
29+ m := newMasker(s.Secrets()...)
30+ registerSecrets(m, secrets)
31+ return m
32+}
33+
34+// A secret is masked as is and in the encodings of CI-2 (Worker.cs:158-171,
35+// HostContext.cs:103-113): base64, JSON, URL.
36+func TestJobSecretIsMaskedInItsEncodings(t *testing.T) {
37+ const sec = `p@ss"w/rd é+1`
38+ m := jobSecretMasker(t, map[string]string{"DB_PASSWORD": sec})
39+ j, _ := json.Marshal(sec)
40+ for name, text := range map[string]string{
41+ "plain": "x " + sec + " y",
42+ "base64": "Basic " + base64.StdEncoding.EncodeToString([]byte(sec)),
43+ "json": "{\"k\":" + string(j) + "}",
44+ "url": "?q=" + strings.ReplaceAll(url.QueryEscape(sec), "+", "%20"), // Uri.EscapeDataString
45+ } {
46+ got := m.mask(text)
47+ if got == text || !strings.Contains(got, "***") {
48+ t.Errorf("%s: %q was not masked: %q", name, text, got)
49+ }
50+ }
51+ if got := m.mask("harmless"); got != "harmless" {
52+ t.Errorf("%q", got)
53+ }
54+}
55+
56+// Worker.cs:158-171: the value is trimmed, and each non-empty trimmed line of a
57+// multi-line value is registered too (split on CR and LF).
58+func TestJobSecretLinesAndTrim(t *testing.T) {
59+ m := jobSecretMasker(t, map[string]string{
60+ "KEY": " \n-----BEGIN KEY-----\r\n line-two-secret \n\n-----END KEY-----\n ",
61+ "PAD": " padded-secret \t\n",
62+ "NONE": "",
63+ "WS": " \r\n ",
64+ })
65+ for _, text := range []string{"-----BEGIN KEY-----", "line-two-secret", "-----END KEY-----", "padded-secret",
66+ "-----BEGIN KEY-----\r\n line-two-secret \n\n-----END KEY-----"} {
67+ if m.mask("a "+text+" b") == "a "+text+" b" {
68+ t.Errorf("%q is not masked", text)
69+ }
70+ }
71+ // empty and blank values register nothing: text is untouched
72+ for _, text := range []string{"", " ", "\r\n", "plain text", "a b"} {
73+ if got := m.mask(text); got != text {
74+ t.Errorf("%q -> %q", text, got)
75+ }
76+ }
77+ // the value as sent, with its padding, is masked as far as the trimmed one
78+ if got := m.mask("v=padded-secret "); got != "v=*** " {
79+ t.Errorf("%q", got)
80+ }
81+}
82+
83+func TestSecretsContext(t *testing.T) {
84+ s, _, err := NewState(tokenContexts(), "", "main", RunnerInfo{}, "")
85+ if err != nil {
86+ t.Fatal(err)
87+ }
88+ s.WithJobData(map[string]string{"B": "2", "A": "1", "GITHUB_TOKEN": "forged", "EMPTY": ""}, nil)
89+ d := s.secrets()
90+ got := map[string]string{}
91+ for _, p := range d.Pairs() {
92+ got[p.Key] = str(d, p.Key)
93+ }
94+ if len(got) != 4 || got["GITHUB_TOKEN"] != testToken || got["A"] != "1" || got["B"] != "2" || got["EMPTY"] != "" {
95+ t.Errorf("secrets context %v", got)
96+ }
97+ // without a token and without secrets the context is empty
98+ s0, _, _ := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
99+ if s0.secrets().Len() != 0 {
100+ t.Error("secrets context is not empty")
101+ }
102+}
103+
104+func TestVarsContext(t *testing.T) {
105+ vars := map[string]string{"A": "from-job"}
106+ // the contexts JSON has no vars: Job.vars is the fallback
107+ s, _, _ := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
108+ s.WithJobData(nil, vars)
109+ if got := dictOf(t, s.other["vars"]); got != "A=from-job" {
110+ t.Errorf("fallback: %s", got)
111+ }
112+ // the contexts JSON has vars: it wins
113+ withVars := strings.Replace(testGitHubContext("main"), `{"github"`, `{"vars":{"A":"from-contexts","B":"b"},"github"`, 1)
114+ s2, _, err := NewState(withVars, "", "main", RunnerInfo{}, "")
115+ if err != nil {
116+ t.Fatal(err)
117+ }
118+ s2.WithJobData(nil, vars)
119+ if got := dictOf(t, s2.other["vars"]); got != "A=from-contexts B=b" {
120+ t.Errorf("contexts: %s", got)
121+ }
122+ // neither: an empty object
123+ s3, _, _ := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
124+ s3.WithJobData(nil, nil)
125+ if got := dictOf(t, s3.other["vars"]); got != "" {
126+ t.Errorf("none: %s", got)
127+ }
128+}
129+
130+func TestJobSecretsEndToEnd(t *testing.T) {
131+ needTools(t, "bash")
132+ const (
133+ pw = "hunter2-secret"
134+ key = "-----BEGIN-----\nsecond-line-secret\n-----END-----"
135+ )
136+ yaml := `on: push
137+jobs:
138+ main:
139+ runs-on: ubuntu-24.04
140+ outputs:
141+ leak: ${{ steps.s.outputs.v }}
142+ leak64: ${{ steps.s.outputs.b }}
143+ line: ${{ steps.s.outputs.l }}
144+ fine: ${{ steps.s.outputs.w }}
145+ steps:
146+ - id: s
147+ env:
148+ PW: ${{ secrets.PW }}
149+ KEY: ${{ secrets.KEY }}
150+ V: ${{ vars.V }}
151+ MISSING: ${{ secrets.NOPE }}
152+ run: |
153+ echo "pw=$PW"
154+ echo "b64=$(printf %s "$PW" | base64)"
155+ echo "line=$(printf %s "$KEY" | sed -n 2p)"
156+ echo "v=$V missing=[$MISSING]"
157+ echo "v=pre-$PW-post" >> "$GITHUB_OUTPUT"
158+ echo "b=$(printf %s "$PW" | base64)" >> "$GITHUB_OUTPUT"
159+ echo "l=second-line-secret" >> "$GITHUB_OUTPUT"
160+ echo "w=$V" >> "$GITHUB_OUTPUT"
161+`
162+ rec, res := runLocal(t, yaml, "main", func(c *Config) {
163+ c.Secrets = map[string]string{"PW": pw, "KEY": key}
164+ c.Vars = map[string]string{"V": "visible"}
165+ })
166+ if res.Conclusion != ConclusionSuccess {
167+ t.Fatalf("%+v", res)
168+ }
169+ var all []string
170+ for _, s := range rec.steps {
171+ all = append(all, s.Name, logOf(s))
172+ }
173+ joined := strings.Join(all, "\n")
174+ for _, secret := range []string{pw, "second-line-secret", base64.StdEncoding.EncodeToString([]byte(pw))} {
175+ if strings.Contains(joined, secret) {
176+ t.Errorf("%q leaked in the log:\n%s", secret, joined)
177+ }
178+ }
179+ for _, want := range []string{"pw=***", "line=***", "v=visible missing=[]"} {
180+ if !strings.Contains(joined, want) {
181+ t.Errorf("log lacks %q:\n%s", want, joined)
182+ }
183+ }
184+ if len(res.Outputs) != 1 || res.Outputs["fine"] != "visible" {
185+ t.Errorf("outputs %v", res.Outputs)
186+ }
187+ done := logOf(rec.step(t, CompleteJobName))
188+ for _, n := range []string{"leak", "leak64", "line"} {
189+ if w := "##[warning]Skip output '" + n + "' since it may contain secret."; !strings.Contains(done, w) {
190+ t.Errorf("warning %q missing:\n%s", w, done)
191+ }
192+ }
193+ if strings.Contains(done, "Skip output 'fine'") {
194+ t.Error("fine output skipped")
195+ }
196+}
197+
198+func TestSecretsWithheldLine(t *testing.T) {
199+ needTools(t, "bash")
200+ yaml := wfHead + " steps:\n - run: echo hi\n"
201+ with := setupLog(t, yaml, "j", func(c *Config) { c.SecretsWithheld = true })
202+ if n := strings.Count(with, SecretsWithheldLine); n != 1 || SecretsWithheldLine != "Secrets are not available to this job: the run was started by a limited token." {
203+ t.Errorf("line count %d:\n%s", n, with)
204+ }
205+ if without := setupLog(t, yaml, "j", nil); strings.Contains(without, "Secrets are not available") {
206+ t.Errorf("printed without the flag:\n%s", without)
207+ }
208+}
209+
210+// dictOf renders a string dictionary as "K=v K=v" in order.
211+func dictOf(t *testing.T, v expr.Value) string {
212+ t.Helper()
213+ d, ok := v.(*expr.Dictionary)
214+ if !ok {
215+ t.Fatalf("not a dictionary: %T", v)
216+ }
217+ var parts []string
218+ for _, p := range d.Pairs() {
219+ parts = append(parts, p.Key+"="+str(d, p.Key))
220+ }
221+ return strings.Join(parts, " ")
222+}
223+
224+// The first lines of "Set up job" are reported before the contexts exist: the
225+// secrets of the job are already in the masker then (GO-002). GitHub's
226+// SecretMasker has no minimum length (SecretMasker.AddValue ignores only null
227+// or empty values, SecretMasker.cs:78-84), so a one-character secret is masked.
228+func TestSecretsAreMaskedFromTheFirstLine(t *testing.T) {
229+ needTools(t, "bash")
230+ yaml := wfHead + " steps:\n - run: echo hi\n"
231+ rec, res := runLocal(t, yaml, "j", func(c *Config) {
232+ c.Secrets = map[string]string{"ONE": "7", "LONG": "topsecretvalue"}
233+ c.RunnerVersion = "v7.1-topsecretvalue"
234+ })
235+ if res.Conclusion != ConclusionSuccess {
236+ t.Fatalf("%+v", res)
237+ }
238+ first := rec.steps[0].Log[0]
239+ if first != "Current runner version: 'v***.1-***'" {
240+ t.Errorf("first line %q", first)
241+ }
242+}
243+
244+// An early failure of "Set up job" (before the contexts are built) is masked in
245+// the log and in the error of the job.
246+func TestEarlySetupFailureIsMasked(t *testing.T) {
247+ yaml := wfHead + " steps:\n - run: echo hi\n"
248+ rec, res := runLocal(t, yaml, "no-such-job-S3CRET", func(c *Config) {
249+ c.Secrets = map[string]string{"S": "S3CRET", "ONE": "-"}
250+ })
251+ if res.Conclusion != ConclusionFailure {
252+ t.Fatalf("%+v", res)
253+ }
254+ all := logOf(rec.steps[0]) + "\n" + res.Error + "\n" + rec.result.Error
255+ if strings.Contains(all, "S3CRET") || strings.Contains(all, "-") || !strings.Contains(all, "no***such***job***") {
256+ t.Errorf("not masked:\n%s", all)
257+ }
258+}
259+
260+// The diagnostics of the runner (Config.Log) bypass the reporter: they are
261+// masked by diag (GO-001). A path with a secret in a failing hashFiles reaches
262+// the diagnostics through the error.
263+func TestDiagnosticsAreMasked(t *testing.T) {
264+ needTools(t, "bash")
265+ const sec = "s3cr3t-dir-name"
266+ yaml := `on: push
267+jobs:
268+ j:
269+ runs-on: x
270+ steps:
271+ - run: ln -s missing ` + sec + `
272+ - run: echo ${{ hashFiles('**') }}
273+`
274+ var logs bytes.Buffer
275+ rec, res := runLocal(t, yaml, "j", func(c *Config) {
276+ c.Secrets = map[string]string{"D": sec}
277+ c.Log = zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &logs})
278+ })
279+ if res.Conclusion != ConclusionFailure {
280+ t.Fatalf("%+v", res)
281+ }
282+ if !strings.Contains(logs.String(), "hashFiles failed") {
283+ t.Fatalf("no diagnostic:\n%s", logs.String())
284+ }
285+ if strings.Contains(logs.String(), sec) || !strings.Contains(logs.String(), "/***: statat ***") {
286+ t.Errorf("diagnostics not masked:\n%s", logs.String())
287+ }
288+ for _, s := range rec.steps {
289+ if strings.Contains(logOf(s), sec) {
290+ t.Errorf("job log leaks:\n%s", logOf(s))
291+ }
292+ }
293+
294+ // The helper masks message, error and fields.
295+ logs.Reset()
296+ j := &job{cfg: Config{Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &logs})}}
297+ j.masker.Store(newMasker(sec))
298+ j.diagWarn(errors.New("open "+sec+": no"), "msg "+sec, "k", "v "+sec)
299+ j.diagDebug(nil, "d "+sec)
300+ if strings.Contains(logs.String(), sec) || strings.Count(logs.String(), "***") != 4 {
301+ t.Errorf("helper: %s", logs.String())
302+ }
303+}
304+
305+// Config.Log is used only by diag: any other use would bypass the masker.
306+func TestOnlyDiagUsesTheDiagnosticsLogger(t *testing.T) {
307+ files, _ := filepath.Glob("*.go")
308+ for _, f := range files {
309+ if strings.HasSuffix(f, "_test.go") {
310+ continue
311+ }
312+ b, _ := os.ReadFile(f)
313+ n := strings.Count(string(b), "cfg.Log")
314+ // job.go: the two uses are in diag.
315+ if (f == "job.go" && n != 2) || (f != "job.go" && n != 0) {
316+ t.Errorf("%s uses cfg.Log %d times: use diagWarn or diagDebug", f, n)
317+ }
318+ }
319+}
@@ -0,0 +1,319 @@
1+// SPDX-License-Identifier: MIT
2+
3+package executor
4+
5+import (
6+ "bytes"
7+ "errors"
8+ "os"
9+ "path/filepath"
10+
11+ "bnema.dev/ermite-actions/expr"
12+ "encoding/base64"
13+ "encoding/json"
14+ "github.com/bnema/zerowrap"
15+ "net/url"
16+ "strings"
17+ "testing"
18+)
19+
20+// jobSecretMasker is the masker of a job holding these secrets, built the way
21+// Run does: the token first, then the secrets of the job.
22+func jobSecretMasker(t *testing.T, secrets map[string]string) *masker {
23+ t.Helper()
24+ s, _, err := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ s.WithJobData(secrets, nil)
29+ m := newMasker(s.Secrets()...)
30+ registerSecrets(m, secrets)
31+ return m
32+}
33+
34+// A secret is masked as is and in the encodings of CI-2 (Worker.cs:158-171,
35+// HostContext.cs:103-113): base64, JSON, URL.
36+func TestJobSecretIsMaskedInItsEncodings(t *testing.T) {
37+ const sec = `p@ss"w/rd é+1`
38+ m := jobSecretMasker(t, map[string]string{"DB_PASSWORD": sec})
39+ j, _ := json.Marshal(sec)
40+ for name, text := range map[string]string{
41+ "plain": "x " + sec + " y",
42+ "base64": "Basic " + base64.StdEncoding.EncodeToString([]byte(sec)),
43+ "json": "{\"k\":" + string(j) + "}",
44+ "url": "?q=" + strings.ReplaceAll(url.QueryEscape(sec), "+", "%20"), // Uri.EscapeDataString
45+ } {
46+ got := m.mask(text)
47+ if got == text || !strings.Contains(got, "***") {
48+ t.Errorf("%s: %q was not masked: %q", name, text, got)
49+ }
50+ }
51+ if got := m.mask("harmless"); got != "harmless" {
52+ t.Errorf("%q", got)
53+ }
54+}
55+
56+// Worker.cs:158-171: the value is trimmed, and each non-empty trimmed line of a
57+// multi-line value is registered too (split on CR and LF).
58+func TestJobSecretLinesAndTrim(t *testing.T) {
59+ m := jobSecretMasker(t, map[string]string{
60+ "KEY": " \n-----BEGIN KEY-----\r\n line-two-secret \n\n-----END KEY-----\n ",
61+ "PAD": " padded-secret \t\n",
62+ "NONE": "",
63+ "WS": " \r\n ",
64+ })
65+ for _, text := range []string{"-----BEGIN KEY-----", "line-two-secret", "-----END KEY-----", "padded-secret",
66+ "-----BEGIN KEY-----\r\n line-two-secret \n\n-----END KEY-----"} {
67+ if m.mask("a "+text+" b") == "a "+text+" b" {
68+ t.Errorf("%q is not masked", text)
69+ }
70+ }
71+ // empty and blank values register nothing: text is untouched
72+ for _, text := range []string{"", " ", "\r\n", "plain text", "a b"} {
73+ if got := m.mask(text); got != text {
74+ t.Errorf("%q -> %q", text, got)
75+ }
76+ }
77+ // the value as sent, with its padding, is masked as far as the trimmed one
78+ if got := m.mask("v=padded-secret "); got != "v=*** " {
79+ t.Errorf("%q", got)
80+ }
81+}
82+
83+func TestSecretsContext(t *testing.T) {
84+ s, _, err := NewState(tokenContexts(), "", "main", RunnerInfo{}, "")
85+ if err != nil {
86+ t.Fatal(err)
87+ }
88+ s.WithJobData(map[string]string{"B": "2", "A": "1", "GITHUB_TOKEN": "forged", "EMPTY": ""}, nil)
89+ d := s.secrets()
90+ got := map[string]string{}
91+ for _, p := range d.Pairs() {
92+ got[p.Key] = str(d, p.Key)
93+ }
94+ if len(got) != 4 || got["GITHUB_TOKEN"] != testToken || got["A"] != "1" || got["B"] != "2" || got["EMPTY"] != "" {
95+ t.Errorf("secrets context %v", got)
96+ }
97+ // without a token and without secrets the context is empty
98+ s0, _, _ := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
99+ if s0.secrets().Len() != 0 {
100+ t.Error("secrets context is not empty")
101+ }
102+}
103+
104+func TestVarsContext(t *testing.T) {
105+ vars := map[string]string{"A": "from-job"}
106+ // the contexts JSON has no vars: Job.vars is the fallback
107+ s, _, _ := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
108+ s.WithJobData(nil, vars)
109+ if got := dictOf(t, s.other["vars"]); got != "A=from-job" {
110+ t.Errorf("fallback: %s", got)
111+ }
112+ // the contexts JSON has vars: it wins
113+ withVars := strings.Replace(testGitHubContext("main"), `{"github"`, `{"vars":{"A":"from-contexts","B":"b"},"github"`, 1)
114+ s2, _, err := NewState(withVars, "", "main", RunnerInfo{}, "")
115+ if err != nil {
116+ t.Fatal(err)
117+ }
118+ s2.WithJobData(nil, vars)
119+ if got := dictOf(t, s2.other["vars"]); got != "A=from-contexts B=b" {
120+ t.Errorf("contexts: %s", got)
121+ }
122+ // neither: an empty object
123+ s3, _, _ := NewState(testGitHubContext("main"), "", "main", RunnerInfo{}, "")
124+ s3.WithJobData(nil, nil)
125+ if got := dictOf(t, s3.other["vars"]); got != "" {
126+ t.Errorf("none: %s", got)
127+ }
128+}
129+
130+func TestJobSecretsEndToEnd(t *testing.T) {
131+ needTools(t, "bash")
132+ const (
133+ pw = "hunter2-secret"
134+ key = "-----BEGIN-----\nsecond-line-secret\n-----END-----"
135+ )
136+ yaml := `on: push
137+jobs:
138+ main:
139+ runs-on: ubuntu-24.04
140+ outputs:
141+ leak: ${{ steps.s.outputs.v }}
142+ leak64: ${{ steps.s.outputs.b }}
143+ line: ${{ steps.s.outputs.l }}
144+ fine: ${{ steps.s.outputs.w }}
145+ steps:
146+ - id: s
147+ env:
148+ PW: ${{ secrets.PW }}
149+ KEY: ${{ secrets.KEY }}
150+ V: ${{ vars.V }}
151+ MISSING: ${{ secrets.NOPE }}
152+ run: |
153+ echo "pw=$PW"
154+ echo "b64=$(printf %s "$PW" | base64)"
155+ echo "line=$(printf %s "$KEY" | sed -n 2p)"
156+ echo "v=$V missing=[$MISSING]"
157+ echo "v=pre-$PW-post" >> "$GITHUB_OUTPUT"
158+ echo "b=$(printf %s "$PW" | base64)" >> "$GITHUB_OUTPUT"
159+ echo "l=second-line-secret" >> "$GITHUB_OUTPUT"
160+ echo "w=$V" >> "$GITHUB_OUTPUT"
161+`
162+ rec, res := runLocal(t, yaml, "main", func(c *Config) {
163+ c.Secrets = map[string]string{"PW": pw, "KEY": key}
164+ c.Vars = map[string]string{"V": "visible"}
165+ })
166+ if res.Conclusion != ConclusionSuccess {
167+ t.Fatalf("%+v", res)
168+ }
169+ var all []string
170+ for _, s := range rec.steps {
171+ all = append(all, s.Name, logOf(s))
172+ }
173+ joined := strings.Join(all, "\n")
174+ for _, secret := range []string{pw, "second-line-secret", base64.StdEncoding.EncodeToString([]byte(pw))} {
175+ if strings.Contains(joined, secret) {
176+ t.Errorf("%q leaked in the log:\n%s", secret, joined)
177+ }
178+ }
179+ for _, want := range []string{"pw=***", "line=***", "v=visible missing=[]"} {
180+ if !strings.Contains(joined, want) {
181+ t.Errorf("log lacks %q:\n%s", want, joined)
182+ }
183+ }
184+ if len(res.Outputs) != 1 || res.Outputs["fine"] != "visible" {
185+ t.Errorf("outputs %v", res.Outputs)
186+ }
187+ done := logOf(rec.step(t, CompleteJobName))
188+ for _, n := range []string{"leak", "leak64", "line"} {
189+ if w := "##[warning]Skip output '" + n + "' since it may contain secret."; !strings.Contains(done, w) {
190+ t.Errorf("warning %q missing:\n%s", w, done)
191+ }
192+ }
193+ if strings.Contains(done, "Skip output 'fine'") {
194+ t.Error("fine output skipped")
195+ }
196+}
197+
198+func TestSecretsWithheldLine(t *testing.T) {
199+ needTools(t, "bash")
200+ yaml := wfHead + " steps:\n - run: echo hi\n"
201+ with := setupLog(t, yaml, "j", func(c *Config) { c.SecretsWithheld = true })
202+ if n := strings.Count(with, SecretsWithheldLine); n != 1 || SecretsWithheldLine != "Secrets are not available to this job: the run was started by a limited token." {
203+ t.Errorf("line count %d:\n%s", n, with)
204+ }
205+ if without := setupLog(t, yaml, "j", nil); strings.Contains(without, "Secrets are not available") {
206+ t.Errorf("printed without the flag:\n%s", without)
207+ }
208+}
209+
210+// dictOf renders a string dictionary as "K=v K=v" in order.
211+func dictOf(t *testing.T, v expr.Value) string {
212+ t.Helper()
213+ d, ok := v.(*expr.Dictionary)
214+ if !ok {
215+ t.Fatalf("not a dictionary: %T", v)
216+ }
217+ var parts []string
218+ for _, p := range d.Pairs() {
219+ parts = append(parts, p.Key+"="+str(d, p.Key))
220+ }
221+ return strings.Join(parts, " ")
222+}
223+
224+// The first lines of "Set up job" are reported before the contexts exist: the
225+// secrets of the job are already in the masker then (GO-002). GitHub's
226+// SecretMasker has no minimum length (SecretMasker.AddValue ignores only null
227+// or empty values, SecretMasker.cs:78-84), so a one-character secret is masked.
228+func TestSecretsAreMaskedFromTheFirstLine(t *testing.T) {
229+ needTools(t, "bash")
230+ yaml := wfHead + " steps:\n - run: echo hi\n"
231+ rec, res := runLocal(t, yaml, "j", func(c *Config) {
232+ c.Secrets = map[string]string{"ONE": "7", "LONG": "topsecretvalue"}
233+ c.RunnerVersion = "v7.1-topsecretvalue"
234+ })
235+ if res.Conclusion != ConclusionSuccess {
236+ t.Fatalf("%+v", res)
237+ }
238+ first := rec.steps[0].Log[0]
239+ if first != "Current runner version: 'v***.1-***'" {
240+ t.Errorf("first line %q", first)
241+ }
242+}
243+
244+// An early failure of "Set up job" (before the contexts are built) is masked in
245+// the log and in the error of the job.
246+func TestEarlySetupFailureIsMasked(t *testing.T) {
247+ yaml := wfHead + " steps:\n - run: echo hi\n"
248+ rec, res := runLocal(t, yaml, "no-such-job-S3CRET", func(c *Config) {
249+ c.Secrets = map[string]string{"S": "S3CRET", "ONE": "-"}
250+ })
251+ if res.Conclusion != ConclusionFailure {
252+ t.Fatalf("%+v", res)
253+ }
254+ all := logOf(rec.steps[0]) + "\n" + res.Error + "\n" + rec.result.Error
255+ if strings.Contains(all, "S3CRET") || strings.Contains(all, "-") || !strings.Contains(all, "no***such***job***") {
256+ t.Errorf("not masked:\n%s", all)
257+ }
258+}
259+
260+// The diagnostics of the runner (Config.Log) bypass the reporter: they are
261+// masked by diag (GO-001). A path with a secret in a failing hashFiles reaches
262+// the diagnostics through the error.
263+func TestDiagnosticsAreMasked(t *testing.T) {
264+ needTools(t, "bash")
265+ const sec = "s3cr3t-dir-name"
266+ yaml := `on: push
267+jobs:
268+ j:
269+ runs-on: x
270+ steps:
271+ - run: ln -s missing ` + sec + `
272+ - run: echo ${{ hashFiles('**') }}
273+`
274+ var logs bytes.Buffer
275+ rec, res := runLocal(t, yaml, "j", func(c *Config) {
276+ c.Secrets = map[string]string{"D": sec}
277+ c.Log = zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &logs})
278+ })
279+ if res.Conclusion != ConclusionFailure {
280+ t.Fatalf("%+v", res)
281+ }
282+ if !strings.Contains(logs.String(), "hashFiles failed") {
283+ t.Fatalf("no diagnostic:\n%s", logs.String())
284+ }
285+ if strings.Contains(logs.String(), sec) || !strings.Contains(logs.String(), "/***: statat ***") {
286+ t.Errorf("diagnostics not masked:\n%s", logs.String())
287+ }
288+ for _, s := range rec.steps {
289+ if strings.Contains(logOf(s), sec) {
290+ t.Errorf("job log leaks:\n%s", logOf(s))
291+ }
292+ }
293+
294+ // The helper masks message, error and fields.
295+ logs.Reset()
296+ j := &job{cfg: Config{Log: zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &logs})}}
297+ j.masker.Store(newMasker(sec))
298+ j.diagWarn(errors.New("open "+sec+": no"), "msg "+sec, "k", "v "+sec)
299+ j.diagDebug(nil, "d "+sec)
300+ if strings.Contains(logs.String(), sec) || strings.Count(logs.String(), "***") != 4 {
301+ t.Errorf("helper: %s", logs.String())
302+ }
303+}
304+
305+// Config.Log is used only by diag: any other use would bypass the masker.
306+func TestOnlyDiagUsesTheDiagnosticsLogger(t *testing.T) {
307+ files, _ := filepath.Glob("*.go")
308+ for _, f := range files {
309+ if strings.HasSuffix(f, "_test.go") {
310+ continue
311+ }
312+ b, _ := os.ReadFile(f)
313+ n := strings.Count(string(b), "cfg.Log")
314+ // job.go: the two uses are in diag.
315+ if (f == "job.go" && n != 2) || (f != "job.go" && n != 0) {
316+ t.Errorf("%s uses cfg.Log %d times: use diagWarn or diagDebug", f, n)
317+ }
318+ }
319+}
M internal/guest/exec.go +47 -17
@@ -190,41 +190,71 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err
190190 }}}); err != nil {
191191 return 1, fmt.Errorf("send hello: %w", err)
192192 }
193- run, err := waitForRun(ctx, conn, cfg.RunTimeout)
193+ run, vals, err := waitForRun(ctx, conn, cfg.RunTimeout)
194194 if err != nil {
195195 return 1, err
196196 }
197197 if run.GetJob() != nil {
198- return sendExit(conn, runJob(ctx, conn, cfg, run.GetJob()), nil)
198+ return sendExit(conn, runJob(ctx, conn, cfg, run.GetJob(), vals), nil)
199199 }
200200 return runStub(ctx, conn, cfg, run)
201201 }
202202
203+// jobValues are the secrets and variables of the job that the host sent in
204+// NamedValue frames before Run. They stay in memory.
205+type jobValues struct{ secrets, vars map[string]string }
206+
207+// maxJobValues bounds what a host can make the executor keep before Run.
208+const maxJobValues = 64 << 20
209+
203210 // waitForRun waits for the Run frame of the host, bounded: a host that never
204-// answers must not pin the sandbox.
205-func waitForRun(ctx context.Context, conn *execstream.Conn, timeout time.Duration) (*execpb.Run, error) {
211+// answers must not pin the sandbox. The NamedValue frames that come before it
212+// are collected.
213+func waitForRun(ctx context.Context, conn *execstream.Conn, timeout time.Duration) (*execpb.Run, jobValues, error) {
206214 type recv struct {
207- f *execpb.Frame
208- err error
215+ run *execpb.Run
216+ vals jobValues
217+ err error
209218 }
210219 first := make(chan recv, 1)
211- go func() { f, err := conn.Recv(); first <- recv{f, err} }()
220+ go func() {
221+ vals := jobValues{secrets: map[string]string{}, vars: map[string]string{}}
222+ size := 0
223+ for {
224+ f, err := conn.Recv()
225+ if err != nil {
226+ first <- recv{err: fmt.Errorf("waiting for run: %w", err)}
227+ return
228+ }
229+ switch b := f.Body.(type) {
230+ case *execpb.Frame_Run:
231+ first <- recv{run: b.Run, vals: vals}
232+ return
233+ case *execpb.Frame_Secret:
234+ size += len(b.Secret.GetName()) + len(b.Secret.GetValue())
235+ vals.secrets[b.Secret.GetName()] = b.Secret.GetValue()
236+ case *execpb.Frame_Var:
237+ size += len(b.Var.GetName()) + len(b.Var.GetValue())
238+ vals.vars[b.Var.GetName()] = b.Var.GetValue()
239+ default:
240+ first <- recv{err: errors.New("first frame from host is not run")}
241+ return
242+ }
243+ if size > maxJobValues {
244+ first <- recv{err: errors.New("the host sent too many secrets and variables before run")}
245+ return
246+ }
247+ }
248+ }()
212249 timer := time.NewTimer(timeout)
213250 defer timer.Stop()
214251 select {
215252 case r := <-first:
216- if r.err != nil {
217- return nil, fmt.Errorf("waiting for run: %w", r.err)
218- }
219- run := r.f.GetRun()
220- if run == nil {
221- return nil, errors.New("first frame from host is not run")
222- }
223- return run, nil
253+ return r.run, r.vals, r.err
224254 case <-timer.C:
225- return nil, errors.New("timed out waiting for the host's run frame")
255+ return nil, jobValues{}, errors.New("timed out waiting for the host's run frame")
226256 case <-ctx.Done():
227- return nil, ctx.Err()
257+ return nil, jobValues{}, ctx.Err()
228258 }
229259 }
230260
@@ -190,41 +190,71 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err
190 }}}); err != nil {190 }}}); err != nil {
191 return 1, fmt.Errorf("send hello: %w", err)191 return 1, fmt.Errorf("send hello: %w", err)
192 }192 }
193- run, err := waitForRun(ctx, conn, cfg.RunTimeout)193+ run, vals, err := waitForRun(ctx, conn, cfg.RunTimeout)
194 if err != nil {194 if err != nil {
195 return 1, err195 return 1, err
196 }196 }
197 if run.GetJob() != nil {197 if run.GetJob() != nil {
198- return sendExit(conn, runJob(ctx, conn, cfg, run.GetJob()), nil)198+ return sendExit(conn, runJob(ctx, conn, cfg, run.GetJob(), vals), nil)
199 }199 }
200 return runStub(ctx, conn, cfg, run)200 return runStub(ctx, conn, cfg, run)
201 }201 }
202 202
203+// jobValues are the secrets and variables of the job that the host sent in
204+// NamedValue frames before Run. They stay in memory.
205+type jobValues struct{ secrets, vars map[string]string }
206+
207+// maxJobValues bounds what a host can make the executor keep before Run.
208+const maxJobValues = 64 << 20
209+
203 // waitForRun waits for the Run frame of the host, bounded: a host that never210 // waitForRun waits for the Run frame of the host, bounded: a host that never
204-// answers must not pin the sandbox.211+// answers must not pin the sandbox. The NamedValue frames that come before it
205-func waitForRun(ctx context.Context, conn *execstream.Conn, timeout time.Duration) (*execpb.Run, error) {212+// are collected.
213+func waitForRun(ctx context.Context, conn *execstream.Conn, timeout time.Duration) (*execpb.Run, jobValues, error) {
206 type recv struct {214 type recv struct {
207- f *execpb.Frame215+ run *execpb.Run
208- err error216+ vals jobValues
217+ err error
209 }218 }
210 first := make(chan recv, 1)219 first := make(chan recv, 1)
211- go func() { f, err := conn.Recv(); first <- recv{f, err} }()220+ go func() {
221+ vals := jobValues{secrets: map[string]string{}, vars: map[string]string{}}
222+ size := 0
223+ for {
224+ f, err := conn.Recv()
225+ if err != nil {
226+ first <- recv{err: fmt.Errorf("waiting for run: %w", err)}
227+ return
228+ }
229+ switch b := f.Body.(type) {
230+ case *execpb.Frame_Run:
231+ first <- recv{run: b.Run, vals: vals}
232+ return
233+ case *execpb.Frame_Secret:
234+ size += len(b.Secret.GetName()) + len(b.Secret.GetValue())
235+ vals.secrets[b.Secret.GetName()] = b.Secret.GetValue()
236+ case *execpb.Frame_Var:
237+ size += len(b.Var.GetName()) + len(b.Var.GetValue())
238+ vals.vars[b.Var.GetName()] = b.Var.GetValue()
239+ default:
240+ first <- recv{err: errors.New("first frame from host is not run")}
241+ return
242+ }
243+ if size > maxJobValues {
244+ first <- recv{err: errors.New("the host sent too many secrets and variables before run")}
245+ return
246+ }
247+ }
248+ }()
212 timer := time.NewTimer(timeout)249 timer := time.NewTimer(timeout)
213 defer timer.Stop()250 defer timer.Stop()
214 select {251 select {
215 case r := <-first:252 case r := <-first:
216- if r.err != nil {253+ return r.run, r.vals, r.err
217- return nil, fmt.Errorf("waiting for run: %w", r.err)
218- }
219- run := r.f.GetRun()
220- if run == nil {
221- return nil, errors.New("first frame from host is not run")
222- }
223- return run, nil
224 case <-timer.C:254 case <-timer.C:
225- return nil, errors.New("timed out waiting for the host's run frame")255+ return nil, jobValues{}, errors.New("timed out waiting for the host's run frame")
226 case <-ctx.Done():256 case <-ctx.Done():
227- return nil, ctx.Err()257+ return nil, jobValues{}, ctx.Err()
228 }258 }
229 }259 }
230 260
M internal/guest/jobexec.go +2 -1
@@ -204,7 +204,7 @@ func cancelKind(k execpb.CancelKind) executor.CancelReason {
204204
205205 // runJob runs the job of a Run frame with the executor and reports on conn.
206206 // It returns the exit code to report.
207-func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *execpb.JobSpec) int {
207+func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *execpb.JobSpec, vals jobValues) int {
208208 su, err := LookupStepUser(cfg.StepUser, cfg.AllowRootSteps)
209209 if err != nil {
210210 // Reported like any failure of "Set up job": the job fails, with the reason
@@ -259,6 +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(),
262263 SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(),
263264 TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay,
264265 })
@@ -204,7 +204,7 @@ func cancelKind(k execpb.CancelKind) executor.CancelReason {
204 204
205 // runJob runs the job of a Run frame with the executor and reports on conn.205 // runJob runs the job of a Run frame with the executor and reports on conn.
206 // It returns the exit code to report.206 // It returns the exit code to report.
207-func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *execpb.JobSpec) int {207+func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *execpb.JobSpec, vals jobValues) int {
208 su, err := LookupStepUser(cfg.StepUser, cfg.AllowRootSteps)208 su, err := LookupStepUser(cfg.StepUser, cfg.AllowRootSteps)
209 if err != nil {209 if err != nil {
210 // Reported like any failure of "Set up job": the job fails, with the reason210 // Reported like any failure of "Set up job": the job fails, with the reason
@@ -259,6 +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 SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(),263 SendAnnotations: spec.GetSendAnnotations(), SendSummaries: spec.GetSendSummaries(),
263 TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay,264 TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay,
264 })265 })
A internal/guest/values_test.go +76 -0
@@ -0,0 +1,76 @@
1+// SPDX-License-Identifier: MIT
2+
3+package guest
4+
5+import (
6+ "context"
7+ "fmt"
8+ "net"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "bnema.dev/ermite-runner/internal/execstream"
14+ "bnema.dev/ermite-runner/internal/execstream/execpb"
15+)
16+
17+// 100 secrets of 48 KiB (4.8 MB) do not fit one frame (MaxFrame is 2 MiB) but
18+// do as NamedValue frames: this crosses the real transport, DriveJob on the
19+// host side and waitForRun on the executor side.
20+func TestManyLargeSecretsCrossTheStream(t *testing.T) {
21+ secrets := map[string]string{}
22+ for i := 0; i < 100; i++ {
23+ secrets[fmt.Sprintf("S%03d", i)] = strings.Repeat(string(rune('a'+i%26)), 48<<10)
24+ }
25+ vars := map[string]string{"A": "1", "B": "2"}
26+
27+ // the old shape, every secret in the Run frame, would not have worked
28+ err := execstream.NewConn(discard{}).Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: &execpb.Run{Job: &execpb.JobSpec{Contexts: strings.Join(valuesOf(secrets), "")}}}})
29+ if err == nil {
30+ t.Fatal("test premise: all the secrets must exceed one frame")
31+ }
32+
33+ hostSide, guestSide := net.Pipe()
34+ defer func() { _ = hostSide.Close(); _ = guestSide.Close() }()
35+ got := make(chan error, 1)
36+ go func() {
37+ c := execstream.NewConn(guestSide)
38+ _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})
39+ run, vals, err := waitForRun(context.Background(), c, 10*time.Second)
40+ switch {
41+ case err != nil:
42+ got <- err
43+ case run.GetJob().GetJobKey() != "k" || len(vals.secrets) != 100 || len(vals.vars) != 2 || vals.vars["B"] != "2":
44+ got <- fmt.Errorf("run %v, %d secrets, vars %v", run.GetJob().GetJobKey(), len(vals.secrets), vals.vars)
45+ default:
46+ for k, v := range secrets {
47+ if vals.secrets[k] != v {
48+ got <- fmt.Errorf("secret %s differs", k)
49+ return
50+ }
51+ }
52+ got <- nil
53+ _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})
54+ }
55+ }()
56+ _, exit, err := execstream.DriveJob(hostSide, &execpb.Run{Job: &execpb.JobSpec{JobKey: "k"}}, nil, execstream.JobHandler{Secrets: secrets, Vars: vars})
57+ if err != nil || exit == nil {
58+ t.Fatalf("DriveJob: %v", err)
59+ }
60+ if err := <-got; err != nil {
61+ t.Fatal(err)
62+ }
63+}
64+
65+type discard struct{}
66+
67+func (discard) Write(p []byte) (int, error) { return len(p), nil }
68+func (discard) Read([]byte) (int, error) { select {} }
69+
70+func valuesOf(m map[string]string) []string {
71+ var out []string
72+ for _, v := range m {
73+ out = append(out, v)
74+ }
75+ return out
76+}
@@ -0,0 +1,76 @@
1+// SPDX-License-Identifier: MIT
2+
3+package guest
4+
5+import (
6+ "context"
7+ "fmt"
8+ "net"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "bnema.dev/ermite-runner/internal/execstream"
14+ "bnema.dev/ermite-runner/internal/execstream/execpb"
15+)
16+
17+// 100 secrets of 48 KiB (4.8 MB) do not fit one frame (MaxFrame is 2 MiB) but
18+// do as NamedValue frames: this crosses the real transport, DriveJob on the
19+// host side and waitForRun on the executor side.
20+func TestManyLargeSecretsCrossTheStream(t *testing.T) {
21+ secrets := map[string]string{}
22+ for i := 0; i < 100; i++ {
23+ secrets[fmt.Sprintf("S%03d", i)] = strings.Repeat(string(rune('a'+i%26)), 48<<10)
24+ }
25+ vars := map[string]string{"A": "1", "B": "2"}
26+
27+ // the old shape, every secret in the Run frame, would not have worked
28+ err := execstream.NewConn(discard{}).Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: &execpb.Run{Job: &execpb.JobSpec{Contexts: strings.Join(valuesOf(secrets), "")}}}})
29+ if err == nil {
30+ t.Fatal("test premise: all the secrets must exceed one frame")
31+ }
32+
33+ hostSide, guestSide := net.Pipe()
34+ defer func() { _ = hostSide.Close(); _ = guestSide.Close() }()
35+ got := make(chan error, 1)
36+ go func() {
37+ c := execstream.NewConn(guestSide)
38+ _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})
39+ run, vals, err := waitForRun(context.Background(), c, 10*time.Second)
40+ switch {
41+ case err != nil:
42+ got <- err
43+ case run.GetJob().GetJobKey() != "k" || len(vals.secrets) != 100 || len(vals.vars) != 2 || vals.vars["B"] != "2":
44+ got <- fmt.Errorf("run %v, %d secrets, vars %v", run.GetJob().GetJobKey(), len(vals.secrets), vals.vars)
45+ default:
46+ for k, v := range secrets {
47+ if vals.secrets[k] != v {
48+ got <- fmt.Errorf("secret %s differs", k)
49+ return
50+ }
51+ }
52+ got <- nil
53+ _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})
54+ }
55+ }()
56+ _, exit, err := execstream.DriveJob(hostSide, &execpb.Run{Job: &execpb.JobSpec{JobKey: "k"}}, nil, execstream.JobHandler{Secrets: secrets, Vars: vars})
57+ if err != nil || exit == nil {
58+ t.Fatalf("DriveJob: %v", err)
59+ }
60+ if err := <-got; err != nil {
61+ t.Fatal(err)
62+ }
63+}
64+
65+type discard struct{}
66+
67+func (discard) Write(p []byte) (int, error) { return len(p), nil }
68+func (discard) Read([]byte) (int, error) { select {} }
69+
70+func valuesOf(m map[string]string) []string {
71+ var out []string
72+ for _, v := range m {
73+ out = append(out, v)
74+ }
75+ return out
76+}
M internal/host/fake_test.go +25 -2
@@ -10,6 +10,7 @@ import (
1010 "net/http/httptest"
1111 "strings"
1212 "sync"
13+ "sync/atomic"
1314 "testing"
1415 "time"
1516
@@ -409,10 +410,26 @@ func (s *fakeSandbox) Start(ctx context.Context, spec sandbox.JobSpec) (sandbox.
409410 defer func() { _ = guest.Close() }()
410411 c := execstream.NewConn(guest)
411412 _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Version: "test", Sandbox: "container"}}})
412- f, err := c.Recv()
413- if err != nil || f.GetRun() == nil {
413+ // NamedValue frames (secrets, variables) come before Run.
414+ vals := &seenValues{secrets: map[string]string{}, vars: map[string]string{}}
415+ var f *execpb.Frame
416+ for {
417+ var err error
418+ if f, err = c.Recv(); err != nil {
419+ return
420+ }
421+ if f.GetSecret() != nil {
422+ vals.secrets[f.GetSecret().Name] = f.GetSecret().Value
423+ } else if f.GetVar() != nil {
424+ vals.vars[f.GetVar().Name] = f.GetVar().Value
425+ } else {
426+ break
427+ }
428+ }
429+ if f.GetRun() == nil {
414430 return
415431 }
432+ lastValues.Store(vals)
416433 cancels := make(chan *execpb.Cancel, 4)
417434 go func() {
418435 for {
@@ -606,3 +623,9 @@ func (s *fakeServer) Result(jobID string) hosttest.Result {
606623 defer s.mu.Unlock()
607624 return hosttest.Result{Steps: s.steps}
608625 }
626+
627+// seenValues are the secrets and variables the scripted executor received
628+// before the Run frame.
629+type seenValues struct{ secrets, vars map[string]string }
630+
631+var lastValues atomic.Pointer[seenValues]
@@ -10,6 +10,7 @@ import (
10 "net/http/httptest"10 "net/http/httptest"
11 "strings"11 "strings"
12 "sync"12 "sync"
13+ "sync/atomic"
13 "testing"14 "testing"
14 "time"15 "time"
15 16
@@ -409,10 +410,26 @@ func (s *fakeSandbox) Start(ctx context.Context, spec sandbox.JobSpec) (sandbox.
409 defer func() { _ = guest.Close() }()410 defer func() { _ = guest.Close() }()
410 c := execstream.NewConn(guest)411 c := execstream.NewConn(guest)
411 _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Version: "test", Sandbox: "container"}}})412 _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Version: "test", Sandbox: "container"}}})
412- f, err := c.Recv()413+ // NamedValue frames (secrets, variables) come before Run.
413- if err != nil || f.GetRun() == nil {414+ vals := &seenValues{secrets: map[string]string{}, vars: map[string]string{}}
415+ var f *execpb.Frame
416+ for {
417+ var err error
418+ if f, err = c.Recv(); err != nil {
419+ return
420+ }
421+ if f.GetSecret() != nil {
422+ vals.secrets[f.GetSecret().Name] = f.GetSecret().Value
423+ } else if f.GetVar() != nil {
424+ vals.vars[f.GetVar().Name] = f.GetVar().Value
425+ } else {
426+ break
427+ }
428+ }
429+ if f.GetRun() == nil {
414 return430 return
415 }431 }
432+ lastValues.Store(vals)
416 cancels := make(chan *execpb.Cancel, 4)433 cancels := make(chan *execpb.Cancel, 4)
417 go func() {434 go func() {
418 for {435 for {
@@ -606,3 +623,9 @@ func (s *fakeServer) Result(jobID string) hosttest.Result {
606 defer s.mu.Unlock()623 defer s.mu.Unlock()
607 return hosttest.Result{Steps: s.steps}624 return hosttest.Result{Steps: s.steps}
608 }625 }
626+
627+// seenValues are the secrets and variables the scripted executor received
628+// before the Run frame.
629+type seenValues struct{ secrets, vars map[string]string }
630+
631+var lastValues atomic.Pointer[seenValues]
M internal/host/job.go +7 -0
@@ -690,6 +690,12 @@ func (j *jobRun) drive(conn sandbox.Conn, rt *Runner) (*execpb.Exit, error) {
690690 DeadlineMs: max(1, time.Until(j.deadline()).Milliseconds()),
691691 RunnerName: rt.Cfg.Name, RunnerVersion: rt.Cfg.Version, RunnerEnvironment: rt.Cfg.environment(),
692692 SendAnnotations: j.features.annotations, SendSummaries: j.features.summaries,
693+ // Job.secrets and Job.vars are not in this frame (it is limited to
694+ // 2 MiB): DriveJob sends them in NamedValue frames before it. The host
695+ // never logs them nor stores them. An old server leaves trusted false
696+ // because it does not send it, so the executor says "not available"
697+ // only when the server has ci.secrets.
698+ SecretsWithheld: j.features.secrets && !j.job.Trusted,
693699 }}
694700 run.Job.CachePort, run.Job.CacheToken = j.cacheFor(rt)
695701 _, exit, err := execstream.DriveJob(conn, run, j.cancelCh, execstream.JobHandler{
@@ -698,6 +704,7 @@ func (j *jobRun) drive(conn sandbox.Conn, rt *Runner) (*execpb.Exit, error) {
698704 JobResult: func(r *execpb.JobResult) { j.mu.Lock(); j.result = r; j.mu.Unlock() },
699705 Resolve: j.resolveActions,
700706 StepAnnotations: j.stepAnnotations, StepSummary: j.stepSummary,
707+ Secrets: j.job.Secrets, Vars: j.job.Vars,
701708 })
702709 return exit, err
703710 }
@@ -690,6 +690,12 @@ func (j *jobRun) drive(conn sandbox.Conn, rt *Runner) (*execpb.Exit, error) {
690 DeadlineMs: max(1, time.Until(j.deadline()).Milliseconds()),690 DeadlineMs: max(1, time.Until(j.deadline()).Milliseconds()),
691 RunnerName: rt.Cfg.Name, RunnerVersion: rt.Cfg.Version, RunnerEnvironment: rt.Cfg.environment(),691 RunnerName: rt.Cfg.Name, RunnerVersion: rt.Cfg.Version, RunnerEnvironment: rt.Cfg.environment(),
692 SendAnnotations: j.features.annotations, SendSummaries: j.features.summaries,692 SendAnnotations: j.features.annotations, SendSummaries: j.features.summaries,
693+ // Job.secrets and Job.vars are not in this frame (it is limited to
694+ // 2 MiB): DriveJob sends them in NamedValue frames before it. The host
695+ // never logs them nor stores them. An old server leaves trusted false
696+ // because it does not send it, so the executor says "not available"
697+ // only when the server has ci.secrets.
698+ SecretsWithheld: j.features.secrets && !j.job.Trusted,
693 }}699 }}
694 run.Job.CachePort, run.Job.CacheToken = j.cacheFor(rt)700 run.Job.CachePort, run.Job.CacheToken = j.cacheFor(rt)
695 _, exit, err := execstream.DriveJob(conn, run, j.cancelCh, execstream.JobHandler{701 _, exit, err := execstream.DriveJob(conn, run, j.cancelCh, execstream.JobHandler{
@@ -698,6 +704,7 @@ func (j *jobRun) drive(conn sandbox.Conn, rt *Runner) (*execpb.Exit, error) {
698 JobResult: func(r *execpb.JobResult) { j.mu.Lock(); j.result = r; j.mu.Unlock() },704 JobResult: func(r *execpb.JobResult) { j.mu.Lock(); j.result = r; j.mu.Unlock() },
699 Resolve: j.resolveActions,705 Resolve: j.resolveActions,
700 StepAnnotations: j.stepAnnotations, StepSummary: j.stepSummary,706 StepAnnotations: j.stepAnnotations, StepSummary: j.stepSummary,
707+ Secrets: j.job.Secrets, Vars: j.job.Vars,
701 })708 })
702 return exit, err709 return exit, err
703 }710 }
M internal/host/results.go +6 -1
@@ -20,6 +20,9 @@ import (
2020 const (
2121 FeatureAnnotations = "ci.annotations"
2222 FeatureSummaries = "ci.summaries"
23+ // FeatureSecrets: the server sends Job.secrets, vars, environment,
24+ // permissions and trusted. Without it Job.trusted is absent, not false.
25+ FeatureSecrets = "ci.secrets"
2326 )
2427
2528 // Limits of Annotation and UploadSummaryRequest (runner.proto). The executor
@@ -39,7 +42,7 @@ const (
3942 type ServerFeatures func(ctx context.Context) ([]string, error)
4043
4144 // jobFeatures is what the server accepts for this job.
42-type jobFeatures struct{ annotations, summaries bool }
45+type jobFeatures struct{ annotations, summaries, secrets bool }
4346
4447 func featuresOf(list []string) jobFeatures {
4548 var f jobFeatures
@@ -49,6 +52,8 @@ func featuresOf(list []string) jobFeatures {
4952 f.annotations = true
5053 case FeatureSummaries:
5154 f.summaries = true
55+ case FeatureSecrets:
56+ f.secrets = true
5257 }
5358 }
5459 return f
@@ -20,6 +20,9 @@ import (
20 const (20 const (
21 FeatureAnnotations = "ci.annotations"21 FeatureAnnotations = "ci.annotations"
22 FeatureSummaries = "ci.summaries"22 FeatureSummaries = "ci.summaries"
23+ // FeatureSecrets: the server sends Job.secrets, vars, environment,
24+ // permissions and trusted. Without it Job.trusted is absent, not false.
25+ FeatureSecrets = "ci.secrets"
23 )26 )
24 27
25 // Limits of Annotation and UploadSummaryRequest (runner.proto). The executor28 // Limits of Annotation and UploadSummaryRequest (runner.proto). The executor
@@ -39,7 +42,7 @@ const (
39 type ServerFeatures func(ctx context.Context) ([]string, error)42 type ServerFeatures func(ctx context.Context) ([]string, error)
40 43
41 // jobFeatures is what the server accepts for this job.44 // jobFeatures is what the server accepts for this job.
42-type jobFeatures struct{ annotations, summaries bool }45+type jobFeatures struct{ annotations, summaries, secrets bool }
43 46
44 func featuresOf(list []string) jobFeatures {47 func featuresOf(list []string) jobFeatures {
45 var f jobFeatures48 var f jobFeatures
@@ -49,6 +52,8 @@ func featuresOf(list []string) jobFeatures {
49 f.annotations = true52 f.annotations = true
50 case FeatureSummaries:53 case FeatureSummaries:
51 f.summaries = true54 f.summaries = true
55+ case FeatureSecrets:
56+ f.secrets = true
52 }57 }
53 }58 }
54 return f59 return f
A internal/host/secrets_test.go +302 -0
@@ -0,0 +1,302 @@
1+// SPDX-License-Identifier: MIT
2+
3+package host
4+
5+import (
6+ "context"
7+ "fmt"
8+ "go/ast"
9+ "go/format"
10+ "go/parser"
11+ "go/token"
12+ "os"
13+ "path/filepath"
14+ "regexp"
15+ "strings"
16+ "testing"
17+
18+ "google.golang.org/protobuf/proto"
19+
20+ "bnema.dev/ermite-runner/internal/execstream/execpb"
21+)
22+
23+const (
24+ secretValue = "s3cr3t-value-DO-NOT-LOG"
25+ secretValue2 = "-----BEGIN-----\nline-two-DO-NOT-LOG\n-----END-----"
26+)
27+
28+func withSecrets(trusted bool) func(*harness) {
29+ return func(h *harness) {
30+ j := testJob("j1")
31+ j.Secrets = map[string]string{"DB_PASSWORD": secretValue, "KEY": secretValue2}
32+ j.Vars = map[string]string{"REGION": "eu"}
33+ j.Trusted = trusted
34+ h.srv.jobs = nil
35+ h.srv.jobs = append(h.srv.jobs, j)
36+ }
37+}
38+
39+func runWithSecrets(t *testing.T, features []string, trusted bool) (*harness, *execpb.Run, string) {
40+ t.Helper()
41+ seenRun.Store(nil)
42+ state := t.TempDir()
43+ h := newHarness(t, resultsScript(nil))
44+ h.runner.Cfg.StateDir = state
45+ h.runner.Features = func(context.Context) ([]string, error) { return features, nil }
46+ withSecrets(trusted)(h)
47+ stop := h.run(t)
48+ eventually(t, "CompleteJob", func() bool { return h.srv.completeCount() == 1 })
49+ if err := stop(); err != nil {
50+ t.Fatal(err)
51+ }
52+ return h, seenRun.Load(), state
53+}
54+
55+func TestSecretsAndVarsReachTheExecutorOnly(t *testing.T) {
56+ h, run, state := runWithSecrets(t, []string{FeatureSecrets, FeatureAnnotations}, true)
57+ if run == nil {
58+ t.Fatal("no Run frame")
59+ }
60+ vals := lastValues.Load()
61+ if vals == nil || vals.secrets["DB_PASSWORD"] != secretValue || vals.secrets["KEY"] != secretValue2 || len(vals.secrets) != 2 {
62+ t.Errorf("secrets not passed: %+v", vals)
63+ }
64+ if vals.vars["REGION"] != "eu" || run.Job.SecretsWithheld {
65+ t.Errorf("vars %v withheld %v", vals.vars, run.Job.SecretsWithheld)
66+ }
67+ // The Run frame itself holds no secret.
68+ if rb, _ := proto.Marshal(run); strings.Contains(string(rb), "DO-NOT-LOG") {
69+ t.Error("the Run frame holds a secret")
70+ }
71+
72+ // Nothing the host does prints the secrets: its log (debug level) and its
73+ // state directory. (protobuf-go does not redact String(), so the guarantee
74+ // is that no code formats the Job, the Run or the JobSpec; see the grep in
75+ // TestNoCodePrintsTheJobMessages.)
76+ if strings.Contains(h.logs.String(), "DO-NOT-LOG") {
77+ t.Errorf("a secret is in the host log:\n%.500s", h.logs.String())
78+ }
79+ err := filepath.Walk(state, func(p string, info os.FileInfo, err error) error {
80+ if err != nil || info.IsDir() {
81+ return err
82+ }
83+ b, _ := os.ReadFile(p)
84+ if strings.Contains(string(b), "DO-NOT-LOG") {
85+ t.Errorf("a secret is in the state directory: %s", p)
86+ }
87+ return nil
88+ })
89+ if err != nil {
90+ t.Fatal(err)
91+ }
92+}
93+
94+// The "not available" line only exists when the server has ci.secrets: an old
95+// server does not send Job.trusted, which then reads false.
96+func TestSecretsWithheldOnlyWithTheFeature(t *testing.T) {
97+ for _, tc := range []struct {
98+ name string
99+ features []string
100+ trusted bool
101+ want bool
102+ }{
103+ {"untrusted, server has ci.secrets", []string{FeatureSecrets}, false, true},
104+ {"trusted, server has ci.secrets", []string{FeatureSecrets}, true, false},
105+ {"untrusted, old server", []string{FeatureAnnotations}, false, false},
106+ {"untrusted, features unreachable", nil, false, false},
107+ } {
108+ t.Run(tc.name, func(t *testing.T) {
109+ _, run, _ := runWithSecrets(t, tc.features, tc.trusted)
110+ if run == nil || run.Job.SecretsWithheld != tc.want {
111+ t.Errorf("secrets_withheld = %v, want %v", run != nil && run.Job.SecretsWithheld, tc.want)
112+ }
113+ })
114+ }
115+}
116+
117+// protobuf-go prints map values in String(), so nothing may format a message
118+// that holds secrets: the Job (ermite.Job, with Secrets) or the Run and JobSpec
119+// that wrap it. This is a heuristic on names, not a proof: printLeaks looks,
120+// in the sources of the host, the guest, execstream and the executor, for
121+// formatting, logging, marshalling and String() calls that are given a value
122+// named like those messages (job, j.job, r.job, spec, run, x.GetJob()), their
123+// Secrets field or GetSecrets(). It does not follow a message through other
124+// names or through values it was copied into. The real guarantees are the
125+// tests that read the host log and the state directory
126+// (TestSecretsAndVarsReachTheExecutorOnly) and the absence of secrets in the
127+// Run frame.
128+func TestNoCodePrintsTheJobMessages(t *testing.T) {
129+ fset := token.NewFileSet()
130+ for _, dir := range []string{".", "../guest", "../execstream", "../executor"} {
131+ files, err := filepath.Glob(filepath.Join(dir, "*.go"))
132+ if err != nil {
133+ t.Fatal(err)
134+ }
135+ for _, file := range files {
136+ if strings.HasSuffix(file, "_test.go") {
137+ continue
138+ }
139+ src, err := os.ReadFile(file)
140+ if err != nil {
141+ t.Fatal(err)
142+ }
143+ for _, leak := range printLeaks(t, fset, file, src) {
144+ t.Error(leak)
145+ }
146+ }
147+ }
148+}
149+
150+var (
151+ // printers are the calls that render their arguments: fmt, loggers (zerolog
152+ // events), the text and JSON encoders of messages.
153+ printers = map[string]bool{"Errorf": true, "Printf": true, "Sprintf": true, "Fprintf": true, "Println": true, "Fprintln": true,
154+ "Print": true, "Sprint": true, "Sprintln": true, "Interface": true, "Any": true, "Stringer": true, "Msgf": true, "Msg": true, "Str": true,
155+ "Fatalf": true, "Logf": true, "Infof": true, "Warnf": true, "Format": true, "Marshal": true, "MarshalToString": true,
156+ "Err": true, "Object": true, "Dict": true, "Strs": true}
157+ // the messages by name
158+ messageName = `(?:(?:j|r|rt)\.job|job|spec|run|j\.run|[A-Za-z_.]+\.GetJob\(\)|[A-Za-z_.]+\.Job)`
159+ messageRe = regexp.MustCompile(`^` + messageName + `$`)
160+ // message.String(), and the secrets of a message or of anything
161+ stringRe = regexp.MustCompile(`^` + messageName + `\.String\(\)$`)
162+ secretsRe = regexp.MustCompile(`\.(?:Secrets|GetSecrets\(\))$`)
163+)
164+
165+func leaky(text string) bool {
166+ return messageRe.MatchString(text) || stringRe.MatchString(text) || secretsRe.MatchString(text)
167+}
168+
169+// printLeaks returns the places of src where a message with secrets, or its
170+// secrets, is given to a printing call, or where String() is called on such a
171+// message.
172+func printLeaks(t *testing.T, fset *token.FileSet, name string, src []byte) []string {
173+ t.Helper()
174+ f, err := parser.ParseFile(fset, name, src, 0)
175+ if err != nil {
176+ t.Fatal(err)
177+ }
178+ text := func(e ast.Expr) string {
179+ var b strings.Builder
180+ _ = format.Node(&b, fset, e)
181+ return b.String()
182+ }
183+ var out []string
184+ report := func(e ast.Expr, what string) {
185+ out = append(out, fmt.Sprintf("%s: %s: %s would print secrets", fset.Position(e.Pos()), what, text(e)))
186+ }
187+ // visit looks for leaky expressions inside an argument of a printing call. A
188+ // field read of a message (j.job.JobId) is a scalar: the message is not
189+ // descended into.
190+ var visit func(e ast.Expr, what string)
191+ visit = func(e ast.Expr, what string) {
192+ if e == nil {
193+ return
194+ }
195+ if leaky(text(e)) {
196+ report(e, what)
197+ return
198+ }
199+ switch x := e.(type) {
200+ case *ast.SelectorExpr:
201+ if !messageRe.MatchString(text(x.X)) {
202+ visit(x.X, what)
203+ }
204+ case *ast.CallExpr:
205+ if id, ok := x.Fun.(*ast.Ident); ok && (id.Name == "len" || id.Name == "cap") {
206+ return // a count is not a secret
207+ }
208+ if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
209+ if !messageRe.MatchString(text(sel.X)) {
210+ visit(sel.X, what)
211+ }
212+ } else {
213+ visit(x.Fun, what)
214+ }
215+ for _, a := range x.Args {
216+ visit(a, what)
217+ }
218+ case *ast.UnaryExpr:
219+ visit(x.X, what)
220+ case *ast.StarExpr:
221+ visit(x.X, what)
222+ case *ast.ParenExpr:
223+ visit(x.X, what)
224+ case *ast.BinaryExpr:
225+ visit(x.X, what)
226+ visit(x.Y, what)
227+ case *ast.IndexExpr:
228+ visit(x.X, what)
229+ case *ast.CompositeLit:
230+ for _, el := range x.Elts {
231+ if kv, ok := el.(*ast.KeyValueExpr); ok {
232+ visit(kv.Value, what)
233+ } else {
234+ visit(el, what)
235+ }
236+ }
237+ }
238+ }
239+ ast.Inspect(f, func(n ast.Node) bool {
240+ call, ok := n.(*ast.CallExpr)
241+ if !ok {
242+ return true
243+ }
244+ if stringRe.MatchString(text(call)) {
245+ report(call, "String()")
246+ return true
247+ }
248+ if sel, ok := call.Fun.(*ast.SelectorExpr); ok && printers[sel.Sel.Name] {
249+ for _, a := range call.Args {
250+ visit(a, sel.Sel.Name+"()")
251+ }
252+ }
253+ return true
254+ })
255+ return out
256+}
257+
258+// The guard finds what it is meant to find and leaves scalar fields alone.
259+func TestPrintLeaksHeuristic(t *testing.T) {
260+ positive := []string{
261+ `fmt.Sprintf("%v", spec)`,
262+ `fmt.Errorf("bad %+v", j.job)`,
263+ `log.Info().Interface("job", j.job).Msg("x")`,
264+ `log.Info().Any("run", run).Msg("x")`,
265+ `_ = spec.String()`,
266+ `_ = j.job.String()`,
267+ `x := run.GetJob().String()`,
268+ `log.Debug().Str("s", fmt.Sprint(j.job.Secrets))`,
269+ `log.Debug().Interface("s", j.job.Secrets).Msg("x")`,
270+ `log.Debug().Interface("s", job.GetSecrets()).Msg("x")`,
271+ `fmt.Println(run.GetJob())`,
272+ `fmt.Println("job", r.job)`,
273+ `_ = prototext.Format(run)`,
274+ `_, _ = json.Marshal(j.job)`,
275+ `fmt.Println(strings.ToUpper(spec.String()))`,
276+ `fmt.Println(&j.job)`,
277+ `fmt.Println([]any{j.job})`,
278+ }
279+ negative := []string{
280+ `fmt.Sprintf("%s", j.job.JobId)`,
281+ `log.Info().Str("job_id", job.JobId).Int64("run_id", job.RunId).Msg("x")`,
282+ `fmt.Errorf("job %s: %w", j.job.JobKey, err)`,
283+ `log.Info().Str("k", run.GetJob().GetJobKey()).Msg("x")`,
284+ `_ = fmt.Sprint(len(j.job.Secrets))`,
285+ `h := JobHandler{Secrets: j.job.Secrets, Vars: j.job.Vars}`,
286+ `_ = proto.Marshal(frame)`,
287+ `fmt.Println(spec.GetRepo())`,
288+ `_ = other.String()`,
289+ `secrets := j.job.Secrets; _ = secrets`,
290+ }
291+ fset := token.NewFileSet()
292+ for _, c := range positive {
293+ if got := printLeaks(t, fset, "p.go", []byte("package p\nfunc f() {\n"+c+"\n}\n")); len(got) == 0 {
294+ t.Errorf("not detected: %s", c)
295+ }
296+ }
297+ for _, c := range negative {
298+ if got := printLeaks(t, fset, "n.go", []byte("package p\nfunc f() {\n"+c+"\n}\n")); len(got) != 0 {
299+ t.Errorf("false positive: %s: %v", c, got)
300+ }
301+ }
302+}
@@ -0,0 +1,302 @@
1+// SPDX-License-Identifier: MIT
2+
3+package host
4+
5+import (
6+ "context"
7+ "fmt"
8+ "go/ast"
9+ "go/format"
10+ "go/parser"
11+ "go/token"
12+ "os"
13+ "path/filepath"
14+ "regexp"
15+ "strings"
16+ "testing"
17+
18+ "google.golang.org/protobuf/proto"
19+
20+ "bnema.dev/ermite-runner/internal/execstream/execpb"
21+)
22+
23+const (
24+ secretValue = "s3cr3t-value-DO-NOT-LOG"
25+ secretValue2 = "-----BEGIN-----\nline-two-DO-NOT-LOG\n-----END-----"
26+)
27+
28+func withSecrets(trusted bool) func(*harness) {
29+ return func(h *harness) {
30+ j := testJob("j1")
31+ j.Secrets = map[string]string{"DB_PASSWORD": secretValue, "KEY": secretValue2}
32+ j.Vars = map[string]string{"REGION": "eu"}
33+ j.Trusted = trusted
34+ h.srv.jobs = nil
35+ h.srv.jobs = append(h.srv.jobs, j)
36+ }
37+}
38+
39+func runWithSecrets(t *testing.T, features []string, trusted bool) (*harness, *execpb.Run, string) {
40+ t.Helper()
41+ seenRun.Store(nil)
42+ state := t.TempDir()
43+ h := newHarness(t, resultsScript(nil))
44+ h.runner.Cfg.StateDir = state
45+ h.runner.Features = func(context.Context) ([]string, error) { return features, nil }
46+ withSecrets(trusted)(h)
47+ stop := h.run(t)
48+ eventually(t, "CompleteJob", func() bool { return h.srv.completeCount() == 1 })
49+ if err := stop(); err != nil {
50+ t.Fatal(err)
51+ }
52+ return h, seenRun.Load(), state
53+}
54+
55+func TestSecretsAndVarsReachTheExecutorOnly(t *testing.T) {
56+ h, run, state := runWithSecrets(t, []string{FeatureSecrets, FeatureAnnotations}, true)
57+ if run == nil {
58+ t.Fatal("no Run frame")
59+ }
60+ vals := lastValues.Load()
61+ if vals == nil || vals.secrets["DB_PASSWORD"] != secretValue || vals.secrets["KEY"] != secretValue2 || len(vals.secrets) != 2 {
62+ t.Errorf("secrets not passed: %+v", vals)
63+ }
64+ if vals.vars["REGION"] != "eu" || run.Job.SecretsWithheld {
65+ t.Errorf("vars %v withheld %v", vals.vars, run.Job.SecretsWithheld)
66+ }
67+ // The Run frame itself holds no secret.
68+ if rb, _ := proto.Marshal(run); strings.Contains(string(rb), "DO-NOT-LOG") {
69+ t.Error("the Run frame holds a secret")
70+ }
71+
72+ // Nothing the host does prints the secrets: its log (debug level) and its
73+ // state directory. (protobuf-go does not redact String(), so the guarantee
74+ // is that no code formats the Job, the Run or the JobSpec; see the grep in
75+ // TestNoCodePrintsTheJobMessages.)
76+ if strings.Contains(h.logs.String(), "DO-NOT-LOG") {
77+ t.Errorf("a secret is in the host log:\n%.500s", h.logs.String())
78+ }
79+ err := filepath.Walk(state, func(p string, info os.FileInfo, err error) error {
80+ if err != nil || info.IsDir() {
81+ return err
82+ }
83+ b, _ := os.ReadFile(p)
84+ if strings.Contains(string(b), "DO-NOT-LOG") {
85+ t.Errorf("a secret is in the state directory: %s", p)
86+ }
87+ return nil
88+ })
89+ if err != nil {
90+ t.Fatal(err)
91+ }
92+}
93+
94+// The "not available" line only exists when the server has ci.secrets: an old
95+// server does not send Job.trusted, which then reads false.
96+func TestSecretsWithheldOnlyWithTheFeature(t *testing.T) {
97+ for _, tc := range []struct {
98+ name string
99+ features []string
100+ trusted bool
101+ want bool
102+ }{
103+ {"untrusted, server has ci.secrets", []string{FeatureSecrets}, false, true},
104+ {"trusted, server has ci.secrets", []string{FeatureSecrets}, true, false},
105+ {"untrusted, old server", []string{FeatureAnnotations}, false, false},
106+ {"untrusted, features unreachable", nil, false, false},
107+ } {
108+ t.Run(tc.name, func(t *testing.T) {
109+ _, run, _ := runWithSecrets(t, tc.features, tc.trusted)
110+ if run == nil || run.Job.SecretsWithheld != tc.want {
111+ t.Errorf("secrets_withheld = %v, want %v", run != nil && run.Job.SecretsWithheld, tc.want)
112+ }
113+ })
114+ }
115+}
116+
117+// protobuf-go prints map values in String(), so nothing may format a message
118+// that holds secrets: the Job (ermite.Job, with Secrets) or the Run and JobSpec
119+// that wrap it. This is a heuristic on names, not a proof: printLeaks looks,
120+// in the sources of the host, the guest, execstream and the executor, for
121+// formatting, logging, marshalling and String() calls that are given a value
122+// named like those messages (job, j.job, r.job, spec, run, x.GetJob()), their
123+// Secrets field or GetSecrets(). It does not follow a message through other
124+// names or through values it was copied into. The real guarantees are the
125+// tests that read the host log and the state directory
126+// (TestSecretsAndVarsReachTheExecutorOnly) and the absence of secrets in the
127+// Run frame.
128+func TestNoCodePrintsTheJobMessages(t *testing.T) {
129+ fset := token.NewFileSet()
130+ for _, dir := range []string{".", "../guest", "../execstream", "../executor"} {
131+ files, err := filepath.Glob(filepath.Join(dir, "*.go"))
132+ if err != nil {
133+ t.Fatal(err)
134+ }
135+ for _, file := range files {
136+ if strings.HasSuffix(file, "_test.go") {
137+ continue
138+ }
139+ src, err := os.ReadFile(file)
140+ if err != nil {
141+ t.Fatal(err)
142+ }
143+ for _, leak := range printLeaks(t, fset, file, src) {
144+ t.Error(leak)
145+ }
146+ }
147+ }
148+}
149+
150+var (
151+ // printers are the calls that render their arguments: fmt, loggers (zerolog
152+ // events), the text and JSON encoders of messages.
153+ printers = map[string]bool{"Errorf": true, "Printf": true, "Sprintf": true, "Fprintf": true, "Println": true, "Fprintln": true,
154+ "Print": true, "Sprint": true, "Sprintln": true, "Interface": true, "Any": true, "Stringer": true, "Msgf": true, "Msg": true, "Str": true,
155+ "Fatalf": true, "Logf": true, "Infof": true, "Warnf": true, "Format": true, "Marshal": true, "MarshalToString": true,
156+ "Err": true, "Object": true, "Dict": true, "Strs": true}
157+ // the messages by name
158+ messageName = `(?:(?:j|r|rt)\.job|job|spec|run|j\.run|[A-Za-z_.]+\.GetJob\(\)|[A-Za-z_.]+\.Job)`
159+ messageRe = regexp.MustCompile(`^` + messageName + `$`)
160+ // message.String(), and the secrets of a message or of anything
161+ stringRe = regexp.MustCompile(`^` + messageName + `\.String\(\)$`)
162+ secretsRe = regexp.MustCompile(`\.(?:Secrets|GetSecrets\(\))$`)
163+)
164+
165+func leaky(text string) bool {
166+ return messageRe.MatchString(text) || stringRe.MatchString(text) || secretsRe.MatchString(text)
167+}
168+
169+// printLeaks returns the places of src where a message with secrets, or its
170+// secrets, is given to a printing call, or where String() is called on such a
171+// message.
172+func printLeaks(t *testing.T, fset *token.FileSet, name string, src []byte) []string {
173+ t.Helper()
174+ f, err := parser.ParseFile(fset, name, src, 0)
175+ if err != nil {
176+ t.Fatal(err)
177+ }
178+ text := func(e ast.Expr) string {
179+ var b strings.Builder
180+ _ = format.Node(&b, fset, e)
181+ return b.String()
182+ }
183+ var out []string
184+ report := func(e ast.Expr, what string) {
185+ out = append(out, fmt.Sprintf("%s: %s: %s would print secrets", fset.Position(e.Pos()), what, text(e)))
186+ }
187+ // visit looks for leaky expressions inside an argument of a printing call. A
188+ // field read of a message (j.job.JobId) is a scalar: the message is not
189+ // descended into.
190+ var visit func(e ast.Expr, what string)
191+ visit = func(e ast.Expr, what string) {
192+ if e == nil {
193+ return
194+ }
195+ if leaky(text(e)) {
196+ report(e, what)
197+ return
198+ }
199+ switch x := e.(type) {
200+ case *ast.SelectorExpr:
201+ if !messageRe.MatchString(text(x.X)) {
202+ visit(x.X, what)
203+ }
204+ case *ast.CallExpr:
205+ if id, ok := x.Fun.(*ast.Ident); ok && (id.Name == "len" || id.Name == "cap") {
206+ return // a count is not a secret
207+ }
208+ if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
209+ if !messageRe.MatchString(text(sel.X)) {
210+ visit(sel.X, what)
211+ }
212+ } else {
213+ visit(x.Fun, what)
214+ }
215+ for _, a := range x.Args {
216+ visit(a, what)
217+ }
218+ case *ast.UnaryExpr:
219+ visit(x.X, what)
220+ case *ast.StarExpr:
221+ visit(x.X, what)
222+ case *ast.ParenExpr:
223+ visit(x.X, what)
224+ case *ast.BinaryExpr:
225+ visit(x.X, what)
226+ visit(x.Y, what)
227+ case *ast.IndexExpr:
228+ visit(x.X, what)
229+ case *ast.CompositeLit:
230+ for _, el := range x.Elts {
231+ if kv, ok := el.(*ast.KeyValueExpr); ok {
232+ visit(kv.Value, what)
233+ } else {
234+ visit(el, what)
235+ }
236+ }
237+ }
238+ }
239+ ast.Inspect(f, func(n ast.Node) bool {
240+ call, ok := n.(*ast.CallExpr)
241+ if !ok {
242+ return true
243+ }
244+ if stringRe.MatchString(text(call)) {
245+ report(call, "String()")
246+ return true
247+ }
248+ if sel, ok := call.Fun.(*ast.SelectorExpr); ok && printers[sel.Sel.Name] {
249+ for _, a := range call.Args {
250+ visit(a, sel.Sel.Name+"()")
251+ }
252+ }
253+ return true
254+ })
255+ return out
256+}
257+
258+// The guard finds what it is meant to find and leaves scalar fields alone.
259+func TestPrintLeaksHeuristic(t *testing.T) {
260+ positive := []string{
261+ `fmt.Sprintf("%v", spec)`,
262+ `fmt.Errorf("bad %+v", j.job)`,
263+ `log.Info().Interface("job", j.job).Msg("x")`,
264+ `log.Info().Any("run", run).Msg("x")`,
265+ `_ = spec.String()`,
266+ `_ = j.job.String()`,
267+ `x := run.GetJob().String()`,
268+ `log.Debug().Str("s", fmt.Sprint(j.job.Secrets))`,
269+ `log.Debug().Interface("s", j.job.Secrets).Msg("x")`,
270+ `log.Debug().Interface("s", job.GetSecrets()).Msg("x")`,
271+ `fmt.Println(run.GetJob())`,
272+ `fmt.Println("job", r.job)`,
273+ `_ = prototext.Format(run)`,
274+ `_, _ = json.Marshal(j.job)`,
275+ `fmt.Println(strings.ToUpper(spec.String()))`,
276+ `fmt.Println(&j.job)`,
277+ `fmt.Println([]any{j.job})`,
278+ }
279+ negative := []string{
280+ `fmt.Sprintf("%s", j.job.JobId)`,
281+ `log.Info().Str("job_id", job.JobId).Int64("run_id", job.RunId).Msg("x")`,
282+ `fmt.Errorf("job %s: %w", j.job.JobKey, err)`,
283+ `log.Info().Str("k", run.GetJob().GetJobKey()).Msg("x")`,
284+ `_ = fmt.Sprint(len(j.job.Secrets))`,
285+ `h := JobHandler{Secrets: j.job.Secrets, Vars: j.job.Vars}`,
286+ `_ = proto.Marshal(frame)`,
287+ `fmt.Println(spec.GetRepo())`,
288+ `_ = other.String()`,
289+ `secrets := j.job.Secrets; _ = secrets`,
290+ }
291+ fset := token.NewFileSet()
292+ for _, c := range positive {
293+ if got := printLeaks(t, fset, "p.go", []byte("package p\nfunc f() {\n"+c+"\n}\n")); len(got) == 0 {
294+ t.Errorf("not detected: %s", c)
295+ }
296+ }
297+ for _, c := range negative {
298+ if got := printLeaks(t, fset, "n.go", []byte("package p\nfunc f() {\n"+c+"\n}\n")); len(got) != 0 {
299+ t.Errorf("false positive: %s: %v", c, got)
300+ }
301+ }
302+}

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