mirror of
https://github.com/go-gitea/gitea
synced 2024-11-24 11:12:45 +01:00
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
6 changed files with 242 additions and 0 deletions
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
|
||||
}
|
||||
|
||||
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).
|
||||
|
|
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.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
|
||||
})
|
||||
m.Group("/workflows/{workflow_name}", func() {
|
||||
m.Get("/badge.svg", actions.GetWorkflowBadge)
|
||||
})
|
||||
}, reqRepoActionsReader, actions.MustEnableActions)
|
||||
|
||||
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 a new issue