feat(ci): add Run.name and title tag runs with the name registered on the default branch
- commit
- 20985cc5d4e61cdb82c6a652dc663759022fea32
- author
- brice · · 2026-10-13 03:36 UTC
- parent
- 1f94d4a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +187 -45
M gen/ermite/storage/ci.pb.go +16 -4
| @@ -391,8 +391,12 @@ type CIRun struct { | |||
| 391 | 391 | CompletedAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"` | |
| 392 | 392 | CancelRequested bool `protobuf:"varint,20,opt,name=cancel_requested,json=cancelRequested,proto3" json:"cancel_requested,omitempty"` | |
| 393 | 393 | CancelRequestedAt *timestamppb.Timestamp `protobuf:"bytes,21,opt,name=cancel_requested_at,json=cancelRequestedAt,proto3" json:"cancel_requested_at,omitempty"` | |
| 394 | - | unknownFields protoimpl.UnknownFields | |
| 395 | - | sizeCache protoimpl.SizeCache | |
| 394 | + | // name is GitHub's name of the run: the evaluated run-name, else the | |
| 395 | + | // workflow name. Empty in runs stored before it existed (read as | |
| 396 | + | // workflow_name). | |
| 397 | + | Name string `protobuf:"bytes,22,opt,name=name,proto3" json:"name,omitempty"` | |
| 398 | + | unknownFields protoimpl.UnknownFields | |
| 399 | + | sizeCache protoimpl.SizeCache | |
| 396 | 400 | } | |
| 397 | 401 | ||
| 398 | 402 | func (x *CIRun) Reset() { | |
| @@ -558,6 +562,13 @@ func (x *CIRun) GetCancelRequestedAt() *timestamppb.Timestamp { | |||
| 558 | 562 | return nil | |
| 559 | 563 | } | |
| 560 | 564 | ||
| 565 | + | func (x *CIRun) GetName() string { | |
| 566 | + | if x != nil { | |
| 567 | + | return x.Name | |
| 568 | + | } | |
| 569 | + | return "" | |
| 570 | + | } | |
| 571 | + | ||
| 561 | 572 | // CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned | |
| 562 | 573 | // from, kept apart so that listing runs does not decode it. | |
| 563 | 574 | type CIRunSource struct { | |
| @@ -1353,7 +1364,7 @@ var File_ermite_storage_ci_proto protoreflect.FileDescriptor | |||
| 1353 | 1364 | ||
| 1354 | 1365 | const file_ermite_storage_ci_proto_rawDesc = "" + | |
| 1355 | 1366 | "\n" + | |
| 1356 | - | "\x17ermite/storage/ci.proto\x12\x0eermite.storage\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcb\x05\n" + | |
| 1367 | + | "\x17ermite/storage/ci.proto\x12\x0eermite.storage\x1a\x1fgoogle/protobuf/timestamp.proto\"\xdf\x05\n" + | |
| 1357 | 1368 | "\x05CIRun\x12\x12\n" + | |
| 1358 | 1369 | "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x0e\n" + | |
| 1359 | 1370 | "\x02id\x18\x02 \x01(\x03R\x02id\x12\x16\n" + | |
| @@ -1378,7 +1389,8 @@ const file_ermite_storage_ci_proto_rawDesc = "" + | |||
| 1378 | 1389 | "started_at\x18\x10 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12=\n" + | |
| 1379 | 1390 | "\fcompleted_at\x18\x11 \x01(\v2\x1a.google.protobuf.TimestampR\vcompletedAt\x12)\n" + | |
| 1380 | 1391 | "\x10cancel_requested\x18\x14 \x01(\bR\x0fcancelRequested\x12J\n" + | |
| 1381 | - | "\x13cancel_requested_at\x18\x15 \x01(\v2\x1a.google.protobuf.TimestampR\x11cancelRequestedAtJ\x04\b\x12\x10\x13J\x04\b\x13\x10\x14\"~\n" + | |
| 1392 | + | "\x13cancel_requested_at\x18\x15 \x01(\v2\x1a.google.protobuf.TimestampR\x11cancelRequestedAt\x12\x12\n" + | |
| 1393 | + | "\x04name\x18\x16 \x01(\tR\x04nameJ\x04\b\x12\x10\x13J\x04\b\x13\x10\x14\"~\n" + | |
| 1382 | 1394 | "\vCIRunSource\x12\x15\n" + | |
| 1383 | 1395 | "\x06run_id\x18\x04 \x01(\x03R\x05runId\x12#\n" + | |
| 1384 | 1396 | "\rworkflow_yaml\x18\x01 \x01(\tR\fworkflowYaml\x12\x1f\n" + | |
| @@ -391,8 +391,12 @@ type CIRun struct { | |||||
| 391 | CompletedAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"` | 391 | CompletedAt *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=completed_at,json=completedAt,proto3" json:"completed_at,omitempty"` | ||
| 392 | CancelRequested bool `protobuf:"varint,20,opt,name=cancel_requested,json=cancelRequested,proto3" json:"cancel_requested,omitempty"` | 392 | CancelRequested bool `protobuf:"varint,20,opt,name=cancel_requested,json=cancelRequested,proto3" json:"cancel_requested,omitempty"` | ||
| 393 | CancelRequestedAt *timestamppb.Timestamp `protobuf:"bytes,21,opt,name=cancel_requested_at,json=cancelRequestedAt,proto3" json:"cancel_requested_at,omitempty"` | 393 | CancelRequestedAt *timestamppb.Timestamp `protobuf:"bytes,21,opt,name=cancel_requested_at,json=cancelRequestedAt,proto3" json:"cancel_requested_at,omitempty"` | ||
| 394 | - | unknownFields protoimpl.UnknownFields | 394 | + | // name is GitHub's name of the run: the evaluated run-name, else the |
| 395 | - | sizeCache protoimpl.SizeCache | 395 | + | // workflow name. Empty in runs stored before it existed (read as |
| 396 | + | // workflow_name). | |||
| 397 | + | Name string `protobuf:"bytes,22,opt,name=name,proto3" json:"name,omitempty"` | |||
| 398 | + | unknownFields protoimpl.UnknownFields | |||
| 399 | + | sizeCache protoimpl.SizeCache | |||
| 396 | } | 400 | } | ||
| 397 | 401 | ||||
| 398 | func (x *CIRun) Reset() { | 402 | func (x *CIRun) Reset() { | ||
| @@ -558,6 +562,13 @@ func (x *CIRun) GetCancelRequestedAt() *timestamppb.Timestamp { | |||||
| 558 | return nil | 562 | return nil | ||
| 559 | } | 563 | } | ||
| 560 | 564 | ||||
| 565 | + | func (x *CIRun) GetName() string { | |||
| 566 | + | if x != nil { | |||
| 567 | + | return x.Name | |||
| 568 | + | } | |||
| 569 | + | return "" | |||
| 570 | + | } | |||
| 571 | + | ||||
| 561 | // CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned | 572 | // CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned | ||
| 562 | // from, kept apart so that listing runs does not decode it. | 573 | // from, kept apart so that listing runs does not decode it. | ||
| 563 | type CIRunSource struct { | 574 | type CIRunSource struct { | ||
| @@ -1353,7 +1364,7 @@ var File_ermite_storage_ci_proto protoreflect.FileDescriptor | |||||
| 1353 | 1364 | ||||
| 1354 | const file_ermite_storage_ci_proto_rawDesc = "" + | 1365 | const file_ermite_storage_ci_proto_rawDesc = "" + | ||
| 1355 | "\n" + | 1366 | "\n" + | ||
| 1356 | - | "\x17ermite/storage/ci.proto\x12\x0eermite.storage\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcb\x05\n" + | 1367 | + | "\x17ermite/storage/ci.proto\x12\x0eermite.storage\x1a\x1fgoogle/protobuf/timestamp.proto\"\xdf\x05\n" + |
| 1357 | "\x05CIRun\x12\x12\n" + | 1368 | "\x05CIRun\x12\x12\n" + | ||
| 1358 | "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x0e\n" + | 1369 | "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x0e\n" + | ||
| 1359 | "\x02id\x18\x02 \x01(\x03R\x02id\x12\x16\n" + | 1370 | "\x02id\x18\x02 \x01(\x03R\x02id\x12\x16\n" + | ||
| @@ -1378,7 +1389,8 @@ const file_ermite_storage_ci_proto_rawDesc = "" + | |||||
| 1378 | "started_at\x18\x10 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12=\n" + | 1389 | "started_at\x18\x10 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12=\n" + | ||
| 1379 | "\fcompleted_at\x18\x11 \x01(\v2\x1a.google.protobuf.TimestampR\vcompletedAt\x12)\n" + | 1390 | "\fcompleted_at\x18\x11 \x01(\v2\x1a.google.protobuf.TimestampR\vcompletedAt\x12)\n" + | ||
| 1380 | "\x10cancel_requested\x18\x14 \x01(\bR\x0fcancelRequested\x12J\n" + | 1391 | "\x10cancel_requested\x18\x14 \x01(\bR\x0fcancelRequested\x12J\n" + | ||
| 1381 | - | "\x13cancel_requested_at\x18\x15 \x01(\v2\x1a.google.protobuf.TimestampR\x11cancelRequestedAtJ\x04\b\x12\x10\x13J\x04\b\x13\x10\x14\"~\n" + | 1392 | + | "\x13cancel_requested_at\x18\x15 \x01(\v2\x1a.google.protobuf.TimestampR\x11cancelRequestedAt\x12\x12\n" + |
| 1393 | + | "\x04name\x18\x16 \x01(\tR\x04nameJ\x04\b\x12\x10\x13J\x04\b\x13\x10\x14\"~\n" + | |||
| 1382 | "\vCIRunSource\x12\x15\n" + | 1394 | "\vCIRunSource\x12\x15\n" + | ||
| 1383 | "\x06run_id\x18\x04 \x01(\x03R\x05runId\x12#\n" + | 1395 | "\x06run_id\x18\x04 \x01(\x03R\x05runId\x12#\n" + | ||
| 1384 | "\rworkflow_yaml\x18\x01 \x01(\tR\fworkflowYaml\x12\x1f\n" + | 1396 | "\rworkflow_yaml\x18\x01 \x01(\tR\fworkflowYaml\x12\x1f\n" + | ||
M internal/adapters/kvstore/ci.go +6 -2
| @@ -268,7 +268,7 @@ func encodeRun(r domain.CIRun) ([]byte, error) { | |||
| 268 | 268 | } | |
| 269 | 269 | return mustMarshal(&storagepb.CIRun{ | |
| 270 | 270 | Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt, | |
| 271 | - | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Title: r.Title, | |
| 271 | + | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Name: r.Name, Title: r.Title, | |
| 272 | 272 | Event: r.Event, Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, | |
| 273 | 273 | Status: encodeStatus(r.Status), Conclusion: encodeConclusion(r.Conclusion), Error: r.Error, | |
| 274 | 274 | CreatedAt: encodeTime(r.CreatedAt), StartedAt: encodeTime(r.StartedAt), CompletedAt: encodeTime(r.CompletedAt), | |
| @@ -285,9 +285,13 @@ func decodeRun(item kv.Item) (domain.CIRun, error) { | |||
| 285 | 285 | if err != nil { | |
| 286 | 286 | return domain.CIRun{}, fmt.Errorf("decode run: %w", err) | |
| 287 | 287 | } | |
| 288 | + | name := pb.GetName() | |
| 289 | + | if name == "" { | |
| 290 | + | name = pb.GetWorkflowName() // stored before the field existed | |
| 291 | + | } | |
| 288 | 292 | return domain.CIRun{ | |
| 289 | 293 | Repo: repo, ID: pb.GetId(), Number: pb.GetNumber(), Attempt: pb.GetAttempt(), | |
| 290 | - | WorkflowPath: pb.GetWorkflowPath(), WorkflowName: pb.GetWorkflowName(), Title: pb.GetTitle(), | |
| 294 | + | WorkflowPath: pb.GetWorkflowPath(), WorkflowName: pb.GetWorkflowName(), Name: name, Title: pb.GetTitle(), | |
| 291 | 295 | Event: pb.GetEvent(), Ref: pb.GetRef(), Sha: pb.GetSha(), Actor: pb.GetActor(), | |
| 292 | 296 | Status: decodeStatus(pb.GetStatus()), Conclusion: decodeConclusion(pb.GetConclusion()), Error: pb.GetError(), | |
| 293 | 297 | CreatedAt: decodeTime(pb.GetCreatedAt()), StartedAt: decodeTime(pb.GetStartedAt()), CompletedAt: decodeTime(pb.GetCompletedAt()), | |
| @@ -268,7 +268,7 @@ func encodeRun(r domain.CIRun) ([]byte, error) { | |||||
| 268 | } | 268 | } | ||
| 269 | return mustMarshal(&storagepb.CIRun{ | 269 | return mustMarshal(&storagepb.CIRun{ | ||
| 270 | Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt, | 270 | Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt, | ||
| 271 | - | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Title: r.Title, | 271 | + | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Name: r.Name, Title: r.Title, |
| 272 | Event: r.Event, Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, | 272 | Event: r.Event, Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, | ||
| 273 | Status: encodeStatus(r.Status), Conclusion: encodeConclusion(r.Conclusion), Error: r.Error, | 273 | Status: encodeStatus(r.Status), Conclusion: encodeConclusion(r.Conclusion), Error: r.Error, | ||
| 274 | CreatedAt: encodeTime(r.CreatedAt), StartedAt: encodeTime(r.StartedAt), CompletedAt: encodeTime(r.CompletedAt), | 274 | CreatedAt: encodeTime(r.CreatedAt), StartedAt: encodeTime(r.StartedAt), CompletedAt: encodeTime(r.CompletedAt), | ||
| @@ -285,9 +285,13 @@ func decodeRun(item kv.Item) (domain.CIRun, error) { | |||||
| 285 | if err != nil { | 285 | if err != nil { | ||
| 286 | return domain.CIRun{}, fmt.Errorf("decode run: %w", err) | 286 | return domain.CIRun{}, fmt.Errorf("decode run: %w", err) | ||
| 287 | } | 287 | } | ||
| 288 | + | name := pb.GetName() | |||
| 289 | + | if name == "" { | |||
| 290 | + | name = pb.GetWorkflowName() // stored before the field existed | |||
| 291 | + | } | |||
| 288 | return domain.CIRun{ | 292 | return domain.CIRun{ | ||
| 289 | Repo: repo, ID: pb.GetId(), Number: pb.GetNumber(), Attempt: pb.GetAttempt(), | 293 | Repo: repo, ID: pb.GetId(), Number: pb.GetNumber(), Attempt: pb.GetAttempt(), | ||
| 290 | - | WorkflowPath: pb.GetWorkflowPath(), WorkflowName: pb.GetWorkflowName(), Title: pb.GetTitle(), | 294 | + | WorkflowPath: pb.GetWorkflowPath(), WorkflowName: pb.GetWorkflowName(), Name: name, Title: pb.GetTitle(), |
| 291 | Event: pb.GetEvent(), Ref: pb.GetRef(), Sha: pb.GetSha(), Actor: pb.GetActor(), | 295 | Event: pb.GetEvent(), Ref: pb.GetRef(), Sha: pb.GetSha(), Actor: pb.GetActor(), | ||
| 292 | Status: decodeStatus(pb.GetStatus()), Conclusion: decodeConclusion(pb.GetConclusion()), Error: pb.GetError(), | 296 | Status: decodeStatus(pb.GetStatus()), Conclusion: decodeConclusion(pb.GetConclusion()), Error: pb.GetError(), | ||
| 293 | CreatedAt: decodeTime(pb.GetCreatedAt()), StartedAt: decodeTime(pb.GetStartedAt()), CompletedAt: decodeTime(pb.GetCompletedAt()), | 297 | CreatedAt: decodeTime(pb.GetCreatedAt()), StartedAt: decodeTime(pb.GetStartedAt()), CompletedAt: decodeTime(pb.GetCompletedAt()), | ||
M internal/adapters/kvstore/ci_test.go +25 -0
| @@ -373,3 +373,28 @@ func TestCICancelReasonsRoundTrip(t *testing.T) { | |||
| 373 | 373 | assert.Equal(t, reason, got.CancelReason) | |
| 374 | 374 | } | |
| 375 | 375 | } | |
| 376 | + | ||
| 377 | + | // Run.Name round-trips, and a run stored without one (before the field existed) | |
| 378 | + | // reads as its workflow name. | |
| 379 | + | func TestCIRunNameRoundTripsAndDefaultsToTheWorkflowName(t *testing.T) { | |
| 380 | + | ctx := context.Background() | |
| 381 | + | _, ci, n := ciRepo(t) | |
| 382 | + | ||
| 383 | + | old, err := ci.CreateRun(ctx, n, ".github/workflows/ci.yml", draftOf(n, ".github/workflows/ci.yml", 0)) | |
| 384 | + | require.NoError(t, err) | |
| 385 | + | got, err := ci.GetRun(ctx, n, old.Run.ID) | |
| 386 | + | require.NoError(t, err) | |
| 387 | + | assert.Equal(t, "ci", got.WorkflowName) | |
| 388 | + | assert.Equal(t, "ci", got.Name, "no stored name: the workflow name") | |
| 389 | + | ||
| 390 | + | named, err := ci.CreateRun(ctx, n, ".github/workflows/ci.yml", func(id, number int64) ports.CIRunDraft { | |
| 391 | + | d := draftOf(n, ".github/workflows/ci.yml", 0)(id, number) | |
| 392 | + | d.Run.Name = "run push on branch by someone" | |
| 393 | + | return d | |
| 394 | + | }) | |
| 395 | + | require.NoError(t, err) | |
| 396 | + | got, err = ci.GetRun(ctx, n, named.Run.ID) | |
| 397 | + | require.NoError(t, err) | |
| 398 | + | assert.Equal(t, "run push on branch by someone", got.Name) | |
| 399 | + | assert.Equal(t, "ci", got.WorkflowName, "the workflow name is unchanged") | |
| 400 | + | } | |
| @@ -373,3 +373,28 @@ func TestCICancelReasonsRoundTrip(t *testing.T) { | |||||
| 373 | assert.Equal(t, reason, got.CancelReason) | 373 | assert.Equal(t, reason, got.CancelReason) | ||
| 374 | } | 374 | } | ||
| 375 | } | 375 | } | ||
| 376 | + | ||||
| 377 | + | // Run.Name round-trips, and a run stored without one (before the field existed) | |||
| 378 | + | // reads as its workflow name. | |||
| 379 | + | func TestCIRunNameRoundTripsAndDefaultsToTheWorkflowName(t *testing.T) { | |||
| 380 | + | ctx := context.Background() | |||
| 381 | + | _, ci, n := ciRepo(t) | |||
| 382 | + | ||||
| 383 | + | old, err := ci.CreateRun(ctx, n, ".github/workflows/ci.yml", draftOf(n, ".github/workflows/ci.yml", 0)) | |||
| 384 | + | require.NoError(t, err) | |||
| 385 | + | got, err := ci.GetRun(ctx, n, old.Run.ID) | |||
| 386 | + | require.NoError(t, err) | |||
| 387 | + | assert.Equal(t, "ci", got.WorkflowName) | |||
| 388 | + | assert.Equal(t, "ci", got.Name, "no stored name: the workflow name") | |||
| 389 | + | ||||
| 390 | + | named, err := ci.CreateRun(ctx, n, ".github/workflows/ci.yml", func(id, number int64) ports.CIRunDraft { | |||
| 391 | + | d := draftOf(n, ".github/workflows/ci.yml", 0)(id, number) | |||
| 392 | + | d.Run.Name = "run push on branch by someone" | |||
| 393 | + | return d | |||
| 394 | + | }) | |||
| 395 | + | require.NoError(t, err) | |||
| 396 | + | got, err = ci.GetRun(ctx, n, named.Run.ID) | |||
| 397 | + | require.NoError(t, err) | |||
| 398 | + | assert.Equal(t, "run push on branch by someone", got.Name) | |||
| 399 | + | assert.Equal(t, "ci", got.WorkflowName, "the workflow name is unchanged") | |||
| 400 | + | } | |||
M internal/adapters/rpc/ci_convert.go +10 -1
| @@ -130,10 +130,19 @@ func cancelReasonToPB(r domain.CICancelReason) pb.CancelReason { | |||
| 130 | 130 | return pb.CancelReason_CANCEL_REASON_UNSPECIFIED | |
| 131 | 131 | } | |
| 132 | 132 | ||
| 133 | + | // runName is Run.name: the name of the run, the workflow name for a run that | |
| 134 | + | // has none of its own. | |
| 135 | + | func runName(r domain.CIRun) string { | |
| 136 | + | if r.Name == "" { | |
| 137 | + | return r.WorkflowName | |
| 138 | + | } | |
| 139 | + | return r.Name | |
| 140 | + | } | |
| 141 | + | ||
| 133 | 142 | func runToPB(r domain.CIRun) *pb.Run { | |
| 134 | 143 | return &pb.Run{ | |
| 135 | 144 | Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt, | |
| 136 | - | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Title: r.Title, Event: r.Event, | |
| 145 | + | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Name: runName(r), Title: r.Title, Event: r.Event, | |
| 137 | 146 | Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, Status: statusToPB(r.Status), Conclusion: conclusionToPB(r.Conclusion), | |
| 138 | 147 | Error: r.Error, CreatedAt: timestamp(r.CreatedAt), StartedAt: timestamp(r.StartedAt), CompletedAt: timestamp(r.CompletedAt), | |
| 139 | 148 | Version: int64(r.Version), //nolint:gosec // a store sequence, far below 2^63 | |
| @@ -130,10 +130,19 @@ func cancelReasonToPB(r domain.CICancelReason) pb.CancelReason { | |||||
| 130 | return pb.CancelReason_CANCEL_REASON_UNSPECIFIED | 130 | return pb.CancelReason_CANCEL_REASON_UNSPECIFIED | ||
| 131 | } | 131 | } | ||
| 132 | 132 | ||||
| 133 | + | // runName is Run.name: the name of the run, the workflow name for a run that | |||
| 134 | + | // has none of its own. | |||
| 135 | + | func runName(r domain.CIRun) string { | |||
| 136 | + | if r.Name == "" { | |||
| 137 | + | return r.WorkflowName | |||
| 138 | + | } | |||
| 139 | + | return r.Name | |||
| 140 | + | } | |||
| 141 | + | ||||
| 133 | func runToPB(r domain.CIRun) *pb.Run { | 142 | func runToPB(r domain.CIRun) *pb.Run { | ||
| 134 | return &pb.Run{ | 143 | return &pb.Run{ | ||
| 135 | Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt, | 144 | Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt, | ||
| 136 | - | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Title: r.Title, Event: r.Event, | 145 | + | WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Name: runName(r), Title: r.Title, Event: r.Event, |
| 137 | Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, Status: statusToPB(r.Status), Conclusion: conclusionToPB(r.Conclusion), | 146 | Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, Status: statusToPB(r.Status), Conclusion: conclusionToPB(r.Conclusion), | ||
| 138 | Error: r.Error, CreatedAt: timestamp(r.CreatedAt), StartedAt: timestamp(r.StartedAt), CompletedAt: timestamp(r.CompletedAt), | 147 | Error: r.Error, CreatedAt: timestamp(r.CreatedAt), StartedAt: timestamp(r.StartedAt), CompletedAt: timestamp(r.CompletedAt), | ||
| 139 | Version: int64(r.Version), //nolint:gosec // a store sequence, far below 2^63 | 148 | Version: int64(r.Version), //nolint:gosec // a store sequence, far below 2^63 | ||
M internal/app/ci_parity_test.go +44 -2
| @@ -220,7 +220,10 @@ func jsonNumber(v any) string { | |||
| 220 | 220 | return string(b) | |
| 221 | 221 | } | |
| 222 | 222 | ||
| 223 | - | // The run title is the evaluated run-name, else the head commit message. | |
| 223 | + | // The run title is the evaluated run-name, else the head commit message. The | |
| 224 | + | // name of the run is the evaluated run-name too (recorded: ctx-github-names, | |
| 225 | + | // name = title = the run-name), else the workflow name; github.workflow stays | |
| 226 | + | // the workflow name. | |
| 224 | 227 | func TestCIRunTitle(t *testing.T) { | |
| 225 | 228 | c := newCIHarness(t, nil) | |
| 226 | 229 | c.registerRunner("alpha", "ubuntu-latest") | |
| @@ -234,14 +237,24 @@ jobs: | |||
| 234 | 237 | - run: echo hi | |
| 235 | 238 | `, ".github/workflows/plain.yml": workflowWith("branches: [main]")}, "the head commit\n\nbody") | |
| 236 | 239 | c.waitFor("two runs", func() bool { return len(c.runs()) == 2 }) | |
| 237 | - | titles := map[string]string{} | |
| 240 | + | titles, names, workflows := map[string]string{}, map[string]string{}, map[string]string{} | |
| 238 | 241 | for _, r := range c.runs() { | |
| 239 | 242 | titles[r.GetWorkflowPath()] = r.GetTitle() | |
| 243 | + | names[r.GetWorkflowPath()] = r.GetName() | |
| 244 | + | workflows[r.GetWorkflowPath()] = r.GetWorkflowName() | |
| 240 | 245 | } | |
| 241 | 246 | assert.Equal(t, map[string]string{ | |
| 242 | 247 | ".github/workflows/named.yml": "run push on branch by someone", | |
| 243 | 248 | ".github/workflows/plain.yml": "the head commit", | |
| 244 | 249 | }, titles) | |
| 250 | + | assert.Equal(t, map[string]string{ | |
| 251 | + | ".github/workflows/named.yml": "run push on branch by someone", | |
| 252 | + | ".github/workflows/plain.yml": ".github/workflows/plain.yml", | |
| 253 | + | }, names, "the run-name, else the workflow name") | |
| 254 | + | assert.Equal(t, map[string]string{ | |
| 255 | + | ".github/workflows/named.yml": "Named", | |
| 256 | + | ".github/workflows/plain.yml": ".github/workflows/plain.yml", | |
| 257 | + | }, workflows, "workflow_name is unchanged by a run-name") | |
| 245 | 258 | } | |
| 246 | 259 | ||
| 247 | 260 | // An over-256 matrix is an invalid workflow, like on github.com | |
| @@ -577,6 +590,35 @@ func TestCIThreeTagsInOnePush(t *testing.T) { | |||
| 577 | 590 | } | |
| 578 | 591 | } | |
| 579 | 592 | ||
| 593 | + | // A tag run without run-name is titled with the name of the workflow registered | |
| 594 | + | // for its path: the "name:" of the file on the default branch, which can differ | |
| 595 | + | // from the file at the tag (recorded: push-tag, push-tags-limit, push-delete-tag | |
| 596 | + | // and skip-directive-tag, whose ci.yml is "push-branch-create" on the default | |
| 597 | + | // branch of the recording repository and has the scenario's own name at the | |
| 598 | + | // tag; and the files with a unique name, where both agree). Its name stays the | |
| 599 | + | // workflow name of the tagged file. | |
| 600 | + | func TestCITagTitleIsTheNameRegisteredOnTheDefaultBranch(t *testing.T) { | |
| 601 | + | c := newCIHarness(t, nil) | |
| 602 | + | runner := c.registerRunner("alpha", "ubuntu-latest") | |
| 603 | + | wf := func(name string) map[string]string { | |
| 604 | + | return map[string]string{".github/workflows/ci.yml": "name: " + name + "\non:\n push:\n tags: ['v*']\njobs:\n j:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"} | |
| 605 | + | } | |
| 606 | + | c.push("main", wf("registered"), "") | |
| 607 | + | // The tagged commit is off every branch and renames the workflow. | |
| 608 | + | c.mustGit(c.wt, "checkout", "-q", "--detach") | |
| 609 | + | c.writeFile(c.wt, ".github/workflows/ci.yml", []byte(wf("renamed")[".github/workflows/ci.yml"])) | |
| 610 | + | _, title := c.tagEvent(runner, "v1", 1) | |
| 611 | + | assert.Equal(t, "registered", title, "the name of ci.yml on the default branch") | |
| 612 | + | r := c.runs()[0] | |
| 613 | + | assert.Equal(t, "renamed", r.GetWorkflowName(), "the workflow of the run is the file at the tag (github.workflow)") | |
| 614 | + | assert.Equal(t, "renamed", r.GetName(), "and so is the name of the run, which has no run-name") | |
| 615 | + | ||
| 616 | + | // A file the default branch does not have: the run's own workflow name. | |
| 617 | + | c.writeFile(c.wt, ".github/workflows/other.yml", []byte("name: other\non:\n push:\n tags: ['w*']\njobs:\n j:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n")) | |
| 618 | + | _, title = c.tagEvent(runner, "w1", 2) | |
| 619 | + | assert.Equal(t, "other", title) | |
| 620 | + | } | |
| 621 | + | ||
| 580 | 622 | // A tag run with a run-name keeps the evaluated run-name as its title (the | |
| 581 | 623 | // run-name of a tag push is not recorded; branch pushes are, ctx-github-names). | |
| 582 | 624 | func TestCITagRunKeepsItsRunName(t *testing.T) { | |
| @@ -220,7 +220,10 @@ func jsonNumber(v any) string { | |||||
| 220 | return string(b) | 220 | return string(b) | ||
| 221 | } | 221 | } | ||
| 222 | 222 | ||||
| 223 | - | // The run title is the evaluated run-name, else the head commit message. | 223 | + | // The run title is the evaluated run-name, else the head commit message. The |
| 224 | + | // name of the run is the evaluated run-name too (recorded: ctx-github-names, | |||
| 225 | + | // name = title = the run-name), else the workflow name; github.workflow stays | |||
| 226 | + | // the workflow name. | |||
| 224 | func TestCIRunTitle(t *testing.T) { | 227 | func TestCIRunTitle(t *testing.T) { | ||
| 225 | c := newCIHarness(t, nil) | 228 | c := newCIHarness(t, nil) | ||
| 226 | c.registerRunner("alpha", "ubuntu-latest") | 229 | c.registerRunner("alpha", "ubuntu-latest") | ||
| @@ -234,14 +237,24 @@ jobs: | |||||
| 234 | - run: echo hi | 237 | - run: echo hi | ||
| 235 | `, ".github/workflows/plain.yml": workflowWith("branches: [main]")}, "the head commit\n\nbody") | 238 | `, ".github/workflows/plain.yml": workflowWith("branches: [main]")}, "the head commit\n\nbody") | ||
| 236 | c.waitFor("two runs", func() bool { return len(c.runs()) == 2 }) | 239 | c.waitFor("two runs", func() bool { return len(c.runs()) == 2 }) | ||
| 237 | - | titles := map[string]string{} | 240 | + | titles, names, workflows := map[string]string{}, map[string]string{}, map[string]string{} |
| 238 | for _, r := range c.runs() { | 241 | for _, r := range c.runs() { | ||
| 239 | titles[r.GetWorkflowPath()] = r.GetTitle() | 242 | titles[r.GetWorkflowPath()] = r.GetTitle() | ||
| 243 | + | names[r.GetWorkflowPath()] = r.GetName() | |||
| 244 | + | workflows[r.GetWorkflowPath()] = r.GetWorkflowName() | |||
| 240 | } | 245 | } | ||
| 241 | assert.Equal(t, map[string]string{ | 246 | assert.Equal(t, map[string]string{ | ||
| 242 | ".github/workflows/named.yml": "run push on branch by someone", | 247 | ".github/workflows/named.yml": "run push on branch by someone", | ||
| 243 | ".github/workflows/plain.yml": "the head commit", | 248 | ".github/workflows/plain.yml": "the head commit", | ||
| 244 | }, titles) | 249 | }, titles) | ||
| 250 | + | assert.Equal(t, map[string]string{ | |||
| 251 | + | ".github/workflows/named.yml": "run push on branch by someone", | |||
| 252 | + | ".github/workflows/plain.yml": ".github/workflows/plain.yml", | |||
| 253 | + | }, names, "the run-name, else the workflow name") | |||
| 254 | + | assert.Equal(t, map[string]string{ | |||
| 255 | + | ".github/workflows/named.yml": "Named", | |||
| 256 | + | ".github/workflows/plain.yml": ".github/workflows/plain.yml", | |||
| 257 | + | }, workflows, "workflow_name is unchanged by a run-name") | |||
| 245 | } | 258 | } | ||
| 246 | 259 | ||||
| 247 | // An over-256 matrix is an invalid workflow, like on github.com | 260 | // An over-256 matrix is an invalid workflow, like on github.com | ||
| @@ -577,6 +590,35 @@ func TestCIThreeTagsInOnePush(t *testing.T) { | |||||
| 577 | } | 590 | } | ||
| 578 | } | 591 | } | ||
| 579 | 592 | ||||
| 593 | + | // A tag run without run-name is titled with the name of the workflow registered | |||
| 594 | + | // for its path: the "name:" of the file on the default branch, which can differ | |||
| 595 | + | // from the file at the tag (recorded: push-tag, push-tags-limit, push-delete-tag | |||
| 596 | + | // and skip-directive-tag, whose ci.yml is "push-branch-create" on the default | |||
| 597 | + | // branch of the recording repository and has the scenario's own name at the | |||
| 598 | + | // tag; and the files with a unique name, where both agree). Its name stays the | |||
| 599 | + | // workflow name of the tagged file. | |||
| 600 | + | func TestCITagTitleIsTheNameRegisteredOnTheDefaultBranch(t *testing.T) { | |||
| 601 | + | c := newCIHarness(t, nil) | |||
| 602 | + | runner := c.registerRunner("alpha", "ubuntu-latest") | |||
| 603 | + | wf := func(name string) map[string]string { | |||
| 604 | + | return map[string]string{".github/workflows/ci.yml": "name: " + name + "\non:\n push:\n tags: ['v*']\njobs:\n j:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n"} | |||
| 605 | + | } | |||
| 606 | + | c.push("main", wf("registered"), "") | |||
| 607 | + | // The tagged commit is off every branch and renames the workflow. | |||
| 608 | + | c.mustGit(c.wt, "checkout", "-q", "--detach") | |||
| 609 | + | c.writeFile(c.wt, ".github/workflows/ci.yml", []byte(wf("renamed")[".github/workflows/ci.yml"])) | |||
| 610 | + | _, title := c.tagEvent(runner, "v1", 1) | |||
| 611 | + | assert.Equal(t, "registered", title, "the name of ci.yml on the default branch") | |||
| 612 | + | r := c.runs()[0] | |||
| 613 | + | assert.Equal(t, "renamed", r.GetWorkflowName(), "the workflow of the run is the file at the tag (github.workflow)") | |||
| 614 | + | assert.Equal(t, "renamed", r.GetName(), "and so is the name of the run, which has no run-name") | |||
| 615 | + | ||||
| 616 | + | // A file the default branch does not have: the run's own workflow name. | |||
| 617 | + | c.writeFile(c.wt, ".github/workflows/other.yml", []byte("name: other\non:\n push:\n tags: ['w*']\njobs:\n j:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n")) | |||
| 618 | + | _, title = c.tagEvent(runner, "w1", 2) | |||
| 619 | + | assert.Equal(t, "other", title) | |||
| 620 | + | } | |||
| 621 | + | ||||
| 580 | // A tag run with a run-name keeps the evaluated run-name as its title (the | 622 | // A tag run with a run-name keeps the evaluated run-name as its title (the | ||
| 581 | // run-name of a tag push is not recorded; branch pushes are, ctx-github-names). | 623 | // run-name of a tag push is not recorded; branch pushes are, ctx-github-names). | ||
| 582 | func TestCITagRunKeepsItsRunName(t *testing.T) { | 624 | func TestCITagRunKeepsItsRunName(t *testing.T) { | ||
M internal/app/cli_run.go +3 -2
| @@ -24,6 +24,7 @@ type jsonRun struct { | |||
| 24 | 24 | Number int64 `json:"number"` | |
| 25 | 25 | Attempt int32 `json:"attempt"` | |
| 26 | 26 | Workflow string `json:"workflow"` | |
| 27 | + | Name string `json:"name"` | |
| 27 | 28 | WorkflowPath string `json:"workflow_path"` | |
| 28 | 29 | Title string `json:"title"` | |
| 29 | 30 | Event string `json:"event"` | |
| @@ -73,7 +74,7 @@ type jsonRunDetail struct { | |||
| 73 | 74 | } | |
| 74 | 75 | ||
| 75 | 76 | const jsonRunHelp = ` | |
| 76 | - | JSON fields of a run (--json): id, repo, number, attempt, workflow, workflow_path, title, event, | |
| 77 | + | JSON fields of a run (--json): id, repo, number, attempt, workflow, name (the name of the run: its run-name, else the workflow), workflow_path, title, event, | |
| 77 | 78 | ref, sha, actor, status (queued, in_progress, completed), conclusion (success, failure, cancelled, | |
| 78 | 79 | skipped, timed_out, neutral, action_required; empty until completed), error, created_at, | |
| 79 | 80 | started_at and completed_at (RFC 3339 UTC, null when unset). "run view" adds latest_attempt and | |
| @@ -113,7 +114,7 @@ func runResult(status ermite.CiStatus, c ermite.CiConclusion) string { | |||
| 113 | 114 | func toJSONRun(r *ermite.Run) jsonRun { | |
| 114 | 115 | return jsonRun{ | |
| 115 | 116 | ID: r.GetId(), Repo: r.GetRepo(), Number: r.GetNumber(), Attempt: r.GetAttempt(), Workflow: r.GetWorkflowName(), | |
| 116 | - | WorkflowPath: r.GetWorkflowPath(), Title: r.GetTitle(), Event: r.GetEvent(), Ref: r.GetRef(), Sha: r.GetSha(), | |
| 117 | + | Name: r.GetName(), WorkflowPath: r.GetWorkflowPath(), Title: r.GetTitle(), Event: r.GetEvent(), Ref: r.GetRef(), Sha: r.GetSha(), | |
| 117 | 118 | Actor: r.GetActor(), Status: statusText(r.GetStatus()), Conclusion: conclusionText(r.GetConclusion()), Error: r.GetError(), | |
| 118 | 119 | CreatedAt: jsonTime(r.GetCreatedAt()), StartedAt: jsonTime(r.GetStartedAt()), CompletedAt: jsonTime(r.GetCompletedAt()), | |
| 119 | 120 | } | |
| @@ -24,6 +24,7 @@ type jsonRun struct { | |||||
| 24 | Number int64 `json:"number"` | 24 | Number int64 `json:"number"` | ||
| 25 | Attempt int32 `json:"attempt"` | 25 | Attempt int32 `json:"attempt"` | ||
| 26 | Workflow string `json:"workflow"` | 26 | Workflow string `json:"workflow"` | ||
| 27 | + | Name string `json:"name"` | |||
| 27 | WorkflowPath string `json:"workflow_path"` | 28 | WorkflowPath string `json:"workflow_path"` | ||
| 28 | Title string `json:"title"` | 29 | Title string `json:"title"` | ||
| 29 | Event string `json:"event"` | 30 | Event string `json:"event"` | ||
| @@ -73,7 +74,7 @@ type jsonRunDetail struct { | |||||
| 73 | } | 74 | } | ||
| 74 | 75 | ||||
| 75 | const jsonRunHelp = ` | 76 | const jsonRunHelp = ` | ||
| 76 | - | JSON fields of a run (--json): id, repo, number, attempt, workflow, workflow_path, title, event, | 77 | + | JSON fields of a run (--json): id, repo, number, attempt, workflow, name (the name of the run: its run-name, else the workflow), workflow_path, title, event, |
| 77 | ref, sha, actor, status (queued, in_progress, completed), conclusion (success, failure, cancelled, | 78 | ref, sha, actor, status (queued, in_progress, completed), conclusion (success, failure, cancelled, | ||
| 78 | skipped, timed_out, neutral, action_required; empty until completed), error, created_at, | 79 | skipped, timed_out, neutral, action_required; empty until completed), error, created_at, | ||
| 79 | started_at and completed_at (RFC 3339 UTC, null when unset). "run view" adds latest_attempt and | 80 | started_at and completed_at (RFC 3339 UTC, null when unset). "run view" adds latest_attempt and | ||
| @@ -113,7 +114,7 @@ func runResult(status ermite.CiStatus, c ermite.CiConclusion) string { | |||||
| 113 | func toJSONRun(r *ermite.Run) jsonRun { | 114 | func toJSONRun(r *ermite.Run) jsonRun { | ||
| 114 | return jsonRun{ | 115 | return jsonRun{ | ||
| 115 | ID: r.GetId(), Repo: r.GetRepo(), Number: r.GetNumber(), Attempt: r.GetAttempt(), Workflow: r.GetWorkflowName(), | 116 | ID: r.GetId(), Repo: r.GetRepo(), Number: r.GetNumber(), Attempt: r.GetAttempt(), Workflow: r.GetWorkflowName(), | ||
| 116 | - | WorkflowPath: r.GetWorkflowPath(), Title: r.GetTitle(), Event: r.GetEvent(), Ref: r.GetRef(), Sha: r.GetSha(), | 117 | + | Name: r.GetName(), WorkflowPath: r.GetWorkflowPath(), Title: r.GetTitle(), Event: r.GetEvent(), Ref: r.GetRef(), Sha: r.GetSha(), |
| 117 | Actor: r.GetActor(), Status: statusText(r.GetStatus()), Conclusion: conclusionText(r.GetConclusion()), Error: r.GetError(), | 118 | Actor: r.GetActor(), Status: statusText(r.GetStatus()), Conclusion: conclusionText(r.GetConclusion()), Error: r.GetError(), | ||
| 118 | CreatedAt: jsonTime(r.GetCreatedAt()), StartedAt: jsonTime(r.GetStartedAt()), CompletedAt: jsonTime(r.GetCompletedAt()), | 119 | CreatedAt: jsonTime(r.GetCreatedAt()), StartedAt: jsonTime(r.GetStartedAt()), CompletedAt: jsonTime(r.GetCompletedAt()), | ||
| 119 | } | 120 | } | ||
M internal/app/cli_run_test.go +1 -1
| @@ -44,7 +44,7 @@ func TestCLIRunsAndRunners(t *testing.T) { | |||
| 44 | 44 | assert.Equal(t, "failure", runs[0].Conclusion) | |
| 45 | 45 | assert.Equal(t, "completed", runs[0].Status) | |
| 46 | 46 | assert.Equal(t, "refs/heads/main", runs[0].Ref) | |
| 47 | - | assert.Equal(t, []string{"actor", "attempt", "completed_at", "conclusion", "created_at", "error", "event", "id", "number", "ref", "repo", "sha", "started_at", "status", "title", "workflow", "workflow_path"}, keys(t, strings.TrimSpace(strings.Trim(strings.TrimSpace(js.stdout), "[]")))) | |
| 47 | + | assert.Equal(t, []string{"actor", "attempt", "completed_at", "conclusion", "created_at", "error", "event", "id", "name", "number", "ref", "repo", "sha", "started_at", "status", "title", "workflow", "workflow_path"}, keys(t, strings.TrimSpace(strings.Trim(strings.TrimSpace(js.stdout), "[]")))) | |
| 48 | 48 | ||
| 49 | 49 | none := h.cli(t, adminToken, "run", "list", "proj", "--status", "queued") | |
| 50 | 50 | require.NoError(t, none.err) | |
| @@ -44,7 +44,7 @@ func TestCLIRunsAndRunners(t *testing.T) { | |||||
| 44 | assert.Equal(t, "failure", runs[0].Conclusion) | 44 | assert.Equal(t, "failure", runs[0].Conclusion) | ||
| 45 | assert.Equal(t, "completed", runs[0].Status) | 45 | assert.Equal(t, "completed", runs[0].Status) | ||
| 46 | assert.Equal(t, "refs/heads/main", runs[0].Ref) | 46 | assert.Equal(t, "refs/heads/main", runs[0].Ref) | ||
| 47 | - | assert.Equal(t, []string{"actor", "attempt", "completed_at", "conclusion", "created_at", "error", "event", "id", "number", "ref", "repo", "sha", "started_at", "status", "title", "workflow", "workflow_path"}, keys(t, strings.TrimSpace(strings.Trim(strings.TrimSpace(js.stdout), "[]")))) | 47 | + | assert.Equal(t, []string{"actor", "attempt", "completed_at", "conclusion", "created_at", "error", "event", "id", "name", "number", "ref", "repo", "sha", "started_at", "status", "title", "workflow", "workflow_path"}, keys(t, strings.TrimSpace(strings.Trim(strings.TrimSpace(js.stdout), "[]")))) |
| 48 | 48 | ||||
| 49 | none := h.cli(t, adminToken, "run", "list", "proj", "--status", "queued") | 49 | none := h.cli(t, adminToken, "run", "list", "proj", "--status", "queued") | ||
| 50 | require.NoError(t, none.err) | 50 | require.NoError(t, none.err) | ||
M internal/domain/ci.go +10 -6
| @@ -155,12 +155,16 @@ func CILostMessage(runner string) string { | |||
| 155 | 155 | ||
| 156 | 156 | // CIRun is one execution of one workflow file for one event. | |
| 157 | 157 | type CIRun struct { | |
| 158 | - | Repo RepoName | |
| 159 | - | ID int64 | |
| 160 | - | Number int64 | |
| 161 | - | Attempt int32 | |
| 162 | - | WorkflowPath string | |
| 163 | - | WorkflowName string | |
| 158 | + | Repo RepoName | |
| 159 | + | ID int64 | |
| 160 | + | Number int64 | |
| 161 | + | Attempt int32 | |
| 162 | + | WorkflowPath string | |
| 163 | + | WorkflowName string | |
| 164 | + | // Name is GitHub's name of the run: the evaluated run-name when the | |
| 165 | + | // workflow has one, else WorkflowName. WorkflowName stays the name of the | |
| 166 | + | // workflow (github.workflow, the checks). | |
| 167 | + | Name string | |
| 164 | 168 | Title string | |
| 165 | 169 | Event string | |
| 166 | 170 | Ref string | |
| @@ -155,12 +155,16 @@ func CILostMessage(runner string) string { | |||||
| 155 | 155 | ||||
| 156 | // CIRun is one execution of one workflow file for one event. | 156 | // CIRun is one execution of one workflow file for one event. | ||
| 157 | type CIRun struct { | 157 | type CIRun struct { | ||
| 158 | - | Repo RepoName | 158 | + | Repo RepoName |
| 159 | - | ID int64 | 159 | + | ID int64 |
| 160 | - | Number int64 | 160 | + | Number int64 |
| 161 | - | Attempt int32 | 161 | + | Attempt int32 |
| 162 | - | WorkflowPath string | 162 | + | WorkflowPath string |
| 163 | - | WorkflowName string | 163 | + | WorkflowName string |
| 164 | + | // Name is GitHub's name of the run: the evaluated run-name when the | |||
| 165 | + | // workflow has one, else WorkflowName. WorkflowName stays the name of the | |||
| 166 | + | // workflow (github.workflow, the checks). | |||
| 167 | + | Name string | |||
| 164 | Title string | 168 | Title string | ||
| 165 | Event string | 169 | Event string | ||
| 166 | Ref string | 170 | Ref string | ||
M internal/usecase/ci_trigger.go +62 -25
| @@ -378,7 +378,7 @@ func (c *CI) startInvalid(ctx context.Context, repo domain.Repo, t ciTrigger, f | |||
| 378 | 378 | now := c.now() | |
| 379 | 379 | draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft { | |
| 380 | 380 | run := domain.CIRun{ | |
| 381 | - | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, | |
| 381 | + | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, Name: name, | |
| 382 | 382 | Title: firstLine(f.head.Message), Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor, | |
| 383 | 383 | Status: domain.CICompleted, Conclusion: domain.CIFailure, Error: cutText(problem, maxRunError), | |
| 384 | 384 | CreatedAt: now, StartedAt: now, CompletedAt: now, | |
| @@ -412,26 +412,29 @@ func (c *CI) publishRun(ctx context.Context, repo domain.Repo, run domain.CIRun) | |||
| 412 | 412 | func (c *CI) startRun(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, wfPath, yaml string, wf *workflow.Workflow) error { | |
| 413 | 413 | now := c.now() | |
| 414 | 414 | name := workflowName(wf, wfPath) | |
| 415 | - | // The title of a run without run-name is the commit message for a branch; | |
| 416 | - | // for a tag GitHub shows the workflow name, not the commit message | |
| 417 | - | // (recorded: filter-tags, filter-paths-tag, filter-branches-ignore, where | |
| 418 | - | // every file has its own name). The four scenarios whose file is ci.yml | |
| 419 | - | // (push-tag, push-tags-limit, push-delete-tag, skip-directive-tag) recorded | |
| 420 | - | // "push-branch-create", which is the name of another scenario's ci.yml and | |
| 421 | - | // not that of the workflow of the run: a leftover on github.com's side that | |
| 422 | - | // no rule produces, so those four titles cannot match. | |
| 415 | + | // The title of a run without run-name is the commit message for a branch. | |
| 416 | + | // For a tag GitHub shows the name of the workflow registered for the path, | |
| 417 | + | // not the commit message (recorded: filter-tags, filter-paths-tag, | |
| 418 | + | // filter-branches-ignore, and push-tag, push-tags-limit, push-delete-tag, | |
| 419 | + | // skip-directive-tag, whose ci.yml is called "push-branch-create" on the | |
| 420 | + | // default branch of the recording repository although each run's own | |
| 421 | + | // workflow has another name: 14 of 14 tag runs). Registered is read as the | |
| 422 | + | // "name:" of the file on the default branch; the first version GitHub saw | |
| 423 | + | // is the same file in the recording and cannot be told apart. | |
| 423 | 424 | title := firstLine(f.head.Message) | |
| 424 | 425 | if f.isTag { | |
| 425 | - | title = name | |
| 426 | + | title = c.registeredName(ctx, repo.Name, wfPath, name) | |
| 426 | 427 | } | |
| 427 | 428 | draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft { | |
| 428 | 429 | run := domain.CIRun{ | |
| 429 | - | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, | |
| 430 | + | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, Name: name, | |
| 430 | 431 | Title: title, Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor, | |
| 431 | 432 | Status: domain.CIQueued, CreatedAt: now, | |
| 432 | 433 | } | |
| 433 | 434 | githubJSON := c.githubJSON(ctx, repo, run, f) | |
| 434 | - | run.Title = c.runTitle(ctx, wf, githubJSON, run.Title) | |
| 435 | + | if runName, ok := c.evalRunName(ctx, wf, githubJSON); ok { | |
| 436 | + | run.Name, run.Title = runName, runName | |
| 437 | + | } | |
| 435 | 438 | d := ports.CIRunDraft{Run: run, Source: domain.CIRunSource{ | |
| 436 | 439 | Repo: repo.Name, WorkflowYAML: yaml, GithubJSON: githubJSON, | |
| 437 | 440 | }} | |
| @@ -472,19 +475,53 @@ func workflowName(wf *workflow.Workflow, path string) string { | |||
| 472 | 475 | return wf.Name | |
| 473 | 476 | } | |
| 474 | 477 | ||
| 475 | - | // runTitle is the title of a run, GitHub's display_title: the run-name | |
| 476 | - | // evaluated with the github, inputs and vars contexts (the contexts the | |
| 477 | - | // schema allows there), else commitTitle, which the caller sets to the first | |
| 478 | - | // line of the head commit message (the workflow name for a tag). A run-name | |
| 479 | - | // that does not evaluate, or that evaluates to nothing, falls back to | |
| 480 | - | // commitTitle, and the failure is logged. | |
| 481 | - | func (c *CI) runTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, commitTitle string) string { | |
| 478 | + | // registeredName is the name GitHub shows for a tag run without run-name: the | |
| 479 | + | // name of the workflow registered for the path, here the "name:" of the file on | |
| 480 | + | // the default branch (the path when that file has none). It is own, the name of | |
| 481 | + | // the workflow of the run, when the default branch has no such file, the file | |
| 482 | + | // does not parse, or anything cannot be read. | |
| 483 | + | func (c *CI) registeredName(ctx context.Context, repo domain.RepoName, wfPath, own string) string { | |
| 484 | + | def, exists, err := c.cfg.Repos.DefaultBranch(ctx, repo) | |
| 485 | + | if err != nil || !exists { | |
| 486 | + | return own | |
| 487 | + | } | |
| 488 | + | tip, err := c.cfg.Git.BranchHead(ctx, repo, def) | |
| 489 | + | if err != nil { | |
| 490 | + | return own | |
| 491 | + | } | |
| 492 | + | tp, err := domain.ParseTreePath(wfPath) | |
| 493 | + | if err != nil { | |
| 494 | + | return own | |
| 495 | + | } | |
| 496 | + | _, rc, err := c.cfg.Reader.OpenBlob(ctx, repo, domain.RevisionOf(tip), tp) | |
| 497 | + | if err != nil { | |
| 498 | + | return own | |
| 499 | + | } | |
| 500 | + | content, err := io.ReadAll(io.LimitReader(rc, maxCIWorkflowBytes+1)) | |
| 501 | + | _ = rc.Close() | |
| 502 | + | if err != nil || len(content) > maxCIWorkflowBytes { | |
| 503 | + | return own | |
| 504 | + | } | |
| 505 | + | wf, errs := workflow.Parse(wfPath, content) | |
| 506 | + | if len(errs) > 0 { | |
| 507 | + | return own | |
| 508 | + | } | |
| 509 | + | return workflowName(wf, wfPath) | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | // evalRunName evaluates the run-name of a workflow with the github, inputs and | |
| 513 | + | // vars contexts (the contexts the schema allows there). ok is false when the | |
| 514 | + | // workflow has none, when it does not evaluate (logged), or when it evaluates | |
| 515 | + | // to nothing: the run then has no name of its own, GitHub's display title falls | |
| 516 | + | // back to the commit message (a branch) or the registered name (a tag), and | |
| 517 | + | // the name of the run to the name of the workflow. | |
| 518 | + | func (c *CI) evalRunName(ctx context.Context, wf *workflow.Workflow, githubJSON string) (string, bool) { | |
| 482 | 519 | if wf.RunName == nil { | |
| 483 | - | return commitTitle | |
| 520 | + | return "", false | |
| 484 | 521 | } | |
| 485 | 522 | gh, err := expr.FromJSON([]byte(githubJSON)) | |
| 486 | 523 | if err != nil { | |
| 487 | - | return commitTitle | |
| 524 | + | return "", false | |
| 488 | 525 | } | |
| 489 | 526 | env := template.Env{Contexts: map[string]expr.Value{ | |
| 490 | 527 | "github": gh, "inputs": expr.NewDictionary(), "vars": expr.NewDictionary(), | |
| @@ -492,13 +529,13 @@ func (c *CI) runTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, co | |||
| 492 | 529 | name, err := template.EvaluateString(ctx, wf.RunName, env) | |
| 493 | 530 | if err != nil { | |
| 494 | 531 | log := zerowrap.FromCtx(ctx) | |
| 495 | - | log.Warn().Err(err).Msg("ci run-name does not evaluate: the run is titled with the commit message") | |
| 496 | - | return commitTitle | |
| 532 | + | log.Warn().Err(err).Msg("ci run-name does not evaluate: the run keeps the workflow name and the default title") | |
| 533 | + | return "", false | |
| 497 | 534 | } | |
| 498 | 535 | if name = firstLine(name); name == "" { | |
| 499 | - | return commitTitle | |
| 536 | + | return "", false | |
| 500 | 537 | } | |
| 501 | - | return name | |
| 538 | + | return name, true | |
| 502 | 539 | } | |
| 503 | 540 | ||
| 504 | 541 | // matrixProblem expands the strategy of every job of the file, whatever its | |
| @@ -378,7 +378,7 @@ func (c *CI) startInvalid(ctx context.Context, repo domain.Repo, t ciTrigger, f | |||||
| 378 | now := c.now() | 378 | now := c.now() | ||
| 379 | draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft { | 379 | draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft { | ||
| 380 | run := domain.CIRun{ | 380 | run := domain.CIRun{ | ||
| 381 | - | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, | 381 | + | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, Name: name, |
| 382 | Title: firstLine(f.head.Message), Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor, | 382 | Title: firstLine(f.head.Message), Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor, | ||
| 383 | Status: domain.CICompleted, Conclusion: domain.CIFailure, Error: cutText(problem, maxRunError), | 383 | Status: domain.CICompleted, Conclusion: domain.CIFailure, Error: cutText(problem, maxRunError), | ||
| 384 | CreatedAt: now, StartedAt: now, CompletedAt: now, | 384 | CreatedAt: now, StartedAt: now, CompletedAt: now, | ||
| @@ -412,26 +412,29 @@ func (c *CI) publishRun(ctx context.Context, repo domain.Repo, run domain.CIRun) | |||||
| 412 | func (c *CI) startRun(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, wfPath, yaml string, wf *workflow.Workflow) error { | 412 | func (c *CI) startRun(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, wfPath, yaml string, wf *workflow.Workflow) error { | ||
| 413 | now := c.now() | 413 | now := c.now() | ||
| 414 | name := workflowName(wf, wfPath) | 414 | name := workflowName(wf, wfPath) | ||
| 415 | - | // The title of a run without run-name is the commit message for a branch; | 415 | + | // The title of a run without run-name is the commit message for a branch. |
| 416 | - | // for a tag GitHub shows the workflow name, not the commit message | 416 | + | // For a tag GitHub shows the name of the workflow registered for the path, |
| 417 | - | // (recorded: filter-tags, filter-paths-tag, filter-branches-ignore, where | 417 | + | // not the commit message (recorded: filter-tags, filter-paths-tag, |
| 418 | - | // every file has its own name). The four scenarios whose file is ci.yml | 418 | + | // filter-branches-ignore, and push-tag, push-tags-limit, push-delete-tag, |
| 419 | - | // (push-tag, push-tags-limit, push-delete-tag, skip-directive-tag) recorded | 419 | + | // skip-directive-tag, whose ci.yml is called "push-branch-create" on the |
| 420 | - | // "push-branch-create", which is the name of another scenario's ci.yml and | 420 | + | // default branch of the recording repository although each run's own |
| 421 | - | // not that of the workflow of the run: a leftover on github.com's side that | 421 | + | // workflow has another name: 14 of 14 tag runs). Registered is read as the |
| 422 | - | // no rule produces, so those four titles cannot match. | 422 | + | // "name:" of the file on the default branch; the first version GitHub saw |
| 423 | + | // is the same file in the recording and cannot be told apart. | |||
| 423 | title := firstLine(f.head.Message) | 424 | title := firstLine(f.head.Message) | ||
| 424 | if f.isTag { | 425 | if f.isTag { | ||
| 425 | - | title = name | 426 | + | title = c.registeredName(ctx, repo.Name, wfPath, name) |
| 426 | } | 427 | } | ||
| 427 | draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft { | 428 | draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft { | ||
| 428 | run := domain.CIRun{ | 429 | run := domain.CIRun{ | ||
| 429 | - | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, | 430 | + | Repo: repo.Name, ID: id, Number: number, Attempt: 1, WorkflowPath: wfPath, WorkflowName: name, Name: name, |
| 430 | Title: title, Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor, | 431 | Title: title, Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor, | ||
| 431 | Status: domain.CIQueued, CreatedAt: now, | 432 | Status: domain.CIQueued, CreatedAt: now, | ||
| 432 | } | 433 | } | ||
| 433 | githubJSON := c.githubJSON(ctx, repo, run, f) | 434 | githubJSON := c.githubJSON(ctx, repo, run, f) | ||
| 434 | - | run.Title = c.runTitle(ctx, wf, githubJSON, run.Title) | 435 | + | if runName, ok := c.evalRunName(ctx, wf, githubJSON); ok { |
| 436 | + | run.Name, run.Title = runName, runName | |||
| 437 | + | } | |||
| 435 | d := ports.CIRunDraft{Run: run, Source: domain.CIRunSource{ | 438 | d := ports.CIRunDraft{Run: run, Source: domain.CIRunSource{ | ||
| 436 | Repo: repo.Name, WorkflowYAML: yaml, GithubJSON: githubJSON, | 439 | Repo: repo.Name, WorkflowYAML: yaml, GithubJSON: githubJSON, | ||
| 437 | }} | 440 | }} | ||
| @@ -472,19 +475,53 @@ func workflowName(wf *workflow.Workflow, path string) string { | |||||
| 472 | return wf.Name | 475 | return wf.Name | ||
| 473 | } | 476 | } | ||
| 474 | 477 | ||||
| 475 | - | // runTitle is the title of a run, GitHub's display_title: the run-name | 478 | + | // registeredName is the name GitHub shows for a tag run without run-name: the |
| 476 | - | // evaluated with the github, inputs and vars contexts (the contexts the | 479 | + | // name of the workflow registered for the path, here the "name:" of the file on |
| 477 | - | // schema allows there), else commitTitle, which the caller sets to the first | 480 | + | // the default branch (the path when that file has none). It is own, the name of |
| 478 | - | // line of the head commit message (the workflow name for a tag). A run-name | 481 | + | // the workflow of the run, when the default branch has no such file, the file |
| 479 | - | // that does not evaluate, or that evaluates to nothing, falls back to | 482 | + | // does not parse, or anything cannot be read. |
| 480 | - | // commitTitle, and the failure is logged. | 483 | + | func (c *CI) registeredName(ctx context.Context, repo domain.RepoName, wfPath, own string) string { |
| 481 | - | func (c *CI) runTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, commitTitle string) string { | 484 | + | def, exists, err := c.cfg.Repos.DefaultBranch(ctx, repo) |
| 485 | + | if err != nil || !exists { | |||
| 486 | + | return own | |||
| 487 | + | } | |||
| 488 | + | tip, err := c.cfg.Git.BranchHead(ctx, repo, def) | |||
| 489 | + | if err != nil { | |||
| 490 | + | return own | |||
| 491 | + | } | |||
| 492 | + | tp, err := domain.ParseTreePath(wfPath) | |||
| 493 | + | if err != nil { | |||
| 494 | + | return own | |||
| 495 | + | } | |||
| 496 | + | _, rc, err := c.cfg.Reader.OpenBlob(ctx, repo, domain.RevisionOf(tip), tp) | |||
| 497 | + | if err != nil { | |||
| 498 | + | return own | |||
| 499 | + | } | |||
| 500 | + | content, err := io.ReadAll(io.LimitReader(rc, maxCIWorkflowBytes+1)) | |||
| 501 | + | _ = rc.Close() | |||
| 502 | + | if err != nil || len(content) > maxCIWorkflowBytes { | |||
| 503 | + | return own | |||
| 504 | + | } | |||
| 505 | + | wf, errs := workflow.Parse(wfPath, content) | |||
| 506 | + | if len(errs) > 0 { | |||
| 507 | + | return own | |||
| 508 | + | } | |||
| 509 | + | return workflowName(wf, wfPath) | |||
| 510 | + | } | |||
| 511 | + | ||||
| 512 | + | // evalRunName evaluates the run-name of a workflow with the github, inputs and | |||
| 513 | + | // vars contexts (the contexts the schema allows there). ok is false when the | |||
| 514 | + | // workflow has none, when it does not evaluate (logged), or when it evaluates | |||
| 515 | + | // to nothing: the run then has no name of its own, GitHub's display title falls | |||
| 516 | + | // back to the commit message (a branch) or the registered name (a tag), and | |||
| 517 | + | // the name of the run to the name of the workflow. | |||
| 518 | + | func (c *CI) evalRunName(ctx context.Context, wf *workflow.Workflow, githubJSON string) (string, bool) { | |||
| 482 | if wf.RunName == nil { | 519 | if wf.RunName == nil { | ||
| 483 | - | return commitTitle | 520 | + | return "", false |
| 484 | } | 521 | } | ||
| 485 | gh, err := expr.FromJSON([]byte(githubJSON)) | 522 | gh, err := expr.FromJSON([]byte(githubJSON)) | ||
| 486 | if err != nil { | 523 | if err != nil { | ||
| 487 | - | return commitTitle | 524 | + | return "", false |
| 488 | } | 525 | } | ||
| 489 | env := template.Env{Contexts: map[string]expr.Value{ | 526 | env := template.Env{Contexts: map[string]expr.Value{ | ||
| 490 | "github": gh, "inputs": expr.NewDictionary(), "vars": expr.NewDictionary(), | 527 | "github": gh, "inputs": expr.NewDictionary(), "vars": expr.NewDictionary(), | ||
| @@ -492,13 +529,13 @@ func (c *CI) runTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, co | |||||
| 492 | name, err := template.EvaluateString(ctx, wf.RunName, env) | 529 | name, err := template.EvaluateString(ctx, wf.RunName, env) | ||
| 493 | if err != nil { | 530 | if err != nil { | ||
| 494 | log := zerowrap.FromCtx(ctx) | 531 | log := zerowrap.FromCtx(ctx) | ||
| 495 | - | log.Warn().Err(err).Msg("ci run-name does not evaluate: the run is titled with the commit message") | 532 | + | log.Warn().Err(err).Msg("ci run-name does not evaluate: the run keeps the workflow name and the default title") |
| 496 | - | return commitTitle | 533 | + | return "", false |
| 497 | } | 534 | } | ||
| 498 | if name = firstLine(name); name == "" { | 535 | if name = firstLine(name); name == "" { | ||
| 499 | - | return commitTitle | 536 | + | return "", false |
| 500 | } | 537 | } | ||
| 501 | - | return name | 538 | + | return name, true |
| 502 | } | 539 | } | ||
| 503 | 540 | ||||
| 504 | // matrixProblem expands the strategy of every job of the file, whatever its | 541 | // matrixProblem expands the strategy of every job of the file, whatever its | ||
M internal/usecase/export_test.go +6 -2
| @@ -15,9 +15,13 @@ func AuthorOf(p domain.Principal) string { return authorOf(p) } | |||
| 15 | 15 | // Sweep exposes the daily sweep to the tests. | |
| 16 | 16 | func (c *CI) Sweep(ctx context.Context) { c.sweep(ctx) } | |
| 17 | 17 | ||
| 18 | - | // RunTitle exposes runTitle to the tests. | |
| 18 | + | // RunTitle gives the title of a run as startRun sets it: the evaluated | |
| 19 | + | // run-name, else commitTitle. | |
| 19 | 20 | func (c *CI) RunTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, commitTitle string) string { | |
| 20 | - | return c.runTitle(ctx, wf, githubJSON, commitTitle) | |
| 21 | + | if name, ok := c.evalRunName(ctx, wf, githubJSON); ok { | |
| 22 | + | return name | |
| 23 | + | } | |
| 24 | + | return commitTitle | |
| 21 | 25 | } | |
| 22 | 26 | ||
| 23 | 27 | // CheckRunID exposes checkRunID to the tests. | |
| @@ -15,9 +15,13 @@ func AuthorOf(p domain.Principal) string { return authorOf(p) } | |||||
| 15 | // Sweep exposes the daily sweep to the tests. | 15 | // Sweep exposes the daily sweep to the tests. | ||
| 16 | func (c *CI) Sweep(ctx context.Context) { c.sweep(ctx) } | 16 | func (c *CI) Sweep(ctx context.Context) { c.sweep(ctx) } | ||
| 17 | 17 | ||||
| 18 | - | // RunTitle exposes runTitle to the tests. | 18 | + | // RunTitle gives the title of a run as startRun sets it: the evaluated |
| 19 | + | // run-name, else commitTitle. | |||
| 19 | func (c *CI) RunTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, commitTitle string) string { | 20 | func (c *CI) RunTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, commitTitle string) string { | ||
| 20 | - | return c.runTitle(ctx, wf, githubJSON, commitTitle) | 21 | + | if name, ok := c.evalRunName(ctx, wf, githubJSON); ok { |
| 22 | + | return name | |||
| 23 | + | } | |||
| 24 | + | return commitTitle | |||
| 21 | } | 25 | } | ||
| 22 | 26 | ||||
| 23 | // CheckRunID exposes checkRunID to the tests. | 27 | // CheckRunID exposes checkRunID to the tests. | ||
M proto/ermite/storage/ci.proto +4 -0
| @@ -95,6 +95,10 @@ message CIRun { | |||
| 95 | 95 | google.protobuf.Timestamp completed_at = 17; | |
| 96 | 96 | bool cancel_requested = 20; | |
| 97 | 97 | google.protobuf.Timestamp cancel_requested_at = 21; | |
| 98 | + | // name is GitHub's name of the run: the evaluated run-name, else the | |
| 99 | + | // workflow name. Empty in runs stored before it existed (read as | |
| 100 | + | // workflow_name). | |
| 101 | + | string name = 22; | |
| 98 | 102 | } | |
| 99 | 103 | ||
| 100 | 104 | // CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned | |
| @@ -95,6 +95,10 @@ message CIRun { | |||||
| 95 | google.protobuf.Timestamp completed_at = 17; | 95 | google.protobuf.Timestamp completed_at = 17; | ||
| 96 | bool cancel_requested = 20; | 96 | bool cancel_requested = 20; | ||
| 97 | google.protobuf.Timestamp cancel_requested_at = 21; | 97 | google.protobuf.Timestamp cancel_requested_at = 21; | ||
| 98 | + | // name is GitHub's name of the run: the evaluated run-name, else the | |||
| 99 | + | // workflow name. Empty in runs stored before it existed (read as | |||
| 100 | + | // workflow_name). | |||
| 101 | + | string name = 22; | |||
| 98 | } | 102 | } | ||
| 99 | 103 | ||||
| 100 | // CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned | 104 | // CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned | ||