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>
This commit is contained in:
parent
e9f4c2db82
commit
db545b208b
37
docs/content/usage/badge.en-us.md
Normal file
37
docs/content/usage/badge.en-us.md
Normal file
|
@ -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.
|
|
@ -339,6 +339,23 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
|
||||||
return run, nil
|
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.
|
// UpdateRun updates a run.
|
||||||
// It requires the inputted run has Version set.
|
// 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).
|
// It will return error if the version is not matched (it means the run has been changed after loaded).
|
||||||
|
|
104
modules/badge/badge.go
Normal file
104
modules/badge/badge.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
56
routers/web/repo/actions/badge.go
Normal file
56
routers/web/repo/actions/badge.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -1371,6 +1371,9 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
|
m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
|
||||||
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
||||||
})
|
})
|
||||||
|
m.Group("/workflows/{workflow_name}", func() {
|
||||||
|
m.Get("/badge.svg", actions.GetWorkflowBadge)
|
||||||
|
})
|
||||||
}, reqRepoActionsReader, actions.MustEnableActions)
|
}, reqRepoActionsReader, actions.MustEnableActions)
|
||||||
|
|
||||||
m.Group("/wiki", func() {
|
m.Group("/wiki", func() {
|
||||||
|
|
25
templates/shared/actions/runner_badge.tmpl
Normal file
25
templates/shared/actions/runner_badge.tmpl
Normal file
|
@ -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>
|
After Width: | Height: | Size: 1.7 KiB |
Loading…
Reference in New Issue
Block a user