Skip to content

Implement actions badge svgs #28102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f510354
Provide actions badge svgs
lunny Sep 22, 2023
dab670a
draw badge
lng2020 Nov 16, 2023
7805652
change field
lng2020 Nov 17, 2023
41d0169
simpilfy code
lng2020 Nov 17, 2023
d705e21
lint
lng2020 Nov 17, 2023
40ccec9
delete file
lng2020 Nov 17, 2023
2319c96
apply camel case
lng2020 Nov 17, 2023
7c52b0b
fmt
lng2020 Nov 17, 2023
0139320
lint
lng2020 Nov 17, 2023
e7cc38b
lint
lng2020 Nov 17, 2023
772a59c
change sql
lng2020 Nov 24, 2023
fba2a8e
Merge remote-tracking branch 'upstream/main' into pr27187
lng2020 Feb 19, 2024
2073d50
Apply reviews
lng2020 Feb 21, 2024
58ee396
Merge remote-tracking branch 'upstream/main' into pr27187
lng2020 Feb 21, 2024
2e41661
improvement
lng2020 Feb 21, 2024
58c8fb7
fix lint
lng2020 Feb 21, 2024
78fda75
Merge remote-tracking branch 'upstream/main' into pr27187
lng2020 Feb 24, 2024
9c271a6
restructure file
lng2020 Feb 24, 2024
b58ec24
add doc
lng2020 Feb 24, 2024
3f4deef
Merge branch 'main' into pr27187
lng2020 Feb 24, 2024
e631800
lint
lng2020 Feb 25, 2024
eae95d3
lint md
lng2020 Feb 25, 2024
c1c897b
simplify font width
lng2020 Feb 25, 2024
9336d62
Merge branch 'main' into pr27187
lng2020 Feb 25, 2024
c5612a6
fix lint
lng2020 Feb 25, 2024
148fa0f
Merge branch 'main' into pr27187
GiteaBot Feb 27, 2024
aea7e85
Fix package import path in recent merged PR
lng2020 Feb 27, 2024
cc97325
Merge branch 'main' into pr27187
GiteaBot Feb 27, 2024
4e61919
Merge branch 'main' into pr27187
GiteaBot Feb 27, 2024
76e3516
Update docs/content/usage/badge.en-us.md
lng2020 Feb 27, 2024
e1cf32e
Merge remote-tracking branch 'upstream/main' into pr27187
lng2020 Feb 27, 2024
f1dfad8
Add comment to explain the badge layout
lng2020 Feb 27, 2024
815dee8
Update modules/badge/badge.go
lng2020 Feb 27, 2024
deda732
Merge branch 'main' into pr27187
GiteaBot Feb 27, 2024
93e0eeb
Merge branch 'main' into pr27187
GiteaBot Feb 27, 2024
bab5721
Merge branch 'main' into pr27187
GiteaBot Feb 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions models/actions/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,21 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error)
return run, nil
}

func GetRepoBranchLastRun(ctx context.Context, repoID int64, branch, workflowFile string) (*ActionRun, error) {
var run ActionRun
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).
In("ref", branch, ""). // cron job has no branch
And("workflow_id = ?", workflowFile).
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).
Expand Down
177 changes: 177 additions & 0 deletions routers/web/repo/actions/badge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package actions

import (
"errors"
"fmt"
"math"
"net/http"
"path/filepath"
"strconv"
"strings"

actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"

"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
)

type Badge struct {
Label string
Message string
Color string
Width int
LabelWidth int
MessageWidth int
LabelX int
MessageX int
LabelLength int
MessageLength int
MessageColor string
MessageShadow string
FontSize int
}

const (
defaultOffset = 9
defaultSpacing = 0
defaultFontSize = 11
)

var drawer = &font.Drawer{
Face: basicfont.Face7x13,
}

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
}

func GetDefaultBranchWorkflowBadge(ctx *context.Context) {
workflowFile := ctx.Params("workflow_name")
branchName := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch).String()
badge, err := getWorkflowBadge(ctx, workflowFile, branchName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound("Not found", fmt.Errorf("%s not found", workflowFile))
return
}
ctx.ServerError("GetWorkflowBadge", err)
return
}
ctx.Data["Badge"] = badge
ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
}

func GetWorkflowBadge(ctx *context.Context) {
workflowFile := ctx.Params("workflow_name")
branchName := ctx.Params("branch_name")
badge, err := getWorkflowBadge(ctx, workflowFile, branchName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound("Not found", fmt.Errorf("%s not found", workflowFile))
return
}
ctx.ServerError("GetWorkflowBadge", err)
return
}
ctx.Data["Badge"] = badge
ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
}

