detached 20985cc

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +187 -45
M gen/ermite/storage/ci.pb.go +16 -4 generated file: collapsed
M internal/adapters/kvstore/ci.go +6 -2
@@ -268,7 +268,7 @@ func encodeRun(r domain.CIRun) ([]byte, error) {
268268 }
269269 return mustMarshal(&storagepb.CIRun{
270270 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,
272272 Event: r.Event, Ref: r.Ref, Sha: r.Sha, Actor: r.Actor,
273273 Status: encodeStatus(r.Status), Conclusion: encodeConclusion(r.Conclusion), Error: r.Error,
274274 CreatedAt: encodeTime(r.CreatedAt), StartedAt: encodeTime(r.StartedAt), CompletedAt: encodeTime(r.CompletedAt),
@@ -285,9 +285,13 @@ func decodeRun(item kv.Item) (domain.CIRun, error) {
285285 if err != nil {
286286 return domain.CIRun{}, fmt.Errorf("decode run: %w", err)
287287 }
288+ name := pb.GetName()
289+ if name == "" {
290+ name = pb.GetWorkflowName() // stored before the field existed
291+ }
288292 return domain.CIRun{
289293 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(),
291295 Event: pb.GetEvent(), Ref: pb.GetRef(), Sha: pb.GetSha(), Actor: pb.GetActor(),
292296 Status: decodeStatus(pb.GetStatus()), Conclusion: decodeConclusion(pb.GetConclusion()), Error: pb.GetError(),
293297 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) {
373373 assert.Equal(t, reason, got.CancelReason)
374374 }
375375 }
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 {
130130 return pb.CancelReason_CANCEL_REASON_UNSPECIFIED
131131 }
132132
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+
133142 func runToPB(r domain.CIRun) *pb.Run {
134143 return &pb.Run{
135144 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,
137146 Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, Status: statusToPB(r.Status), Conclusion: conclusionToPB(r.Conclusion),
138147 Error: r.Error, CreatedAt: timestamp(r.CreatedAt), StartedAt: timestamp(r.StartedAt), CompletedAt: timestamp(r.CompletedAt),
139148 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_UNSPECIFIED130 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^63148 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 {
220220 return string(b)
221221 }
222222
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.
224227 func TestCIRunTitle(t *testing.T) {
225228 c := newCIHarness(t, nil)
226229 c.registerRunner("alpha", "ubuntu-latest")
@@ -234,14 +237,24 @@ jobs:
234237 - run: echo hi
235238 `, ".github/workflows/plain.yml": workflowWith("branches: [main]")}, "the head commit\n\nbody")
236239 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{}
238241 for _, r := range c.runs() {
239242 titles[r.GetWorkflowPath()] = r.GetTitle()
243+ names[r.GetWorkflowPath()] = r.GetName()
244+ workflows[r.GetWorkflowPath()] = r.GetWorkflowName()
240245 }
241246 assert.Equal(t, map[string]string{
242247 ".github/workflows/named.yml": "run push on branch by someone",
243248 ".github/workflows/plain.yml": "the head commit",
244249 }, 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")
245258 }
246259
247260 // An over-256 matrix is an invalid workflow, like on github.com
@@ -577,6 +590,35 @@ func TestCIThreeTagsInOnePush(t *testing.T) {
577590 }
578591 }
579592
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+
580622 // A tag run with a run-name keeps the evaluated run-name as its title (the
581623 // run-name of a tag push is not recorded; branch pushes are, ctx-github-names).
582624 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 hi237 - 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.com260 // 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 (the622 // 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 {
2424 Number int64 `json:"number"`
2525 Attempt int32 `json:"attempt"`
2626 Workflow string `json:"workflow"`
27+ Name string `json:"name"`
2728 WorkflowPath string `json:"workflow_path"`
2829 Title string `json:"title"`
2930 Event string `json:"event"`
@@ -73,7 +74,7 @@ type jsonRunDetail struct {
7374 }
7475
7576 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,
7778 ref, sha, actor, status (queued, in_progress, completed), conclusion (success, failure, cancelled,
7879 skipped, timed_out, neutral, action_required; empty until completed), error, created_at,
7980 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 {
113114 func toJSONRun(r *ermite.Run) jsonRun {
114115 return jsonRun{
115116 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(),
117118 Actor: r.GetActor(), Status: statusText(r.GetStatus()), Conclusion: conclusionText(r.GetConclusion()), Error: r.GetError(),
118119 CreatedAt: jsonTime(r.GetCreatedAt()), StartedAt: jsonTime(r.GetStartedAt()), CompletedAt: jsonTime(r.GetCompletedAt()),
119120 }
@@ -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 and80 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) {
4444 assert.Equal(t, "failure", runs[0].Conclusion)
4545 assert.Equal(t, "completed", runs[0].Status)
4646 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), "[]"))))
4848
4949 none := h.cli(t, adminToken, "run", "list", "proj", "--status", "queued")
5050 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 {
155155
156156 // CIRun is one execution of one workflow file for one event.
157157 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
164168 Title string
165169 Event string
166170 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 RepoName158+ Repo RepoName
159- ID int64159+ ID int64
160- Number int64160+ Number int64
161- Attempt int32161+ Attempt int32
162- WorkflowPath string162+ WorkflowPath string
163- WorkflowName string163+ 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 string168 Title string
165 Event string169 Event string
166 Ref string170 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
378378 now := c.now()
379379 draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft {
380380 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,
382382 Title: firstLine(f.head.Message), Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor,
383383 Status: domain.CICompleted, Conclusion: domain.CIFailure, Error: cutText(problem, maxRunError),
384384 CreatedAt: now, StartedAt: now, CompletedAt: now,
@@ -412,26 +412,29 @@ func (c *CI) publishRun(ctx context.Context, repo domain.Repo, run domain.CIRun)
412412 func (c *CI) startRun(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, wfPath, yaml string, wf *workflow.Workflow) error {
413413 now := c.now()
414414 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.
423424 title := firstLine(f.head.Message)
424425 if f.isTag {
425- title = name
426+ title = c.registeredName(ctx, repo.Name, wfPath, name)
426427 }
427428 draft, err := c.cfg.Store.CreateRun(ctx, repo.Name, wfPath, func(id, number int64) ports.CIRunDraft {
428429 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,
430431 Title: title, Event: "push", Ref: f.update.Ref, Sha: f.commit.String(), Actor: t.actor,
431432 Status: domain.CIQueued, CreatedAt: now,
432433 }
433434 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+ }
435438 d := ports.CIRunDraft{Run: run, Source: domain.CIRunSource{
436439 Repo: repo.Name, WorkflowYAML: yaml, GithubJSON: githubJSON,
437440 }}
@@ -472,19 +475,53 @@ func workflowName(wf *workflow.Workflow, path string) string {
472475 return wf.Name
473476 }
474477
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) {
482519 if wf.RunName == nil {
483- return commitTitle
520+ return "", false
484521 }
485522 gh, err := expr.FromJSON([]byte(githubJSON))
486523 if err != nil {
487- return commitTitle
524+ return "", false
488525 }
489526 env := template.Env{Contexts: map[string]expr.Value{
490527 "github": gh, "inputs": expr.NewDictionary(), "vars": expr.NewDictionary(),
@@ -492,13 +529,13 @@ func (c *CI) runTitle(ctx context.Context, wf *workflow.Workflow, githubJSON, co
492529 name, err := template.EvaluateString(ctx, wf.RunName, env)
493530 if err != nil {
494531 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
497534 }
498535 if name = firstLine(name); name == "" {
499- return commitTitle
536+ return "", false
500537 }
501- return name
538+ return name, true
502539 }
503540
504541 // 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 message416+ // For a tag GitHub shows the name of the workflow registered for the path,
417- // (recorded: filter-tags, filter-paths-tag, filter-branches-ignore, where417+ // not the commit message (recorded: filter-tags, filter-paths-tag,
418- // every file has its own name). The four scenarios whose file is ci.yml418+ // filter-branches-ignore, and push-tag, push-tags-limit, push-delete-tag,
419- // (push-tag, push-tags-limit, push-delete-tag, skip-directive-tag) recorded419+ // 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 and420+ // 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 that421+ // 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 = name426+ 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.Name475 return wf.Name
473 }476 }
474 477
475-// runTitle is the title of a run, GitHub's display_title: the run-name478+// 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 the479+// 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 first480+// 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-name481+// 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 to482+// 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 commitTitle520+ 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 commitTitle524+ 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 commitTitle533+ return "", false
497 }534 }
498 if name = firstLine(name); name == "" {535 if name = firstLine(name); name == "" {
499- return commitTitle536+ return "", false
500 }537 }
501- return name538+ return name, true
502 }539 }
503 540
504 // matrixProblem expands the strategy of every job of the file, whatever its541 // 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) }
1515 // Sweep exposes the daily sweep to the tests.
1616 func (c *CI) Sweep(ctx context.Context) { c.sweep(ctx) }
1717
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.
1920 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
2125 }
2226
2327 // 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 {
9595 google.protobuf.Timestamp completed_at = 17;
9696 bool cancel_requested = 20;
9797 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;
98102 }
99103
100104 // 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 planned104 // CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned

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 41

↑↓ moveenter openesc close