mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-02-13 21:00:00 +00:00
add events query parameter to badge url (#5728)
Co-authored-by: damage <damage@devloop.de>
This commit is contained in:
@@ -15,4 +15,10 @@ The status badge displays the status for the latest build to your default branch
|
||||
+<scheme>://<hostname>/api/badges/<repo-id>/status.svg?branch=<branch>
|
||||
```
|
||||
|
||||
Please note status badges do not include pull request results, since the status of a pull request does not provide an accurate representation of your repository state.
|
||||
By default status badges do not include pull request results, since the status of a pull request does not provide an accurate representation of your repository state.
|
||||
If you'd like to respect other or further events, you can add the `events` query parameter, otherwise the badge represents only the state of the last push event:
|
||||
|
||||
```diff
|
||||
-<scheme>://<hostname>/api/badges/<repo-id>/status.svg
|
||||
+<scheme>://<hostname>/api/badges/<repo-id>/status.svg?events=manual,cron
|
||||
```
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -72,7 +73,27 @@ func GetBadge(c *gin.Context) {
|
||||
branch = repo.Branch
|
||||
}
|
||||
|
||||
pipeline, err := _store.GetPipelineBadge(repo, branch)
|
||||
// Events to lookup, multiple separated by comma
|
||||
var events []model.WebhookEvent
|
||||
eventsQuery := c.Query("events")
|
||||
// If none given, fallback to default "push"
|
||||
if len(eventsQuery) == 0 {
|
||||
events = []model.WebhookEvent{model.EventPush}
|
||||
} else {
|
||||
strEvents := strings.Split(eventsQuery, ",")
|
||||
events = make([]model.WebhookEvent, len(strEvents))
|
||||
for i, strEvent := range strEvents {
|
||||
event := model.WebhookEvent(strEvent)
|
||||
if err := event.Validate(); err == nil {
|
||||
events[i] = event
|
||||
} else {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipeline, err := _store.GetPipelineBadge(repo, branch, events)
|
||||
if err != nil {
|
||||
if !errors.Is(err, types.RecordNotExist) {
|
||||
log.Warn().Err(err).Msg("could not get last pipeline for badge")
|
||||
|
||||
@@ -35,11 +35,12 @@ func (s storage) GetPipelineNumber(repo *model.Repo, num int64) (*model.Pipeline
|
||||
).Get(pipeline))
|
||||
}
|
||||
|
||||
func (s storage) GetPipelineBadge(repo *model.Repo, branch string) (*model.Pipeline, error) {
|
||||
func (s storage) GetPipelineBadge(repo *model.Repo, branch string, events []model.WebhookEvent) (*model.Pipeline, error) {
|
||||
pipeline := new(model.Pipeline)
|
||||
return pipeline, wrapGet(s.engine.
|
||||
Desc("number").
|
||||
Where(builder.Eq{"repo_id": repo.ID, "branch": branch, "event": model.EventPush}).
|
||||
Where(builder.Eq{"repo_id": repo.ID, "branch": branch}).
|
||||
Where(builder.In("event", events)).
|
||||
Where(builder.Neq{"status": model.StatusBlocked}).
|
||||
Get(pipeline))
|
||||
}
|
||||
|
||||
@@ -1822,8 +1822,8 @@ func (_c *MockStore_GetPipeline_Call) RunAndReturn(run func(n int64) (*model.Pip
|
||||
}
|
||||
|
||||
// GetPipelineBadge provides a mock function for the type MockStore
|
||||
func (_mock *MockStore) GetPipelineBadge(repo *model.Repo, s string) (*model.Pipeline, error) {
|
||||
ret := _mock.Called(repo, s)
|
||||
func (_mock *MockStore) GetPipelineBadge(repo *model.Repo, s string, webhookEvents []model.WebhookEvent) (*model.Pipeline, error) {
|
||||
ret := _mock.Called(repo, s, webhookEvents)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPipelineBadge")
|
||||
@@ -1831,18 +1831,18 @@ func (_mock *MockStore) GetPipelineBadge(repo *model.Repo, s string) (*model.Pip
|
||||
|
||||
var r0 *model.Pipeline
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*model.Repo, string) (*model.Pipeline, error)); ok {
|
||||
return returnFunc(repo, s)
|
||||
if returnFunc, ok := ret.Get(0).(func(*model.Repo, string, []model.WebhookEvent) (*model.Pipeline, error)); ok {
|
||||
return returnFunc(repo, s, webhookEvents)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(*model.Repo, string) *model.Pipeline); ok {
|
||||
r0 = returnFunc(repo, s)
|
||||
if returnFunc, ok := ret.Get(0).(func(*model.Repo, string, []model.WebhookEvent) *model.Pipeline); ok {
|
||||
r0 = returnFunc(repo, s, webhookEvents)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Pipeline)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(*model.Repo, string) error); ok {
|
||||
r1 = returnFunc(repo, s)
|
||||
if returnFunc, ok := ret.Get(1).(func(*model.Repo, string, []model.WebhookEvent) error); ok {
|
||||
r1 = returnFunc(repo, s, webhookEvents)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -1857,11 +1857,12 @@ type MockStore_GetPipelineBadge_Call struct {
|
||||
// GetPipelineBadge is a helper method to define mock.On call
|
||||
// - repo *model.Repo
|
||||
// - s string
|
||||
func (_e *MockStore_Expecter) GetPipelineBadge(repo interface{}, s interface{}) *MockStore_GetPipelineBadge_Call {
|
||||
return &MockStore_GetPipelineBadge_Call{Call: _e.mock.On("GetPipelineBadge", repo, s)}
|
||||
// - webhookEvents []model.WebhookEvent
|
||||
func (_e *MockStore_Expecter) GetPipelineBadge(repo interface{}, s interface{}, webhookEvents interface{}) *MockStore_GetPipelineBadge_Call {
|
||||
return &MockStore_GetPipelineBadge_Call{Call: _e.mock.On("GetPipelineBadge", repo, s, webhookEvents)}
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetPipelineBadge_Call) Run(run func(repo *model.Repo, s string)) *MockStore_GetPipelineBadge_Call {
|
||||
func (_c *MockStore_GetPipelineBadge_Call) Run(run func(repo *model.Repo, s string, webhookEvents []model.WebhookEvent)) *MockStore_GetPipelineBadge_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 *model.Repo
|
||||
if args[0] != nil {
|
||||
@@ -1871,9 +1872,14 @@ func (_c *MockStore_GetPipelineBadge_Call) Run(run func(repo *model.Repo, s stri
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 []model.WebhookEvent
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].([]model.WebhookEvent)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
@@ -1884,7 +1890,7 @@ func (_c *MockStore_GetPipelineBadge_Call) Return(pipeline *model.Pipeline, err
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockStore_GetPipelineBadge_Call) RunAndReturn(run func(repo *model.Repo, s string) (*model.Pipeline, error)) *MockStore_GetPipelineBadge_Call {
|
||||
func (_c *MockStore_GetPipelineBadge_Call) RunAndReturn(run func(repo *model.Repo, s string, webhookEvents []model.WebhookEvent) (*model.Pipeline, error)) *MockStore_GetPipelineBadge_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ type Store interface {
|
||||
// GetPipelineNumber gets a pipeline by number.
|
||||
GetPipelineNumber(*model.Repo, int64) (*model.Pipeline, error)
|
||||
// GetPipelineBadge gets the last relevant pipeline for the badge.
|
||||
GetPipelineBadge(*model.Repo, string) (*model.Pipeline, error)
|
||||
GetPipelineBadge(*model.Repo, string, []model.WebhookEvent) (*model.Pipeline, error)
|
||||
// GetPipelineLast gets the last pipeline for the branch.
|
||||
GetPipelineLast(*model.Repo, string) (*model.Pipeline, error)
|
||||
// GetPipelineLastBefore gets the last pipeline before pipeline number N.
|
||||
|
||||
@@ -174,7 +174,8 @@
|
||||
"type_url": "URL",
|
||||
"type_markdown": "Markdown",
|
||||
"type_html": "HTML",
|
||||
"branch": "Branch"
|
||||
"branch": "Branch",
|
||||
"events": "Events"
|
||||
},
|
||||
"actions": {
|
||||
"actions": "Actions",
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
<input
|
||||
:id="`checkbox-${id}`"
|
||||
type="checkbox"
|
||||
class="checkbox border-wp-control-neutral-200 checked:border-wp-control-ok-200 checked:bg-wp-control-ok-200 focus-visible:border-wp-control-neutral-300 checked:focus-visible:border-wp-control-ok-300 relative h-5 w-5 shrink-0 cursor-pointer rounded-md border transition-colors duration-150"
|
||||
class="checkbox border-wp-control-neutral-200 disabled:border-wp-control-neutral-200 disabled:bg-wp-control-neutral-300 checked:border-wp-control-ok-200 checked:bg-wp-control-ok-200 focus-visible:border-wp-control-neutral-300 checked:focus-visible:border-wp-control-ok-300 relative h-5 w-5 shrink-0 cursor-pointer rounded-md border transition-colors duration-150"
|
||||
:checked="innerValue"
|
||||
:disabled="disabled || false"
|
||||
@click="innerValue = !innerValue"
|
||||
/>
|
||||
<div class="ml-4 flex flex-col">
|
||||
@@ -21,6 +22,7 @@ const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -65,6 +67,10 @@ const id = (Math.random() + 1).toString(36).substring(7);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.checkbox:disabled::before {
|
||||
border-color: var(--wp-text-alt-100);
|
||||
}
|
||||
|
||||
.checkbox:checked::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:key="option.value"
|
||||
:model-value="innerValue.includes(option.value)"
|
||||
:label="option.text"
|
||||
:disabled="disabled ? disabled(option) : false"
|
||||
:description="option.description"
|
||||
class="mb-2"
|
||||
@update:model-value="clickOption(option)"
|
||||
@@ -19,6 +20,7 @@ import type { CheckboxOption } from './form.types';
|
||||
const props = defineProps<{
|
||||
modelValue?: CheckboxOption['value'][];
|
||||
options?: CheckboxOption[];
|
||||
disabled?: (option: CheckboxOption) => boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: CheckboxOption['value'][]): void;
|
||||
|
||||
@@ -30,6 +30,15 @@
|
||||
<InputField v-slot="{ id }" :label="$t('repo.settings.badge.branch')">
|
||||
<SelectField :id="id" v-model="branch" :options="branches" required />
|
||||
</InputField>
|
||||
<InputField v-slot="{ id }" :label="$t('repo.settings.badge.events')">
|
||||
<CheckboxesField
|
||||
:id="id"
|
||||
v-model="events"
|
||||
:options="badgeEventsOptions"
|
||||
:disabled="isDisabled"
|
||||
@update:model-value="eventsChanged"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<div v-if="badgeContent" class="flex flex-col space-y-4">
|
||||
<div>
|
||||
@@ -44,7 +53,8 @@ import { useStorage } from '@vueuse/core';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import type { SelectOption } from '~/components/form/form.types';
|
||||
import CheckboxesField from '~/components/form/CheckboxesField.vue';
|
||||
import type { CheckboxOption, SelectOption } from '~/components/form/form.types';
|
||||
import InputField from '~/components/form/InputField.vue';
|
||||
import SelectField from '~/components/form/SelectField.vue';
|
||||
import Settings from '~/components/layout/Settings.vue';
|
||||
@@ -53,6 +63,7 @@ import useConfig from '~/compositions/useConfig';
|
||||
import { requiredInject } from '~/compositions/useInjectProvide';
|
||||
import { usePaginate } from '~/compositions/usePaginate';
|
||||
import { useWPTitle } from '~/compositions/useWPTitle';
|
||||
import { WebhookEvents } from '~/lib/api/types';
|
||||
|
||||
const apiClient = useApiClient();
|
||||
const repo = requiredInject('repo');
|
||||
@@ -62,6 +73,7 @@ const badgeType = useStorage('woodpecker:last-badge-type', 'markdown');
|
||||
const defaultBranch = computed(() => repo.value.default_branch);
|
||||
const branches = ref<SelectOption[]>([]);
|
||||
const branch = ref<string>('');
|
||||
const events = ref<string[]>([WebhookEvents.Push]);
|
||||
|
||||
async function loadBranches() {
|
||||
branches.value = (await usePaginate((page) => apiClient.getRepoBranches(repo.value.id, { page })))
|
||||
@@ -80,9 +92,22 @@ const baseUrl = `${window.location.protocol}//${window.location.hostname}${
|
||||
window.location.port ? `:${window.location.port}` : ''
|
||||
}`;
|
||||
const { rootPath } = useConfig();
|
||||
const badgeUrl = computed(
|
||||
() => `${rootPath}/api/badges/${repo.value.id}/status.svg${branch.value !== '' ? `?branch=${branch.value}` : ''}`,
|
||||
);
|
||||
const badgeUrl = computed(() => {
|
||||
const params = [];
|
||||
|
||||
if (branch.value !== '') {
|
||||
params.push(`branch=${encodeURIComponent(branch.value)}`);
|
||||
}
|
||||
|
||||
if (events.value.length > 0) {
|
||||
// dont set events parameters, if only WebhookEvents.Push is selected, as this is the default behaviour
|
||||
if (events.value.length !== 1 || events.value.at(0) !== WebhookEvents.Push) {
|
||||
params.push(`events=${encodeURIComponent(events.value.join(','))}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${rootPath}/api/badges/${repo.value.id}/status.svg${params.length > 0 ? `?${params.join('&')}` : ''}`;
|
||||
});
|
||||
const repoUrl = computed(
|
||||
() =>
|
||||
`${rootPath}/repos/${repo.value.id}${branch.value !== '' ? `/branches/${encodeURIComponent(branch.value)}` : ''}`,
|
||||
@@ -98,7 +123,7 @@ const badgeContent = computed(() => {
|
||||
}
|
||||
|
||||
if (badgeType.value === 'html') {
|
||||
return `<a href="${baseUrl}${repoUrl.value}" target="_blank">\n <img src="${baseUrl}${badgeUrl.value}" alt="status-badge" />\n</a>`;
|
||||
return `<a href="${baseUrl}${repoUrl.value}" target="_blank">\n <img src="${baseUrl}${badgeUrl.value.replace('&', '&')}" alt="status-badge" />\n</a>`;
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -113,5 +138,35 @@ watch(repo, () => {
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const badgeEventsOptions: CheckboxOption[] = [
|
||||
{ value: WebhookEvents.Push, text: t('repo.pipeline.event.push'), description: t('default') },
|
||||
{ value: WebhookEvents.Tag, text: t('repo.pipeline.event.tag') },
|
||||
{ value: WebhookEvents.Release, text: t('repo.pipeline.event.release') },
|
||||
{ value: WebhookEvents.PullRequest, text: t('repo.pipeline.event.pr') },
|
||||
{ value: WebhookEvents.PullRequestClosed, text: t('repo.pipeline.event.pr_closed') },
|
||||
{ value: WebhookEvents.PullRequestMetadata, text: t('repo.pipeline.event.pr_metadata') },
|
||||
{ value: WebhookEvents.Deploy, text: t('repo.pipeline.event.deploy') },
|
||||
{ value: WebhookEvents.Cron, text: t('repo.pipeline.event.cron') },
|
||||
{ value: WebhookEvents.Manual, text: t('repo.pipeline.event.manual') },
|
||||
];
|
||||
|
||||
useWPTitle(computed(() => [t('repo.settings.badge.badge'), repo.value.full_name]));
|
||||
|
||||
function eventsChanged() {
|
||||
if (events.value.length === 0) {
|
||||
events.value.push(WebhookEvents.Push);
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
return (option: CheckboxOption) => {
|
||||
if (events.value.length === 1 && events.value[0] === WebhookEvents.Push) {
|
||||
// disable Push checkbox if only Push is selected because it's the default
|
||||
return option.value === WebhookEvents.Push;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user