Skip to content

Commit db545b2

Browse files
lng2020lunnyGiteaBotdelvh
authored
Implement actions badge svgs (#28102)
replace #27187 close #23688 The badge has two parts: label(workflow name) and message(action status). 5 colors are provided with 7 statuses. Color mapping: ```go var statusColorMap = map[actions_model.Status]string{ actions_model.StatusSuccess: "#4c1", // Green actions_model.StatusSkipped: "#dfb317", // Yellow actions_model.StatusUnknown: "#97ca00", // Light Green actions_model.StatusFailure: "#e05d44", // Red actions_model.StatusCancelled: "#fe7d37", // Orange actions_model.StatusWaiting: "#dfb317", // Yellow actions_model.StatusRunning: "#dfb317", // Yellow actions_model.StatusBlocked: "#dfb317", // Yellow } ``` preview: ![1](https://github.com/go-gitea/gitea/assets/70063547/5465cbaf-23cd-4437-9848-2738c3cb8985) ![2](https://github.com/go-gitea/gitea/assets/70063547/ec393d26-c6e6-4d38-b72c-51f2494c5e71) ![3](https://github.com/go-gitea/gitea/assets/70063547/3edb4fdf-1b08-4a02-ab2a-6bdd7f532fb2) ![4](https://github.com/go-gitea/gitea/assets/70063547/8c189de2-2169-4251-b115-0e39a52f3df8) ![5](https://github.com/go-gitea/gitea/assets/70063547/3fe22c73-c2d7-4fec-9ea4-c501a1e4e3bd) --------- Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: Giteabot <[email protected]> Co-authored-by: delvh <[email protected]>
1 parent e9f4c2d commit db545b2

File tree

6 files changed

+242
-0
lines changed

6 files changed

+242
-0
lines changed

docs/content/usage/badge.en-us.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
date: "2023-02-25T00:00:00+00:00"
3+
title: "Badge"
4+
slug: "badge"
5+
sidebar_position: 11
6+
toc: false
7+
draft: false
8+
aliases:
9+
- /en-us/badge
10+
menu:
11+
sidebar:
12+
parent: "usage"
13+
name: "Badge"
14+
sidebar_position: 11
15+
identifier: "Badge"
16+
---
17+
18+
# Badge
19+
20+
Gitea has its builtin Badge system which allows you to display the status of your repository in other places. You can use the following badges:
21+
22+
## Workflow Badge
23+
24+
The Gitea Actions workflow badge is a badge that shows the status of the latest workflow run.
25+
It is designed to be compatible with [GitHub Actions workflow badge](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge).
26+
27+
You can use the following URL to get the badge:
28+
29+
```
30+
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
31+
```
32+
33+
- `{owner}`: The owner of the repository.
34+
- `{repo}`: The name of the repository.
35+
- `{workflow_file}`: The name of the workflow file.
36+
- `{branch}`: Optional. The branch of the workflow. Default to your repository's default branch.
37+
- `{event}`: Optional. The event of the workflow. Default to none.

models/actions/run.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,23 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
339339
return run, nil
340340
}
341341

342+
func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) {
343+
var run ActionRun
344+
q := db.GetEngine(ctx).Where("repo_id=?", repoID).
345+
And("ref = ?", branch).
346+
And("workflow_id = ?", workflowFile)
347+
if event != "" {
348+
q.And("event = ?", event)
349+
}
350+
has, err := q.Desc("id").Get(&run)
351+
if err != nil {
352+
return nil, err
353+
} else if !has {
354+
return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile)
355+
}
356+
return &run, nil
357+
}
358+
342359
// UpdateRun updates a run.
343360
// It requires the inputted run has Version set.
344361
// It will return error if the version is not matched (it means the run has been changed after loaded).

modules/badge/badge.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package badge
5+
6+
import (
7+
actions_model "code.gitea.io/gitea/models/actions"
8+
)
9+
10+
// The Badge layout: |offset|label|message|
11+
// We use 10x scale to calculate more precisely
12+
// Then scale down to normal size in tmpl file
13+
14+
type Label struct {
15+
text string
16+
width int
17+
}
18+
19+
func (l Label) Text() string {
20+
return l.text
21+
}
22+
23+
func (l Label) Width() int {
24+
return l.width
25+
}
26+
27+
func (l Label) TextLength() int {
28+
return int(float64(l.width-defaultOffset) * 9.5)
29+
}
30+
31+
func (l Label) X() int {
32+
return l.width*5 + 10
33+
}
34+
35+
type Message struct {
36+
text string
37+
width int
38+
x int
39+
}
40+
41+
func (m Message) Text() string {
42+
return m.text
43+
}
44+
45+
func (m Message) Width() int {
46+
return m.width
47+
}
48+
49+
func (m Message) X() int {
50+
return m.x
51+
}
52+
53+
func (m Message) TextLength() int {
54+
return int(float64(m.width-defaultOffset) * 9.5)
55+
}
56+
57+
type Badge struct {
58+
Color string
59+
FontSize int
60+
Label Label
61+
Message Message
62+
}
63+
64+
func (b Badge) Width() int {
65+
return b.Label.width + b.Message.width
66+
}
67+
68+
const (
69+
defaultOffset = 9
70+
defaultFontSize = 11
71+
DefaultColor = "#9f9f9f" // Grey
72+
defaultFontWidth = 7 // approximate speculation
73+
)
74+
75+
var StatusColorMap = map[actions_model.Status]string{
76+
actions_model.StatusSuccess: "#4c1", // Green
77+
actions_model.StatusSkipped: "#dfb317", // Yellow
78+
actions_model.StatusUnknown: "#97ca00", // Light Green
79+
actions_model.StatusFailure: "#e05d44", // Red
80+
actions_model.StatusCancelled: "#fe7d37", // Orange
81+
actions_model.StatusWaiting: "#dfb317", // Yellow
82+
actions_model.StatusRunning: "#dfb317", // Yellow
83+
actions_model.StatusBlocked: "#dfb317", // Yellow
84+
}
85+
86+
// GenerateBadge generates badge with given template
87+
func GenerateBadge(label, message, color string) Badge {
88+
lw := defaultFontWidth*len(label) + defaultOffset
89+
mw := defaultFontWidth*len(message) + defaultOffset
90+
x := lw*10 + mw*5 - 10
91+
return Badge{
92+
Label: Label{
93+
text: label,
94+
width: lw,
95+
},
96+
Message: Message{
97+
text: message,
98+
width: mw,
99+
x: x,
100+
},
101+
FontSize: defaultFontSize * 10,
102+
Color: color,
103+
}
104+
}