func getWorkflowBadge(ctx *context.Context, workflowFile, branchName string) (Badge, error) {
run, err := actions_model.GetRepoBranchLastRun(ctx, ctx.Repo.Repository.ID, branchName, workflowFile)
if err != nil {
return Badge{}, err
}

color, ok := statusColorMap[run.Status]
if !ok {
return Badge{}, fmt.Errorf("unknown status %d", run.Status)
}

extension := filepath.Ext(workflowFile)
workflowName := strings.TrimSuffix(workflowFile, extension)
return generateBadge(workflowName, run.Status.String(), color), nil
}

// utils for badge generation -------------------------------------

// generateBadge generates badge with given template
func generateBadge(label, message, color string) Badge {
gF := float64(defaultOffset)
lW := float64(drawer.MeasureString(label)>>6) + gF
mW := float64(drawer.MeasureString(message)>>6) + gF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it right to do so (guessing font size)?

The SVG uses font-family="Geneva,DejaVu Sans,sans-serif", do you really know its width on client side?

Copy link
Member

@silverwind silverwind Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is no way knowing how wide it would render on a client, but as long as these approximations work on common clients, I can accept them.

fW := lW + mW
lX := (lW/2 + 1) * 10
mX := (lW + (mW / 2) - 1) * 10
lL := (lW - gF) * (10.0 + defaultSpacing - 0.5)
mL := (mW - gF) * (10.0 + defaultSpacing - 0.5)
fS := defaultFontSize * 10

mC, mS := determineMessageColorsFromHex(color)

return Badge{
Label: label,
Message: message,
Color: color,
Width: int(fW),
LabelWidth: int(lW),
MessageWidth: int(mW),
LabelX: int(lX),
MessageX: int(mX),
LabelLength: int(lL),
MessageLength: int(mL),
MessageColor: mC,
MessageShadow: mS,
FontSize: fS,
}
}

// determineMessageColorsFromHex takes a hex color string and returns text and shadow colors
func determineMessageColorsFromHex(hexColor string) (string, string) {
hexColor = strings.TrimPrefix(hexColor, "#")
// Check for shorthand hex color
if len(hexColor) == 3 {
hexColor = strings.Repeat(string(hexColor[0]), 2) +
strings.Repeat(string(hexColor[1]), 2) +
strings.Repeat(string(hexColor[2]), 2)
}

badgeColor, _ := strconv.ParseInt(hexColor, 16, 32)

if badgeColor == 0 || computeLuminance(badgeColor) < 0.65 {
return "#fff", "#010101"
}

return "#333", "#ccc"
}

// computeLuminance calculates the relative luminance of a color
func computeLuminance(inputColor int64) float64 {
r := singleColorLuminance(float64(inputColor>>16&0xFF) / 255)
g := singleColorLuminance(float64(inputColor>>8&0xFF) / 255)
b := singleColorLuminance(float64(inputColor&0xFF) / 255)

return 0.2126*r + 0.7152*g + 0.0722*b
}

// singleColorLuminance calculates luminance for a single color
func singleColorLuminance(colorValue float64) float64 {
if colorValue <= 0.03928 {
return colorValue / 12.92
}

return math.Pow((colorValue+0.055)/1.055, 2.4)
}
4 changes: 4 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,10 @@ func registerRoutes(m *web.Route) {
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
})
m.Group("/workflows/{workflow_name}", func() {
m.Get("/badge.svg", actions.GetDefaultBranchWorkflowBadge)
m.Get("/{branch_name}/badge.svg", actions.GetWorkflowBadge)
})
}, reqRepoActionsReader, actions.MustEnableActions)

m.Group("/wiki", func() {
Expand Down
26 changes: 26 additions & 0 deletions templates/shared/actions/runner_badge.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{{$ := .Badge}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{$.Width}}" height="18"
role="img" aria-label="{{$.Label}}: {{$.Message}}">
<title>{$.Label}: {$.Message}</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="{{$.Width}}" height="18" rx="4" fill="#fff" />
</clipPath>
<g clip-path="url(#r)">
<rect width="{{$.LabelWidth}}" height="18" fill="#555" />
<rect x="{{$.LabelWidth}}" width="{{$.MessageWidth}}" height="18" fill="{{$.Color}}" />
<rect width="{{$.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="{{$.FontSize}}"><text aria-hidden="true" x="{{$.LabelX}}" y="140" fill="#010101" fill-opacity=".3"
transform="scale(.1)" textLength="{{$.LabelLength}}">{{$.Label}}</text><text x="{{$.LabelX}}" y="130"
transform="scale(.1)" fill="#fff" textLength="{{$.LabelLength}}">{{$.Label}}</text><text aria-hidden="true"
x="{{$.MessageX}}" y="140" fill="{{$.MessageShadow}}" fill-opacity=".3" transform="scale(.1)"
textLength="{{$.MessageLength}}">{{$.Message}}</text><text x="{{$.MessageX}}" y="130" transform="scale(.1)"
fill="{{$.MessageColor}}" textLength="{{$.MessageLength}}">{{$.Message}}</text></g>
</svg>