From 4ef99810a9dead1435cf44a0ecc53b65e490467f Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Wed, 26 Oct 2022 01:23:28 +0200 Subject: [PATCH] Allow to run cron manually (#1338) Closes #1154 --- server/api/cron.go | 32 +++++++++++++++++++ server/api/pipeline.go | 6 ++++ server/cron/cron.go | 6 ++-- server/cron/cron_test.go | 2 +- server/router/api.go | 1 + web/components.d.ts | 1 + web/src/assets/locales/en.json | 1 + web/src/components/atomic/Icon.vue | 4 ++- web/src/components/repo/settings/CronTab.vue | 25 +++++++++++---- .../components/repo/settings/GeneralTab.vue | 10 +++--- web/src/lib/api/index.ts | 4 +++ 11 files changed, 76 insertions(+), 16 deletions(-) diff --git a/server/api/cron.go b/server/api/cron.go index 714bda4a468..cdf42f54e1f 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -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) diff --git a/server/api/pipeline.go b/server/api/pipeline.go index db5259f802b..c6b7600b7ad 100644 --- a/server/api/pipeline.go +++ b/server/api/pipeline.go @@ -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 } diff --git a/server/cron/cron.go b/server/cron/cron.go index c9f5dc25316..919b856f053 100644 --- a/server/cron/cron.go +++ b/server/cron/cron.go @@ -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 diff --git a/server/cron/cron_test.go b/server/cron/cron_test.go index 831c156ca3d..94b319d0df4 100644 --- a/server/cron/cron_test.go +++ b/server/cron/cron_test.go @@ -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) diff --git a/server/router/api.go b/server/router/api.go index fbb413becf5..357eb5be891 100644 --- a/server/router/api.go +++ b/server/router/api.go @@ -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) diff --git a/web/components.d.ts b/web/components.d.ts index caf10baec2e..992bc6a15e2 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -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'] diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 0ce845afc09..b11aef09633 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -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)" diff --git a/web/src/components/atomic/Icon.vue b/web/src/components/atomic/Icon.vue index ed5671fda6a..16f5c4d7be5 100644 --- a/web/src/components/atomic/Icon.vue +++ b/web/src/components/atomic/Icon.vue @@ -41,6 +41,7 @@ +
@@ -90,7 +91,8 @@ export type IconNames = | 'stopwatch' | 'download' | 'auto-scroll' - | 'auto-scroll-off'; + | 'auto-scroll-off' + | 'play'; export default defineComponent({ name: 'Icon', diff --git a/web/src/components/repo/settings/CronTab.vue b/web/src/components/repo/settings/CronTab.vue index 1cca473c6a2..256166edefb 100644 --- a/web/src/components/repo/settings/CronTab.vue +++ b/web/src/components/repo/settings/CronTab.vue @@ -31,12 +31,8 @@ {{ $t('repo.settings.crons.next_exec') }}: {{ date.toLocaleString(new Date(cron.next_exec * 1000)) }} {{ $t('repo.settings.crons.not_executed_yet') }} - + + { + 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(); }); diff --git a/web/src/components/repo/settings/GeneralTab.vue b/web/src/components/repo/settings/GeneralTab.vue index fdadf18f2ea..a68cf3e8d49 100644 --- a/web/src/components/repo/settings/GeneralTab.vue +++ b/web/src/components/repo/settings/GeneralTab.vue @@ -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[] = [ diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index 817ca39a130..16ac6bac06a 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -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 { + return this._post(`/api/repos/${owner}/${repo}/cron/${cronId}`) as Promise; + } + getOrgPermissions(owner: string): Promise { return this._get(`/api/orgs/${owner}/permissions`) as Promise; }