aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNanguan Lin <nanguanlin6@gmail.com>2024-02-28 01:56:18 +0800
committerGitHub <noreply@github.com>2024-02-27 18:56:18 +0100
commitdb545b208b4bd3d1961c519da66ee2b4421afa5c (patch)
treec63ec0ac9895b40a16b36cae9b246e973f849f4d
parente9f4c2db8291c54044345aebd9381ac820ed9687 (diff)
downloadgitea-db545b208b4bd3d1961c519da66ee2b4421afa5c.tar.gz
gitea-db545b208b4bd3d1961c519da66ee2b4421afa5c.zip
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 <xiaolunwen@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io> Co-authored-by: delvh <dev.lh@web.de>
-rw-r--r--docs/content/usage/badge.en-us.md37
-rw-r--r--models/actions/run.go17
-rw-r--r--modules/badge/badge.go104
-rw-r--r--routers/web/repo/actions/badge.go56
-rw-r--r--routers/web/web.go3
-rw-r--r--templates/shared/actions/runner_badge.tmpl25
6 files changed, 242 insertions, 0 deletions
diff --git a/docs/content/usage/badge.en-us.md b/docs/content/usage/badge.en-us.md
new file mode 100644
index 0000000000..212134e01c
--- /dev/null
+++ b/docs/content/usage/badge.en-us.md
@@ -0,0 +1,37 @@
+---
+date: "2023-02-25T00:00:00+00:00"
+title: "Badge"
+slug: "badge"
+sidebar_position: 11
+toc: false
+draft: false
+aliases:
+ - /en-us/badge
+menu:
+ sidebar:
+ parent: "usage"
+ name: "Badge"
+ sidebar_position: 11
+ identifier: "Badge"
+---
+
+# Badge
+
+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:
+
+## Workflow Badge
+
+The Gitea Actions workflow badge is a badge that shows the status of the latest workflow run.
+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).
+
+You can use the following URL to get the badge:
+
+```
+https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
+```
+
+- `{owner}`: The owner of the repository.
+- `{repo}`: The name of the repository.
+- `{workflow_file}`: The name of the workflow file.
+- `{branch}`: Optional. The branch of the workflow. Default to your repository's default branch.
+- `{event}`: Optional. The event of the workflow. Default to none.
diff --git a/models/actions/run.go b/models/actions/run.go
index fcac58d515..7b3125949b 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -339,6 +339,23 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
return run, nil
}
+func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) {
+ var run ActionRun
+ q := db.GetEngine(ctx).Where("repo_id=?", repoID).
+ And("ref = ?", branch).
+ And("workflow_id = ?", workflowFile)
+ if event != "" {
+ q.And("event = ?", event)
+ }
+ has, err := q.Desc("id").Get(&run)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile)
+ }
+ return &run, nil
+}
+
// UpdateRun updates a run.
// It requires the inputted run has Version set.
// It will return error if the version is not matched (it means the run has been changed after loaded).
diff --git a/modules/badge/badge.go b/modules/badge/badge.go
new file mode 100644
index 0000000000..b30d0b4729
--- /dev/null
+++ b/modules/badge/badge.go
@@ -0,0 +1,104 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package badge
+
+import (
+ actions_model "code.gitea.io/gitea/models/actions"
+)
+
+// The Badge layout: |offset|label|message|
+// We use 10x scale to calculate more precisely
+// Then scale down to normal size in tmpl file
+
+type Label struct {
+ text string
+ width int
+}
+
+func (l Label) Text() string {
+ return l.text
+}
+
+func (l Label) Width() int {
+ return l.width
+}
+
+func (l Label) TextLength() int {
+ return int(float64(l.width-defaultOffset) * 9.5)
+}
+
+func (l Label) X() int {
+ return l.width*5 + 10
+}
+
+type Message struct {
+ text string
+ width int
+ x int
+}
+
+func (m Message) Text() string {
+ return m.text
+}
+
+func (m Message) Width() int {
+ return m.width
+}
+
+func (m Message) X() int {
+ return m.x
+}
+
+func (m Message) TextLength() int {
+ return int(float64(m.width-defaultOffset) * 9.5)
+}
+
+type Badge struct {
+ Color string
+ FontSize int
+ Label Label
+ Message Message
+}
+
+func (b Badge) Width() int {
+ return b.Label.width + b.Message.width
+}
+
+const (
+ defaultOffset = 9
+ defaultFontSize = 11
+ DefaultColor = "#9f9f9f" // Grey
+ defaultFontWidth = 7 // approximate speculation
+)
+
+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
+}
+
+// GenerateBadge generates badge with given template
+func GenerateBadge(label, message, color string) Badge {
+ lw := defaultFontWidth*len(label) + defaultOffset
+ mw := defaultFontWidth*len(message) + defaultOffset
+ x := lw*10 + mw*5 - 10
+ return Badge{
+ Label: Label{
+ text: label,
+ width: lw,
+ },
+ Message: Message{
+ text: message,
+ width: mw,
+ x: x,
+ },
+ FontSize: defaultFontSize * 10,
+ Color: color,
+ }
+}
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
new file mode 100644
index 0000000000..6fa951826c
--- /dev/null
+++ b/routers/web/repo/actions/badge.go
@@ -0,0 +1,56 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/badge"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+)
+
+func GetWorkflowBadge(ctx *context.Context) {
+ workflowFile := ctx.Params("workflow_name")
+ branch := ctx.Req.URL.Query().Get("branch")
+ if branch == "" {
+ branch = ctx.Repo.Repository.DefaultBranch
+ }
+ branchRef := fmt.Sprintf("refs/heads/%s", branch)
+ event := ctx.Req.URL.Query().Get("event")
+
+ badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+ if err != nil {
+ ctx.ServerError("GetWorkflowBadge", err)
+ return
+ }
+
+ ctx.Data["Badge"] = badge
+ ctx.RespHeader().Set("Content-Type", "image/svg+xml")
+ ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+}
+
+func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
+ extension := filepath.Ext(workflowFile)
+ workflowName := strings.TrimSuffix(workflowFile, extension)
+
+ run, err := actions_model.GetWorkflowLatestRun(ctx, ctx.Repo.Repository.ID, workflowFile, branchName, event)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ return badge.GenerateBadge(workflowName, "no status", badge.DefaultColor), nil
+ }
+ return badge.Badge{}, err
+ }
+
+ color, ok := badge.StatusColorMap[run.Status]
+ if !ok {
+ return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
+ }
+ return badge.GenerateBadge(workflowName, run.Status.String(), color), nil
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 452998703a..b6dd9500c8 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1371,6 +1371,9 @@ func registerRoutes(m *web.Route) {
m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
})
+ m.Group("/workflows/{workflow_name}", func() {
+ m.Get("/badge.svg", actions.GetWorkflowBadge)
+ })
}, reqRepoActionsReader, actions.MustEnableActions)
m.Group("/wiki", func() {
diff --git a/templates/shared/actions/runner_badge.tmpl b/templates/shared/actions/runner_badge.tmpl
new file mode 100644
index 0000000000..816e87e177
--- /dev/null
+++ b/templates/shared/actions/runner_badge.tmpl
@@ -0,0 +1,25 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="18"
+ role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}">
+ <title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title>
+ <linearGradient id="s" x2="0" y2="100%">
+ <stop offset="0" stop-color="#fff" stop-opacity=".7" />
+ <stop offset=".1" stop-color="#aaa" stop-opacity=".1" />
+ <stop offset=".9" stop-color="#000" stop-opacity=".3" />
+ <stop offset="1" stop-color="#000" stop-opacity=".5" />
+ </linearGradient>
+ <clipPath id="r">
+ <rect width="{{.Badge.Width}}" height="18" rx="4" fill="#fff" />
+ </clipPath>
+ <g clip-path="url(#r)">
+ <rect width="{{.Badge.Label.Width}}" height="18" fill="#555" />
+ <rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="18" fill="{{.Badge.Color}}" />
+ <rect width="{{.Badge.Width}}" height="18" fill="url(#s)" />
+ </g>
+ <g fill="#fff" text-anchor="middle" font-family="Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision"
+ font-size="{{.Badge.FontSize}}"><text aria-hidden="true" x="{{.Badge.Label.X}}" y="140" fill="#010101" fill-opacity=".3"
+ transform="scale(.1)" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text x="{{.Badge.Label.X}}" y="130"
+ transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text aria-hidden="true"
+ x="{{.Badge.Message.X}}" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)"
+ textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text><text x="{{.Badge.Message.X}}" y="130" transform="scale(.1)"
+ fill="#fff" textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text></g>
+</svg>