Allow to run cron manually (#1338)

Closes #1154
This commit is contained in:
qwerty287 2022-10-26 01:23:28 +02:00 committed by GitHub
parent ed44c3b50f
commit de4e62cfcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 76 additions and 16 deletions

View file

@ -23,6 +23,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server"
cronScheduler "github.com/woodpecker-ci/woodpecker/server/cron"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/pipeline"
"github.com/woodpecker-ci/woodpecker/server/router/middleware/session"
"github.com/woodpecker-ci/woodpecker/server/store"
)
@ -45,6 +46,37 @@ func GetCron(c *gin.Context) {
c.JSON(200, cron)
}
// RunCron starts a cron job now.
func RunCron(c *gin.Context) {
repo := session.Repo(c)
_store := store.FromContext(c)
id, err := strconv.ParseInt(c.Param("cron"), 10, 64)
if err != nil {
c.String(400, "Error parsing cron id. %s", err)
return
}
cron, err := _store.CronFind(repo, id)
if err != nil {
c.String(http.StatusNotFound, "Error getting cron %q. %s", id, err)
return
}
repo, newPipeline, err := cronScheduler.CreatePipeline(c, _store, server.Config.Services.Remote, cron)
if err != nil {
c.String(http.StatusInternalServerError, "Error creating pipeline for cron %q. %s", id, err)
return
}
pl, err := pipeline.Create(c, _store, repo, newPipeline)
if err != nil {
handlePipelineErr(c, err)
return
}
c.JSON(200, pl)
}
// PostCron persists the cron job to the database.
func PostCron(c *gin.Context) {
repo := session.Repo(c)

View file

@ -21,6 +21,7 @@ package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -28,6 +29,7 @@ import (
"time"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/store/types"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
@ -95,6 +97,10 @@ func GetPipelines(c *gin.Context) {
pipelines, err := store.FromContext(c).GetPipelineList(repo, page)
if err != nil {
if errors.Is(err, types.RecordNotExist) {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.AbortWithStatus(http.StatusInternalServerError)
return
}

View file

@ -96,16 +96,16 @@ func runCron(store store.Store, remote remote.Remote, cron *model.Cron, now time
return nil
}
repo, newBuild, err := createBuild(ctx, store, remote, cron)
repo, newPipeline, err := CreatePipeline(ctx, store, remote, cron)
if err != nil {
return err
}
_, err = pipeline.Create(ctx, store, repo, newBuild)
_, err = pipeline.Create(ctx, store, repo, newPipeline)
return err
}
func createBuild(ctx context.Context, store store.Store, remote remote.Remote, cron *model.Cron) (*model.Repo, *model.Pipeline, error) {
func CreatePipeline(ctx context.Context, store store.Store, remote remote.Remote, cron *model.Cron) (*model.Repo, *model.Pipeline, error) {
repo, err := store.GetRepo(cron.RepoID)
if err != nil {
return nil, nil, err

View file

@ -49,7 +49,7 @@ func TestCreateBuild(t *testing.T) {
store.On("GetUser", mock.Anything).Return(creator, nil)
remote.On("BranchHead", mock.Anything, creator, repo1, "default").Return("sha1", nil)
_, pipeline, err := createBuild(ctx, store, remote, &model.Cron{
_, pipeline, err := CreatePipeline(ctx, store, remote, &model.Cron{
Name: "test",
})
assert.NoError(t, err)

View file

@ -114,6 +114,7 @@ func apiRoutes(e *gin.Engine) {
repo.GET("/cron", session.MustPush, api.GetCronList)
repo.POST("/cron", session.MustPush, api.PostCron)
repo.GET("/cron/:cron", session.MustPush, api.GetCron)
repo.POST("/cron/:cron", session.MustPush, api.RunCron)
repo.PATCH("/cron/:cron", session.MustPush, api.PatchCron)
repo.DELETE("/cron/:cron", session.MustPush, api.DeleteCron)

1
web/components.d.ts vendored
View file

@ -32,6 +32,7 @@ declare module '@vue/runtime-core' {
IIcBaselineFileDownload: typeof import('~icons/ic/baseline-file-download')['default']
IIcBaselineFileDownloadOff: typeof import('~icons/ic/baseline-file-download-off')['default']
IIcBaselineHealing: typeof import('~icons/ic/baseline-healing')['default']
IIcBaselinePlayArrow: typeof import('~icons/ic/baseline-play-arrow')['default']
IIconoirArrowLeft: typeof import('~icons/iconoir/arrow-left')['default']
IIconParkOutlineAlarmClock: typeof import('~icons/icon-park-outline/alarm-clock')['default']
IIcRoundLightMode: typeof import('~icons/ic/round-light-mode')['default']

View file

@ -154,6 +154,7 @@
"deleted": "Cron deleted",
"next_exec": "Next execution",
"not_executed_yet": "Not executed yet",
"run": "Run now",
"branch": {
"title": "Branch",
"placeholder": "Branch (uses default branch if empty)"

View file

@ -41,6 +41,7 @@
<i-icon-park-outline-alarm-clock v-else-if="name === 'stopwatch'" class="h-6 w-6" />
<i-ic-baseline-file-download v-else-if="name === 'auto-scroll'" class="h-6 w-6" />
<i-ic-baseline-file-download-off v-else-if="name === 'auto-scroll-off'" class="h-6 w-6" />
<i-ic-baseline-play-arrow v-else-if="name === 'play'" class="h-6 w-6" />
<div v-else-if="name === 'blank'" class="h-6 w-6" />
</template>
@ -90,7 +91,8 @@ export type IconNames =
| 'stopwatch'
| 'download'
| 'auto-scroll'
| 'auto-scroll-off';
| 'auto-scroll-off'
| 'play';
export default defineComponent({
name: 'Icon',

View file

@ -31,12 +31,8 @@
{{ $t('repo.settings.crons.next_exec') }}: {{ date.toLocaleString(new Date(cron.next_exec * 1000)) }}</span
>
<span v-else class="ml-auto">{{ $t('repo.settings.crons.not_executed_yet') }}</span>
<IconButton
icon="edit"
class="ml-auto w-8 h-8"
:title="$t('repo.settings.crons.edit')"
@click="selectedCron = cron"
/>
<IconButton icon="play" class="ml-auto w-8 h-8" :title="$t('repo.settings.crons.run')" @click="runCron(cron)" />
<IconButton icon="edit" class="w-8 h-8" :title="$t('repo.settings.crons.edit')" @click="selectedCron = cron" />
<IconButton
icon="trash"
class="w-8 h-8 hover:text-red-400 hover:dark:text-red-500"
@ -105,6 +101,7 @@ import { useAsyncAction } from '~/compositions/useAsyncAction';
import { useDate } from '~/compositions/useDate';
import useNotifications from '~/compositions/useNotifications';
import { Cron, Repo } from '~/lib/api/types';
import router from '~/router';
const apiClient = useApiClient();
const notifications = useNotifications();
@ -156,6 +153,22 @@ const { doSubmit: deleteCron, isLoading: isDeleting } = useAsyncAction(async (_c
await loadCrons();
});
const { doSubmit: runCron } = useAsyncAction(async (_cron: Cron) => {
if (!repo?.value) {
throw new Error("Unexpected: Can't load repo");
}
const pipeline = await apiClient.runCron(repo.value.owner, repo.value.name, _cron.id);
await router.push({
name: 'repo-pipeline',
params: {
repoOwner: repo.value.owner,
repoName: repo.value.name,
pipelineId: pipeline.number,
},
});
});
onMounted(async () => {
await loadCrons();
});

View file

@ -166,16 +166,16 @@ export default defineComponent({
text: i18n.t('repo.settings.general.visibility.public.public'),
description: i18n.t('repo.settings.general.visibility.public.desc'),
},
{
value: RepoVisibility.Private,
text: i18n.t('repo.settings.general.visibility.private.private'),
description: i18n.t('repo.settings.general.visibility.private.desc'),
},
{
value: RepoVisibility.Internal,
text: i18n.t('repo.settings.general.visibility.internal.internal'),
description: i18n.t('repo.settings.general.visibility.internal.desc'),
},
{
value: RepoVisibility.Private,
text: i18n.t('repo.settings.general.visibility.private.private'),
description: i18n.t('repo.settings.general.visibility.private.desc'),
},
];
const cancelPreviousPipelineEventsOptions: CheckboxOption[] = [

View file

@ -162,6 +162,10 @@ export default class WoodpeckerClient extends ApiClient {
return this._delete(`/api/repos/${owner}/${repo}/cron/${cronId}`);
}
runCron(owner: string, repo: string, cronId: number): Promise<Pipeline> {
return this._post(`/api/repos/${owner}/${repo}/cron/${cronId}`) as Promise<Pipeline>;
}
getOrgPermissions(owner: string): Promise<OrgPermissions> {
return this._get(`/api/orgs/${owner}/permissions`) as Promise<OrgPermissions>;
}