mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00: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:      --------- 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:
		
							
								
								
									
										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 | 
		Reference in New Issue
	
	Block a user