v0.14.0 f9976e7

gen_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package api_test
4
5import (
6	"errors"
7	"testing"
8
9	"connectrpc.com/connect"
10
11	"google.golang.org/protobuf/proto"
12	"google.golang.org/protobuf/reflect/protoreflect"
13	"google.golang.org/protobuf/types/known/fieldmaskpb"
14
15	"bnema.dev/ermite-api/gen/ermite"
16	"bnema.dev/ermite-api/gen/ermite/ermiteconnect"
17)
18
19// TestProcedures pins the procedure paths: they are the wire contract.
20func TestProcedures(t *testing.T) {
21	tests := []struct{ got, want string }{
22		{ermiteconnect.MirrorServiceGetMirrorKeyProcedure, "/ermite.MirrorService/GetMirrorKey"},
23		{ermiteconnect.MirrorServiceRotateMirrorKeyProcedure, "/ermite.MirrorService/RotateMirrorKey"},
24		{ermiteconnect.MirrorServiceGetMirrorConfigProcedure, "/ermite.MirrorService/GetMirrorConfig"},
25		{ermiteconnect.MirrorServiceSetMirrorConfigProcedure, "/ermite.MirrorService/SetMirrorConfig"},
26		{ermiteconnect.MirrorServiceSetRepoMirrorProcedure, "/ermite.MirrorService/SetRepoMirror"},
27		{ermiteconnect.MirrorServiceRemoveRepoMirrorProcedure, "/ermite.MirrorService/RemoveRepoMirror"},
28		{ermiteconnect.MirrorServiceSetRepoMirrorExcludedProcedure, "/ermite.MirrorService/SetRepoMirrorExcluded"},
29		{ermiteconnect.MirrorServiceGetMirrorStatusProcedure, "/ermite.MirrorService/GetMirrorStatus"},
30		{ermiteconnect.MirrorServiceListMirrorStatusProcedure, "/ermite.MirrorService/ListMirrorStatus"},
31		{ermiteconnect.MirrorServiceSyncMirrorProcedure, "/ermite.MirrorService/SyncMirror"},
32
33		{ermiteconnect.RepoServiceListReposProcedure, "/ermite.RepoService/ListRepos"},
34		{ermiteconnect.RepoServiceGetRepoProcedure, "/ermite.RepoService/GetRepo"},
35		{ermiteconnect.RepoServiceCreateRepoProcedure, "/ermite.RepoService/CreateRepo"},
36		{ermiteconnect.RepoServiceUpdateRepoProcedure, "/ermite.RepoService/UpdateRepo"},
37		{ermiteconnect.RepoServiceDeleteRepoProcedure, "/ermite.RepoService/DeleteRepo"},
38		{ermiteconnect.GitServiceListRefsProcedure, "/ermite.GitService/ListRefs"},
39		{ermiteconnect.GitServiceGetTreeProcedure, "/ermite.GitService/GetTree"},
40		{ermiteconnect.GitServiceListPathsProcedure, "/ermite.GitService/ListPaths"},
41		{ermiteconnect.GitServiceGetBlobProcedure, "/ermite.GitService/GetBlob"},
42		{ermiteconnect.GitServiceListCommitsProcedure, "/ermite.GitService/ListCommits"},
43		{ermiteconnect.GitServiceGetCommitProcedure, "/ermite.GitService/GetCommit"},
44		{ermiteconnect.GitServiceResolveRevisionProcedure, "/ermite.GitService/ResolveRevision"},
45		{ermiteconnect.TokenServiceCreateTokenProcedure, "/ermite.TokenService/CreateToken"},
46		{ermiteconnect.TokenServiceListTokensProcedure, "/ermite.TokenService/ListTokens"},
47		{ermiteconnect.TokenServiceRevokeTokenProcedure, "/ermite.TokenService/RevokeToken"},
48		{ermiteconnect.EventServiceWatchProcedure, "/ermite.EventService/Watch"},
49		{ermiteconnect.MergeRequestServiceCreateMergeRequestProcedure, "/ermite.MergeRequestService/CreateMergeRequest"},
50		{ermiteconnect.MergeRequestServiceListMergeRequestsProcedure, "/ermite.MergeRequestService/ListMergeRequests"},
51		{ermiteconnect.MergeRequestServiceGetMergeRequestProcedure, "/ermite.MergeRequestService/GetMergeRequest"},
52		{ermiteconnect.MergeRequestServiceAddCommentProcedure, "/ermite.MergeRequestService/AddComment"},
53		{ermiteconnect.MergeRequestServiceResolveCommentProcedure, "/ermite.MergeRequestService/ResolveComment"},
54		{ermiteconnect.MergeRequestServiceCloseMergeRequestProcedure, "/ermite.MergeRequestService/CloseMergeRequest"},
55		{ermiteconnect.MergeRequestServiceMergeMergeRequestProcedure, "/ermite.MergeRequestService/MergeMergeRequest"},
56		{ermiteconnect.MergeRequestServiceMarkReadProcedure, "/ermite.MergeRequestService/MarkRead"},
57		{ermiteconnect.MergeRequestServiceEnableAutoMergeProcedure, "/ermite.MergeRequestService/EnableAutoMerge"},
58		{ermiteconnect.MergeRequestServiceCancelAutoMergeProcedure, "/ermite.MergeRequestService/CancelAutoMerge"},
59		{ermiteconnect.SessionServiceCreateLoginLinkProcedure, "/ermite.SessionService/CreateLoginLink"},
60		{ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"},
61		{ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"},
62		{ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"},
63		{ermiteconnect.ServerServiceRotateOIDCKeyProcedure, "/ermite.ServerService/RotateOIDCKey"},
64		{ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"},
65		{ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"},
66		{ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"},
67		{ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"},
68		{ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},
69		{ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},
70		{ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},
71		{ermiteconnect.RunnerServiceResolveActionsProcedure, "/ermite.RunnerService/ResolveActions"},
72		{ermiteconnect.RunnerServiceUploadSummaryProcedure, "/ermite.RunnerService/UploadSummary"},
73		{ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},
74		{ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},
75		{ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},
76		{ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"},
77		{ermiteconnect.RunServiceListAnnotationsProcedure, "/ermite.RunService/ListAnnotations"},
78		{ermiteconnect.RunServiceGetStepSummariesProcedure, "/ermite.RunService/GetStepSummaries"},
79		{ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"},
80		{ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"},
81		{ermiteconnect.RunServiceDispatchWorkflowProcedure, "/ermite.RunService/DispatchWorkflow"},
82		{ermiteconnect.RunServiceListWorkflowsProcedure, "/ermite.RunService/ListWorkflows"},
83		{ermiteconnect.RunServiceDisableWorkflowProcedure, "/ermite.RunService/DisableWorkflow"},
84		{ermiteconnect.RunServiceEnableWorkflowProcedure, "/ermite.RunService/EnableWorkflow"},
85		{ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"},
86		{ermiteconnect.RunServiceListRunnersProcedure, "/ermite.RunService/ListRunners"},
87		{ermiteconnect.RunServiceRemoveRunnerProcedure, "/ermite.RunService/RemoveRunner"},
88		{ermiteconnect.SecretServiceSetSecretProcedure, "/ermite.SecretService/SetSecret"},
89		{ermiteconnect.SecretServiceListSecretsProcedure, "/ermite.SecretService/ListSecrets"},
90		{ermiteconnect.SecretServiceDeleteSecretProcedure, "/ermite.SecretService/DeleteSecret"},
91		{ermiteconnect.VariableServiceSetVariableProcedure, "/ermite.VariableService/SetVariable"},
92		{ermiteconnect.VariableServiceGetVariableProcedure, "/ermite.VariableService/GetVariable"},
93		{ermiteconnect.VariableServiceListVariablesProcedure, "/ermite.VariableService/ListVariables"},
94		{ermiteconnect.VariableServiceDeleteVariableProcedure, "/ermite.VariableService/DeleteVariable"},
95		{ermiteconnect.EnvironmentServiceCreateEnvironmentProcedure, "/ermite.EnvironmentService/CreateEnvironment"},
96		{ermiteconnect.EnvironmentServiceListEnvironmentsProcedure, "/ermite.EnvironmentService/ListEnvironments"},
97		{ermiteconnect.EnvironmentServiceDeleteEnvironmentProcedure, "/ermite.EnvironmentService/DeleteEnvironment"},
98		{ermiteconnect.JobArtifactServiceCreateJobArtifactProcedure, "/ermite.JobArtifactService/CreateJobArtifact"},
99		{ermiteconnect.JobArtifactServiceUploadJobArtifactBlockProcedure, "/ermite.JobArtifactService/UploadJobArtifactBlock"},
100		{ermiteconnect.JobArtifactServiceCommitJobArtifactProcedure, "/ermite.JobArtifactService/CommitJobArtifact"},
101		{ermiteconnect.JobArtifactServiceFinalizeJobArtifactProcedure, "/ermite.JobArtifactService/FinalizeJobArtifact"},
102		{ermiteconnect.JobArtifactServiceListJobArtifactsProcedure, "/ermite.JobArtifactService/ListJobArtifacts"},
103		{ermiteconnect.JobArtifactServiceDownloadJobArtifactProcedure, "/ermite.JobArtifactService/DownloadJobArtifact"},
104		{ermiteconnect.JobArtifactServiceDeleteJobArtifactProcedure, "/ermite.JobArtifactService/DeleteJobArtifact"},
105		{ermiteconnect.ArtifactServiceListArtifactsProcedure, "/ermite.ArtifactService/ListArtifacts"},
106		{ermiteconnect.ArtifactServiceDownloadArtifactProcedure, "/ermite.ArtifactService/DownloadArtifact"},
107		{ermiteconnect.ArtifactServiceDeleteArtifactProcedure, "/ermite.ArtifactService/DeleteArtifact"},
108	}
109	for _, tt := range tests {
110		if tt.got != tt.want {
111			t.Errorf("procedure = %q, want %q", tt.got, tt.want)
112		}
113	}
114}
115
116// TestEnumNumbers pins enum numbers: they are part of the wire contract.
117func TestEnumNumbers(t *testing.T) {
118	if got := ermite.Scope_SCOPE_TOKEN_ADMIN; got != 9 {
119		t.Errorf("SCOPE_TOKEN_ADMIN = %d, want 9", got)
120	}
121	if got := ermite.FileStatus_FILE_STATUS_TYPE_CHANGED; got != 6 {
122		t.Errorf("FILE_STATUS_TYPE_CHANGED = %d, want 6", got)
123	}
124	if got := ermite.Visibility_VISIBILITY_PRIVATE; got != 2 {
125		t.Errorf("VISIBILITY_PRIVATE = %d, want 2", got)
126	}
127	if got := ermite.MergeMode_MERGE_MODE_SQUASH; got != 2 {
128		t.Errorf("MERGE_MODE_SQUASH = %d, want 2", got)
129	}
130	if got := ermite.MergeMode_MERGE_MODE_MERGE_COMMIT; got != 3 {
131		t.Errorf("MERGE_MODE_MERGE_COMMIT = %d, want 3", got)
132	}
133	if got := ermite.MergeabilityState_MERGEABILITY_STATE_CONFLICT; got != 4 {
134		t.Errorf("MERGEABILITY_STATE_CONFLICT = %d, want 4", got)
135	}
136	if got := ermite.MergeRequestState_MERGE_REQUEST_STATE_CLOSED; got != 3 {
137		t.Errorf("MERGE_REQUEST_STATE_CLOSED = %d, want 3", got)
138	}
139	if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 {
140		t.Errorf("DIFF_SIDE_NEW = %d, want 2", got)
141	}
142	enums := []struct {
143		name string
144		got  int32
145		want int32
146	}{
147		{"CI_STATUS_UNSPECIFIED", int32(ermite.CiStatus_CI_STATUS_UNSPECIFIED), 0},
148		{"CI_STATUS_QUEUED", int32(ermite.CiStatus_CI_STATUS_QUEUED), 1},
149		{"CI_STATUS_IN_PROGRESS", int32(ermite.CiStatus_CI_STATUS_IN_PROGRESS), 2},
150		{"CI_STATUS_COMPLETED", int32(ermite.CiStatus_CI_STATUS_COMPLETED), 3},
151		{"CI_STATUS_PENDING", int32(ermite.CiStatus_CI_STATUS_PENDING), 4},
152		{"CI_CONCLUSION_UNSPECIFIED", int32(ermite.CiConclusion_CI_CONCLUSION_UNSPECIFIED), 0},
153		{"CI_CONCLUSION_SUCCESS", int32(ermite.CiConclusion_CI_CONCLUSION_SUCCESS), 1},
154		{"CI_CONCLUSION_FAILURE", int32(ermite.CiConclusion_CI_CONCLUSION_FAILURE), 2},
155		{"CI_CONCLUSION_CANCELLED", int32(ermite.CiConclusion_CI_CONCLUSION_CANCELLED), 3},
156		{"CI_CONCLUSION_SKIPPED", int32(ermite.CiConclusion_CI_CONCLUSION_SKIPPED), 4},
157		{"CI_CONCLUSION_TIMED_OUT", int32(ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT), 5},
158		{"CI_CONCLUSION_NEUTRAL", int32(ermite.CiConclusion_CI_CONCLUSION_NEUTRAL), 6},
159		{"CI_CONCLUSION_ACTION_REQUIRED", int32(ermite.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED), 7},
160		{"STEP_STATE_UNSPECIFIED", int32(ermite.StepState_STEP_STATE_UNSPECIFIED), 0},
161		{"STEP_STATE_PENDING", int32(ermite.StepState_STEP_STATE_PENDING), 1},
162		{"STEP_STATE_IN_PROGRESS", int32(ermite.StepState_STEP_STATE_IN_PROGRESS), 2},
163		{"STEP_STATE_COMPLETED", int32(ermite.StepState_STEP_STATE_COMPLETED), 3},
164		{"RUNNER_ISOLATION_UNSPECIFIED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_UNSPECIFIED), 0},
165		{"RUNNER_ISOLATION_VM", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_VM), 1},
166		{"RUNNER_ISOLATION_CONTAINER_SYSBOX", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_SYSBOX), 2},
167		{"RUNNER_ISOLATION_CONTAINER_PRIVILEGED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED), 3},
168		{"CANCEL_REASON_UNSPECIFIED", int32(ermite.CancelReason_CANCEL_REASON_UNSPECIFIED), 0},
169		{"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1},
170		{"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2},
171		{"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3},
172		{"ANNOTATION_LEVEL_UNSPECIFIED", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED), 0},
173		{"ANNOTATION_LEVEL_NOTICE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_NOTICE), 1},
174		{"ANNOTATION_LEVEL_WARNING", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING), 2},
175		{"ANNOTATION_LEVEL_FAILURE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE), 3},
176		{"ACTION_SOURCE_UNSPECIFIED", int32(ermite.ActionSource_ACTION_SOURCE_UNSPECIFIED), 0},
177		{"ACTION_SOURCE_GITHUB", int32(ermite.ActionSource_ACTION_SOURCE_GITHUB), 1},
178		{"ACTION_SOURCE_ERMITE", int32(ermite.ActionSource_ACTION_SOURCE_ERMITE), 2},
179	}
180	for _, e := range enums {
181		if e.got != e.want {
182			t.Errorf("%s = %d, want %d", e.name, e.got, e.want)
183		}
184	}
185}
186
187// TestEventRoundTrip checks that an event with a oneof kind survives the wire.
188func TestEventRoundTrip(t *testing.T) {
189	in := &ermite.WatchResponse{Event: &ermite.Event{
190		Id: "42",
191		Kind: &ermite.Event_Pushed{Pushed: &ermite.Pushed{
192			Repo: "ermite",
193			RefUpdates: []*ermite.RefUpdate{
194				{Ref: "refs/heads/main", OldId: "a", NewId: "b"},
195			},
196		}},
197	}}
198	b, err := proto.Marshal(in)
199	if err != nil {
200		t.Fatal(err)
201	}
202	var out ermite.WatchResponse
203	if err := proto.Unmarshal(b, &out); err != nil {
204		t.Fatal(err)
205	}
206	if !proto.Equal(in, &out) {
207		t.Errorf("round trip mismatch: %v != %v", &out, in)
208	}
209	if out.GetEvent().GetPushed().GetRefUpdates()[0].GetNewId() != "b" {
210		t.Errorf("unexpected ref update: %v", &out)
211	}
212}
213
214// TestUpdateRepoMask checks that the request carries a field mask.
215func TestUpdateRepoMask(t *testing.T) {
216	req := &ermite.UpdateRepoRequest{
217		Repo:       "ermite",
218		Visibility: ermite.Visibility_VISIBILITY_PUBLIC,
219		UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"visibility"}},
220	}
221	if got := req.GetUpdateMask().GetPaths(); len(got) != 1 || got[0] != "visibility" {
222		t.Errorf("paths = %v", got)
223	}
224}
225
226// TestMergeRequestEventRoundTrip checks the merge request event kinds and the
227// repository settings survive the wire.
228func TestMergeRequestEventRoundTrip(t *testing.T) {
229	in := &ermite.WatchResponse{Event: &ermite.Event{
230		Id: "43",
231		Kind: &ermite.Event_CommentAdded{CommentAdded: &ermite.CommentAdded{
232			Repo:   "ermite",
233			Number: 7,
234			Comment: &ermite.Comment{
235				Id: "c1", Path: "a.go", Line: 3, Side: ermite.DiffSide_DIFF_SIDE_OLD, Text: "hi",
236			},
237		}},
238	}}
239	b, err := proto.Marshal(in)
240	if err != nil {
241		t.Fatal(err)
242	}
243	var out ermite.WatchResponse
244	if err := proto.Unmarshal(b, &out); err != nil {
245		t.Fatal(err)
246	}
247	if !proto.Equal(in, &out) {
248		t.Errorf("round trip mismatch: %v != %v", &out, in)
249	}
250	if got := out.GetEvent().GetCommentAdded().GetNumber(); got != 7 {
251		t.Errorf("number = %d, want 7", got)
252	}
253
254	req := &ermite.UpdateRepoRequest{
255		Repo: "ermite",
256		Settings: &ermite.RepoSettings{
257			AllowedMergeModes:  []ermite.MergeMode{ermite.MergeMode_MERGE_MODE_SQUASH},
258			FailingChecksBlock: true,
259		},
260		UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"settings.allowed_merge_modes", "settings.failing_checks_block"}},
261	}
262	if got := req.GetSettings().GetAllowedMergeModes(); len(got) != 1 || got[0] != ermite.MergeMode_MERGE_MODE_SQUASH {
263		t.Errorf("modes = %v", got)
264	}
265}
266
267// TestCIEventRoundTrip checks that the CI event kinds keep their field numbers
268// and survive the wire.
269func TestCIEventRoundTrip(t *testing.T) {
270	in := &ermite.WatchResponse{Event: &ermite.Event{
271		Id: "44",
272		Kind: &ermite.Event_JobUpdated{JobUpdated: &ermite.JobUpdated{Job: &ermite.RunJob{
273			Id:         "j1",
274			RunId:      9,
275			Attempt:    2,
276			Status:     ermite.CiStatus_CI_STATUS_COMPLETED,
277			Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT,
278			Isolation:  ermite.RunnerIsolation_RUNNER_ISOLATION_VM,
279			Steps:      []*ermite.Step{{Number: 1, Name: "Set up job", LogStart: 0, LogEnd: 3}},
280		}}},
281	}}
282	b, err := proto.Marshal(in)
283	if err != nil {
284		t.Fatal(err)
285	}
286	var out ermite.WatchResponse
287	if err := proto.Unmarshal(b, &out); err != nil {
288		t.Fatal(err)
289	}
290	if !proto.Equal(in, &out) {
291		t.Errorf("round trip mismatch: %v != %v", &out, in)
292	}
293	f := (&ermite.Event{}).ProtoReflect().Descriptor().Fields()
294	for name, num := range map[string]int{"run_updated": 12, "job_updated": 13} {
295		if fd := f.ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
296			t.Errorf("event field %s: %v, want number %d", name, fd, num)
297		}
298	}
299}
300
301// TestJobRoundTrip checks the job handed to a runner survives the wire.
302func TestJobRoundTrip(t *testing.T) {
303	in := &ermite.FetchJobResponse{Job: &ermite.Job{
304		JobId: "j1", RunId: 9, Attempt: 1, JobToken: "tok", WorkflowPath: ".github/workflows/ci.yml",
305		WorkflowYaml: "on: push", JobKey: "build", Matrix: "{}", Contexts: "{}",
306		Labels: []string{"self-hosted"}, TimeoutMinutes: 360, Repo: "ermite",
307		Sha: "0123456789012345678901234567890123456789", Ref: "refs/heads/main",
308	}}
309	b, err := proto.Marshal(in)
310	if err != nil {
311		t.Fatal(err)
312	}
313	var out ermite.FetchJobResponse
314	if err := proto.Unmarshal(b, &out); err != nil {
315		t.Fatal(err)
316	}
317	if !proto.Equal(in, &out) {
318		t.Errorf("round trip mismatch: %v != %v", &out, in)
319	}
320}
321
322// TestWatchJobRoundTrip checks the three kinds of WatchJob message and the
323// JobEnded detail.
324func TestWatchJobRoundTrip(t *testing.T) {
325	msgs := []*ermite.WatchJobResponse{
326		{Msg: &ermite.WatchJobResponse_Keepalive{Keepalive: &ermite.Keepalive{}}},
327		{Msg: &ermite.WatchJobResponse_Cancel{Cancel: &ermite.CancelRequest{
328			Reason: ermite.CancelReason_CANCEL_REASON_FAIL_FAST, Message: "m",
329		}}},
330		{Msg: &ermite.WatchJobResponse_Ended{Ended: &ermite.JobEnded{
331			Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED, Error: "e",
332		}}},
333	}
334	for _, in := range msgs {
335		b, err := proto.Marshal(in)
336		if err != nil {
337			t.Fatal(err)
338		}
339		var out ermite.WatchJobResponse
340		if err := proto.Unmarshal(b, &out); err != nil {
341			t.Fatal(err)
342		}
343		if !proto.Equal(in, &out) {
344			t.Errorf("round trip mismatch: %v != %v", &out, in)
345		}
346	}
347	// The detail travels in a Connect error.
348	d, err := connect.NewErrorDetail(&ermite.JobEnded{Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT})
349	if err != nil {
350		t.Fatal(err)
351	}
352	cerr := connect.NewError(connect.CodeFailedPrecondition, errors.New("job ended"))
353	cerr.AddDetail(d)
354	v, err := cerr.Details()[0].Value()
355	if err != nil {
356		t.Fatal(err)
357	}
358	if got := v.(*ermite.JobEnded).GetConclusion(); got != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT {
359		t.Errorf("detail conclusion = %v", got)
360	}
361}
362
363// TestCompleteJobRoundTrip checks the final report of a job and its answer.
364func TestCompleteJobRoundTrip(t *testing.T) {
365	in := &ermite.CompleteJobRequest{
366		JobId:          "j1",
367		Conclusion:     ermite.CiConclusion_CI_CONCLUSION_CANCELLED,
368		Outputs:        map[string]string{"a": "b"},
369		RunnerShutdown: true,
370		Steps:          []*ermite.StepUpdate{{Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED}},
371	}
372	b, err := proto.Marshal(in)
373	if err != nil {
374		t.Fatal(err)
375	}
376	var out ermite.CompleteJobRequest
377	if err := proto.Unmarshal(b, &out); err != nil {
378		t.Fatal(err)
379	}
380	if !proto.Equal(in, &out) {
381		t.Errorf("round trip mismatch: %v != %v", &out, in)
382	}
383	r := &ermite.CompleteJobResponse{Applied: false, Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT}
384	if r.GetApplied() || r.GetConclusion() != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT {
385		t.Errorf("unexpected response %v", r)
386	}
387}
388
389// TestRunNameField pins the number of Run.name, the GitHub name of the run
390// that sits next to workflow_name, and that it survives the wire.
391func TestRunNameField(t *testing.T) {
392	fd := (&ermite.Run{}).ProtoReflect().Descriptor().Fields().ByName("name")
393	if fd == nil || fd.Number() != 19 {
394		t.Fatalf("Run.name = %v, want field 19", fd)
395	}
396	in := &ermite.Run{WorkflowName: "Names Workflow", Name: "run push on branch by someone", Title: "run push on branch by someone"}
397	b, err := proto.Marshal(in)
398	if err != nil {
399		t.Fatal(err)
400	}
401	var out ermite.Run
402	if err := proto.Unmarshal(b, &out); err != nil {
403		t.Fatal(err)
404	}
405	if out.GetName() != in.GetName() || out.GetWorkflowName() != "Names Workflow" {
406		t.Errorf("round trip: name %q, workflow_name %q", out.GetName(), out.GetWorkflowName())
407	}
408}
409
410// TestCI2FieldNumbers pins the numbers of the fields added for CI-2.
411func TestCI2FieldNumbers(t *testing.T) {
412	tests := []struct {
413		msg    proto.Message
414		fields map[string]int
415	}{
416		{&ermite.Job{}, map[string]int{"github_token": 15}},
417		{&ermite.UpdateJobRequest{}, map[string]int{"annotations": 3}},
418		{&ermite.CompleteJobRequest{}, map[string]int{"annotations": 6}},
419		{&ermite.StepUpdate{}, map[string]int{"outcome": 7}},
420		{&ermite.Step{}, map[string]int{"outcome": 9}},
421		{&ermite.Annotation{}, map[string]int{
422			"level": 1, "title": 2, "message": 3, "file": 4, "start_line": 5,
423			"end_line": 6, "start_column": 7, "end_column": 8, "step_number": 9,
424		}},
425		{&ermite.ActionRef{}, map[string]int{"uses": 1}},
426		{&ermite.ResolveActionsRequest{}, map[string]int{"job_id": 1, "refs": 2}},
427		{&ermite.ResolveActionsResponse{}, map[string]int{"actions": 1}},
428		{&ermite.ResolvedAction{}, map[string]int{
429			"ref": 1, "name_with_owner": 2, "path": 3, "resolved_sha": 4,
430			"tarball_url": 5, "token": 6, "source": 7, "error": 8,
431		}},
432		{&ermite.UploadSummaryRequest{}, map[string]int{"job_id": 1, "step_number": 2, "markdown": 3}},
433		{&ermite.ListAnnotationsRequest{}, map[string]int{"repo": 1, "run_id": 2, "attempt": 3, "job_id": 4}},
434		{&ermite.ListAnnotationsResponse{}, map[string]int{"annotations": 1, "truncated": 2}},
435		{&ermite.JobAnnotation{}, map[string]int{"job_id": 1, "annotation": 2}},
436		{&ermite.GetStepSummariesRequest{}, map[string]int{"repo": 1, "job_id": 2, "attempt": 3}},
437		{&ermite.GetStepSummariesResponse{}, map[string]int{"summaries": 1, "truncated": 2}},
438		{&ermite.StepSummary{}, map[string]int{"step_number": 1, "markdown": 2}},
439	}
440	for _, tt := range tests {
441		md := tt.msg.ProtoReflect().Descriptor()
442		for name, num := range tt.fields {
443			if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
444				t.Errorf("%s.%s: %v, want number %d", md.Name(), name, fd, num)
445			}
446		}
447	}
448}
449
450// TestCI2RoundTrip checks that the CI-2 messages survive the wire.
451func TestCI2RoundTrip(t *testing.T) {
452	msgs := []proto.Message{
453		&ermite.Job{JobId: "j1", GithubToken: "ermitegh_x"},
454		&ermite.UpdateJobRequest{
455			JobId: "j1",
456			Steps: []*ermite.StepUpdate{{
457				Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED,
458				Conclusion: ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
459				Outcome:    ermite.CiConclusion_CI_CONCLUSION_FAILURE,
460			}},
461			Annotations: []*ermite.Annotation{{
462				Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING, Title: "t", Message: "m",
463				File: "a.go", StartLine: 1, EndLine: 2, StartColumn: 3, EndColumn: 4, StepNumber: 2,
464			}},
465		},
466		&ermite.CompleteJobRequest{
467			JobId:       "j1",
468			Conclusion:  ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
469			Annotations: []*ermite.Annotation{{Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE, Message: "m", StepNumber: 3}},
470		},
471		&ermite.ResolveActionsRequest{JobId: "j1", Refs: []*ermite.ActionRef{{Uses: "actions/checkout@v4"}, {Uses: "o/r/p@main"}}},
472		&ermite.ResolveActionsResponse{Actions: []*ermite.ResolvedAction{
473			{
474				Ref: "o/r/p@main", NameWithOwner: "o/r", Path: "p", ResolvedSha: "0123456789012345678901234567890123456789",
475				TarballUrl: "https://example.test/o/r.tar.gz", Token: "t", Source: ermite.ActionSource_ACTION_SOURCE_ERMITE,
476			},
477			{Ref: "x/y@z", Error: "Unable to resolve action `x/y@z`, unable to find version `z`"},
478		}},
479		&ermite.UploadSummaryRequest{JobId: "j1", StepNumber: 2, Markdown: "# hi"},
480		&ermite.ListAnnotationsResponse{Annotations: []*ermite.JobAnnotation{{JobId: "j1", Annotation: &ermite.Annotation{Message: "m"}}}, Truncated: true},
481		&ermite.GetStepSummariesResponse{Summaries: []*ermite.StepSummary{{StepNumber: 2, Markdown: "# hi"}}, Truncated: true},
482		&ermite.RunJob{Id: "j1", Steps: []*ermite.Step{{Number: 2, Outcome: ermite.CiConclusion_CI_CONCLUSION_FAILURE}}},
483	}
484	for _, in := range msgs {
485		b, err := proto.Marshal(in)
486		if err != nil {
487			t.Fatal(err)
488		}
489		out := in.ProtoReflect().New().Interface()
490		if err := proto.Unmarshal(b, out); err != nil {
491			t.Fatal(err)
492		}
493		if !proto.Equal(in, out) {
494			t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
495		}
496	}
497}
498
499// TestCI3FieldNumbers pins the numbers of the fields added for CI-3.
500func TestCI3FieldNumbers(t *testing.T) {
501	tests := []struct {
502		msg    proto.Message
503		fields map[string]int
504	}{
505		{&ermite.Job{}, map[string]int{
506			"secrets": 16, "vars": 17, "environment": 18, "permissions": 19, "cache": 20, "trusted": 21,
507		}},
508		{&ermite.JobPermissions{}, map[string]int{"levels": 1}},
509		{&ermite.JobCache{}, map[string]int{"repository": 1, "write_ref": 2, "read_refs": 3}},
510		{&ermite.Run{}, map[string]int{"trusted": 20}},
511		{&ermite.RunJob{}, map[string]int{"concurrency_group": 20}},
512		{&ermite.DispatchWorkflowRequest{}, map[string]int{"repo": 1, "workflow_path": 2, "ref": 3, "inputs": 4}},
513		{&ermite.DispatchWorkflowResponse{}, map[string]int{"run": 1}},
514		{&ermite.Workflow{}, map[string]int{"path": 1, "name": 2, "state": 3}},
515		{&ermite.ListWorkflowsRequest{}, map[string]int{"repo": 1}},
516		{&ermite.ListWorkflowsResponse{}, map[string]int{"workflows": 1}},
517		{&ermite.DisableWorkflowRequest{}, map[string]int{"repo": 1, "workflow": 2}},
518		{&ermite.DisableWorkflowResponse{}, map[string]int{"workflow": 1}},
519		{&ermite.EnableWorkflowRequest{}, map[string]int{"repo": 1, "workflow": 2}},
520		{&ermite.EnableWorkflowResponse{}, map[string]int{"workflow": 1}},
521		{&ermite.SettingsScope{}, map[string]int{"instance": 1, "repo": 2, "environment": 3}},
522		{&ermite.EnvironmentRef{}, map[string]int{"repo": 1, "name": 2}},
523		{&ermite.Secret{}, map[string]int{"name": 1, "updated_at": 2}},
524		{&ermite.Variable{}, map[string]int{"name": 1, "value": 2, "updated_at": 3}},
525		{&ermite.Environment{}, map[string]int{"repo": 1, "name": 2, "created_at": 3}},
526		{&ermite.SetSecretRequest{}, map[string]int{"scope": 1, "name": 2, "value": 3}},
527		{&ermite.ListSecretsRequest{}, map[string]int{"scope": 1, "page_size": 2, "page_token": 3}},
528		{&ermite.ListSecretsResponse{}, map[string]int{"secrets": 1, "next_page_token": 2}},
529		{&ermite.ListVariablesResponse{}, map[string]int{"variables": 1, "next_page_token": 2}},
530		{&ermite.ListEnvironmentsRequest{}, map[string]int{"repo": 1, "page_size": 2, "page_token": 3}},
531		{&ermite.ListEnvironmentsResponse{}, map[string]int{"environments": 1, "next_page_token": 2}},
532	}
533	for _, tt := range tests {
534		md := tt.msg.ProtoReflect().Descriptor()
535		for name, num := range tt.fields {
536			if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
537				t.Errorf("%s.%s: %v, want number %d", md.Name(), name, fd, num)
538			}
539		}
540	}
541}
542
543// TestCI3RoundTrip checks that the CI-3 messages survive the wire.
544func TestCI3RoundTrip(t *testing.T) {
545	msgs := []proto.Message{
546		&ermite.Job{
547			JobId: "j1", Secrets: map[string]string{"TOKEN": "s3cret"}, Vars: map[string]string{"REGION": "eu"},
548			Environment: "staging", Trusted: true,
549			Permissions: &ermite.JobPermissions{Levels: map[string]string{"contents": "write", "packages": "none"}},
550			Cache: &ermite.JobCache{
551				Repository: "ermite", WriteRef: "refs/pull/7/merge",
552				ReadRefs: []string{"refs/pull/7/merge", "refs/heads/main"},
553			},
554		},
555		&ermite.Run{Id: 9, Event: "workflow_dispatch", Trusted: true},
556		&ermite.RunJob{Id: "j1", Status: ermite.CiStatus_CI_STATUS_PENDING, ConcurrencyGroup: "deploy"},
557		&ermite.DispatchWorkflowRequest{Repo: "ermite", WorkflowPath: "ci.yml", Ref: "main", Inputs: map[string]string{"debug": "true"}},
558		&ermite.DispatchWorkflowResponse{Run: &ermite.Run{Id: 9, Event: "workflow_dispatch"}},
559		&ermite.ListWorkflowsResponse{Workflows: []*ermite.Workflow{{Path: ".github/workflows/release.yml", Name: "Release", State: ermite.WorkflowState_WORKFLOW_STATE_DISABLED}}},
560		&ermite.DisableWorkflowRequest{Repo: "ermite", Workflow: "release.yml"},
561		&ermite.EnableWorkflowResponse{Workflow: &ermite.Workflow{Path: ".github/workflows/release.yml", State: ermite.WorkflowState_WORKFLOW_STATE_ENABLED}},
562		&ermite.SetSecretRequest{Scope: &ermite.SettingsScope{Where: &ermite.SettingsScope_Instance{Instance: true}}, Name: "A", Value: "v"},
563		&ermite.SetSecretRequest{Scope: &ermite.SettingsScope{Where: &ermite.SettingsScope_Repo{Repo: "ermite"}}, Name: "A", Value: "v"},
564		&ermite.SetVariableRequest{
565			Scope: &ermite.SettingsScope{Where: &ermite.SettingsScope_Environment{Environment: &ermite.EnvironmentRef{Repo: "ermite", Name: "prod"}}},
566			Name:  "A", Value: "v",
567		},
568		&ermite.ListSecretsResponse{Secrets: []*ermite.Secret{{Name: "A"}}, NextPageToken: "t"},
569		&ermite.ListEnvironmentsResponse{Environments: []*ermite.Environment{{Repo: "ermite", Name: "prod"}}},
570	}
571	for _, in := range msgs {
572		b, err := proto.Marshal(in)
573		if err != nil {
574			t.Fatal(err)
575		}
576		out := in.ProtoReflect().New().Interface()
577		if err := proto.Unmarshal(b, out); err != nil {
578			t.Fatal(err)
579		}
580		if !proto.Equal(in, out) {
581			t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
582		}
583	}
584}
585
586// TestMirrorWire pins event numbers and verifies presence of explicit zero patches.
587func TestMirrorWire(t *testing.T) {
588	fields := (&ermite.Event{}).ProtoReflect().Descriptor().Fields()
589	for name, number := range map[protoreflect.Name]protoreflect.FieldNumber{"mirror_failed": 23, "mirror_recovered": 24} {
590		if fd := fields.ByName(name); fd == nil || fd.Number() != number || fd.ContainingOneof().Name() != "kind" {
591			t.Fatalf("Event.%s: %v, want kind field %d", name, fd, number)
592		}
593	}
594	for _, msg := range []proto.Message{&ermite.MirrorFailed{}, &ermite.MirrorRecovered{}} {
595		fields := msg.ProtoReflect().Descriptor().Fields()
596		if fields.ByName("url") != nil || fields.ByName("detail") != nil {
597			t.Fatalf("%T leaks URL or detail", msg)
598		}
599	}
600	for _, in := range []proto.Message{
601		&ermite.SetMirrorConfigRequest{Enabled: proto.Bool(false), UrlTemplate: proto.String(""), Cron: proto.String(""), AllBranches: proto.Bool(false)},
602		&ermite.SetRepoMirrorRequest{Repo: "repo", AllBranches: proto.Bool(false)},
603		&ermite.Event{Kind: &ermite.Event_MirrorFailed{MirrorFailed: &ermite.MirrorFailed{Repo: "repo", State: ermite.MirrorState_MIRROR_STATE_PAUSED, FailureClass: "diverged"}}},
604		&ermite.Event{Kind: &ermite.Event_MirrorRecovered{MirrorRecovered: &ermite.MirrorRecovered{Repo: "repo"}}},
605	} {
606		b, err := proto.Marshal(in)
607		if err != nil {
608			t.Fatal(err)
609		}
610		out := in.ProtoReflect().New().Interface()
611		if err := proto.Unmarshal(b, out); err != nil {
612			t.Fatal(err)
613		}
614		if !proto.Equal(in, out) {
615			t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
616		}
617	}
618}

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 2

Tags 18

↑↓ moveenter openesc close