routers/web/repo/actions/badge.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"path/filepath"
11+
"strings"
12+
13+
actions_model "code.gitea.io/gitea/models/actions"
14+
"code.gitea.io/gitea/modules/badge"
15+
"code.gitea.io/gitea/modules/util"
16+
"code.gitea.io/gitea/services/context"
17+
)
18+
19+
func GetWorkflowBadge(ctx *context.Context) {
20+
workflowFile := ctx.Params("workflow_name")
21+
branch := ctx.Req.URL.Query().Get("branch")
22+
if branch == "" {
23+
branch = ctx.Repo.Repository.DefaultBranch
24+
}
25+
branchRef := fmt.Sprintf("refs/heads/%s", branch)
26+
event := ctx.Req.URL.Query().Get("event")
27+
28+
badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
29+
if err != nil {
30+
ctx.ServerError("GetWorkflowBadge", err)
31+
return
32+
}
33+
34+
ctx.Data["Badge"] = badge
35+
ctx.RespHeader().Set("Content-Type", "image/svg+xml")
36+
ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
37+
}
38+
39+
func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
40+
extension := filepath.Ext(workflowFile)
41+
workflowName := strings.TrimSuffix(workflowFile, extension)
42+
43+
run, err := actions_model.GetWorkflowLatestRun(ctx, ctx.Repo.Repository.ID, workflowFile, branchName, event)
44+
if err != nil {
45+
if errors.Is(err, util.ErrNotExist) {
46+
return badge.GenerateBadge(workflowName, "no status", badge.DefaultColor), nil
47+
}
48+
return badge.Badge{}, err
49+
}
50+
51+
color, ok := badge.StatusColorMap[run.Status]
52+
if !ok {
53+
return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
54+
}
55+
return badge.GenerateBadge(workflowName, run.Status.String(), color), nil
56+
}

routers/web/web.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,9 @@ func registerRoutes(m *web.Route) {
13711371
m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
13721372
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
13731373
})
1374+
m.Group("/workflows/{workflow_name}", func() {
1375+
m.Get("/badge.svg", actions.GetWorkflowBadge)
1376+
})
13741377
}, reqRepoActionsReader, actions.MustEnableActions)
13751378

13761379
m.Group("/wiki", func() {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="18"
2+
role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}">
3+
<title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title>
4+
<linearGradient id="s" x2="0" y2="100%">
5+
<stop offset="0" stop-color="#fff" stop-opacity=".7" />
6+
<stop offset=".1" stop-color="#aaa" stop-opacity=".1" />
7+
<stop offset=".9" stop-color="#000" stop-opacity=".3" />
8+
<stop offset="1" stop-color="#000" stop-opacity=".5" />
9+
</linearGradient>
10+
<clipPath id="r">
11+
<rect width="{{.Badge.Width}}" height="18" rx="4" fill="#fff" />
12+
</clipPath>
13+
<g clip-path="url(#r)">
14+
<rect width="{{.Badge.Label.Width}}" height="18" fill="#555" />
15+
<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="18" fill="{{.Badge.Color}}" />
16+
<rect width="{{.Badge.Width}}" height="18" fill="url(#s)" />
17+
</g>
18+
<g fill="#fff" text-anchor="middle" font-family="Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision"
19+
font-size="{{.Badge.FontSize}}"><text aria-hidden="true" x="{{.Badge.Label.X}}" y="140" fill="#010101" fill-opacity=".3"
20+
transform="scale(.1)" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text x="{{.Badge.Label.X}}" y="130"
21+
transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text aria-hidden="true"
22+
x="{{.Badge.Message.X}}" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)"
23+
textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text><text x="{{.Badge.Message.X}}" y="130" transform="scale(.1)"
24+
fill="#fff" textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text></g>
25+
</svg>

0 commit comments

Comments
 (0)