diff --git a/docs/content/usage/badge.en-us.md b/docs/content/usage/badge.en-us.md new file mode 100644 index 00000000000..212134e01c1 --- /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 fcac58d515c..7b3125949b8 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 00000000000..b30d0b47294 --- /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 00000000000..6fa951826c3 --- /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 452998703a3..b6dd9500c86 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 00000000000..816e87e177a --- /dev/null +++ b/templates/shared/actions/runner_badge.tmpl @@ -0,0 +1,25 @@ + + {{.Badge.Label.Text}}: {{.Badge.Message.Text}} + + + + + + + + + + + + + + + {{.Badge.Label.Text}}{{.Badge.Message.Text}} +