-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
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
Implement actions badge svgs #28102
Changes from 10 commits
f510354
dab670a
7805652
41d0169
d705e21
40ccec9
2319c96
7c52b0b
0139320
e7cc38b
772a59c
fba2a8e
2073d50
58ee396
2e41661
58c8fb7
78fda75
9c271a6
b58ec24
3f4deef
e631800
eae95d3
c1c897b
9336d62
c5612a6
148fa0f
aea7e85
cc97325
4e61919
76e3516
e1cf32e
f1dfad8
815dee8
deda732
93e0eeb
bab5721
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} | ||
lng2020 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it right to do so (guessing font size)? The SVG uses There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
lng2020 marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{{$ := .Badge}} | ||
lng2020 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<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> |
Uh oh!
There was an error while loading. Please reload this page.