mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-21 07:56:31 +00:00
Add ability to trigger manual builds (#1156)
closes #83 closes #240 Co-authored-by: Anbraten <anton@ju60.de> Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com> Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
parent
86bc751b95
commit
b4d89a1cce
33 changed files with 1096 additions and 806 deletions
|
@ -23,5 +23,6 @@ var Command = &cli.Command{
|
||||||
buildQueueCmd,
|
buildQueueCmd,
|
||||||
buildKillCmd,
|
buildKillCmd,
|
||||||
buildPsCmd,
|
buildPsCmd,
|
||||||
|
buildCreateCmd,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
78
cli/build/build_create.go
Normal file
78
cli/build/build_create.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/woodpecker-ci/woodpecker/woodpecker-go/woodpecker"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
"github.com/woodpecker-ci/woodpecker/cli/common"
|
||||||
|
"github.com/woodpecker-ci/woodpecker/cli/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var buildCreateCmd = &cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "create new build",
|
||||||
|
ArgsUsage: "<repo/name>",
|
||||||
|
Action: buildCreate,
|
||||||
|
Flags: append(common.GlobalFlags,
|
||||||
|
common.FormatFlag(tmplBuildList),
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "branch",
|
||||||
|
Usage: "branch to create build from",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "var",
|
||||||
|
Usage: "key=value",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCreate(c *cli.Context) error {
|
||||||
|
repo := c.Args().First()
|
||||||
|
|
||||||
|
owner, name, err := internal.ParseRepo(repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := internal.NewClient(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
branch := c.String("branch")
|
||||||
|
variables := make(map[string]string)
|
||||||
|
|
||||||
|
for _, vaz := range c.StringSlice("var") {
|
||||||
|
sp := strings.SplitN(vaz, "=", 2)
|
||||||
|
if len(sp) == 2 {
|
||||||
|
variables[sp[0]] = sp[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &woodpecker.BuildOptions{
|
||||||
|
Branch: branch,
|
||||||
|
Variables: variables,
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := client.BuildCreate(owner, name, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.New("_").Parse(c.String("format") + "\n")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(os.Stdout, build); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -273,10 +273,10 @@ when:
|
||||||
:::info
|
:::info
|
||||||
**By default steps are filtered by following event types:**
|
**By default steps are filtered by following event types:**
|
||||||
|
|
||||||
`push`, `pull_request`, `tag`, `deployment`.
|
`push`, `pull_request`, `tag`, `deployment`, `manual`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Available events: `push`, `pull_request`, `tag`, `deployment`, `cron`
|
Available events: `push`, `pull_request`, `tag`, `deployment`, `cron`, `manual`
|
||||||
|
|
||||||
Execute a step if the build event is a `tag`:
|
Execute a step if the build event is a `tag`:
|
||||||
|
|
||||||
|
|
|
@ -198,6 +198,30 @@ State: {{ .State }}
|
||||||
|
|
||||||
**--token, -t**="": server auth token
|
**--token, -t**="": server auth token
|
||||||
|
|
||||||
|
### create
|
||||||
|
|
||||||
|
create new build
|
||||||
|
|
||||||
|
**--branch**="": branch to create build from
|
||||||
|
|
||||||
|
**--format**="": format output (default: [33mBuild #{{ .Number }} [0m
|
||||||
|
Status: {{ .Status }}
|
||||||
|
Event: {{ .Event }}
|
||||||
|
Commit: {{ .Commit }}
|
||||||
|
Branch: {{ .Branch }}
|
||||||
|
Ref: {{ .Ref }}
|
||||||
|
Author: {{ .Author }} {{ if .Email }}<{{.Email}}>{{ end }}
|
||||||
|
Message: {{ .Message }}
|
||||||
|
)
|
||||||
|
|
||||||
|
**--log-level**="": set logging level (default: info)
|
||||||
|
|
||||||
|
**--server, -s**="": server address
|
||||||
|
|
||||||
|
**--token, -t**="": server auth token
|
||||||
|
|
||||||
|
**--var**="": key=value
|
||||||
|
|
||||||
## log
|
## log
|
||||||
|
|
||||||
manage logs
|
manage logs
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module github.com/woodpecker-ci/woodpecker
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.15.1-0.20220720025709-de34275bb64e
|
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe
|
||||||
codeberg.org/6543/go-yaml2json v0.2.1
|
codeberg.org/6543/go-yaml2json v0.2.1
|
||||||
github.com/bmatcuk/doublestar/v4 v4.2.0
|
github.com/bmatcuk/doublestar/v4 v4.2.0
|
||||||
github.com/caddyserver/certmagic v0.17.1-0.20220901172127-2e22c6fa8c47
|
github.com/caddyserver/certmagic v0.17.1-0.20220901172127-2e22c6fa8c47
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -54,8 +54,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
||||||
code.gitea.io/sdk/gitea v0.15.1-0.20220720025709-de34275bb64e h1:xayGBU2DwsrA5ZyqKNpXB91w3BfnkNcLDWZ7Ynn/w+g=
|
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe h1:PeLyxnUZE85QuJtBZ4P8qCQcgWG5Ked67mlNgr0WkCQ=
|
||||||
code.gitea.io/sdk/gitea v0.15.1-0.20220720025709-de34275bb64e/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE=
|
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE=
|
||||||
codeberg.org/6543/go-yaml2json v0.2.1 h1:S0dxlzRRpYnSLODxpbqaUfmJYZZg0Wcpf8bI9YzyOXo=
|
codeberg.org/6543/go-yaml2json v0.2.1 h1:S0dxlzRRpYnSLODxpbqaUfmJYZZg0Wcpf8bI9YzyOXo=
|
||||||
codeberg.org/6543/go-yaml2json v0.2.1/go.mod h1:mz61q14LWF4ZABrgMEDMmk3t9dPi6zgR1uBh2VKV2RQ=
|
codeberg.org/6543/go-yaml2json v0.2.1/go.mod h1:mz61q14LWF4ZABrgMEDMmk3t9dPi6zgR1uBh2VKV2RQ=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
|
|
@ -29,6 +29,7 @@ const (
|
||||||
EventTag = "tag"
|
EventTag = "tag"
|
||||||
EventDeploy = "deployment"
|
EventDeploy = "deployment"
|
||||||
EventCron = "cron"
|
EventCron = "cron"
|
||||||
|
EventManual = "manual"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
|
@ -166,6 +166,7 @@ func (c *Constraint) SetDefaultEventFilter() {
|
||||||
frontend.EventPull,
|
frontend.EventPull,
|
||||||
frontend.EventTag,
|
frontend.EventTag,
|
||||||
frontend.EventDeploy,
|
frontend.EventDeploy,
|
||||||
|
frontend.EventManual,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,15 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/woodpecker-ci/woodpecker/server"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
@ -34,6 +37,53 @@ import (
|
||||||
"github.com/woodpecker-ci/woodpecker/server/store"
|
"github.com/woodpecker-ci/woodpecker/server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func CreateBuild(c *gin.Context) {
|
||||||
|
_store := store.FromContext(c)
|
||||||
|
repo := session.Repo(c)
|
||||||
|
|
||||||
|
var p model.BuildOptions
|
||||||
|
|
||||||
|
err := json.NewDecoder(c.Request.Body).Decode(&p)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := session.User(c)
|
||||||
|
|
||||||
|
lastCommit, _ := server.Config.Services.Remote.BranchHead(c, user, repo, p.Branch)
|
||||||
|
|
||||||
|
tmpBuild := createTmpBuild(model.EventManual, lastCommit, repo, user, &p)
|
||||||
|
|
||||||
|
build, err := pipeline.Create(c, _store, repo, tmpBuild)
|
||||||
|
if err != nil {
|
||||||
|
handlePipelineErr(c, err)
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, build)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTmpBuild(event model.WebhookEvent, commitSHA string, repo *model.Repo, user *model.User, opts *model.BuildOptions) *model.Build {
|
||||||
|
return &model.Build{
|
||||||
|
Event: event,
|
||||||
|
Commit: commitSHA,
|
||||||
|
Branch: opts.Branch,
|
||||||
|
Timestamp: time.Now().UTC().Unix(),
|
||||||
|
|
||||||
|
Avatar: user.Avatar,
|
||||||
|
Message: "MANUAL BUILD @ " + opts.Branch,
|
||||||
|
|
||||||
|
Ref: opts.Branch,
|
||||||
|
AdditionalVariables: opts.Variables,
|
||||||
|
|
||||||
|
Author: user.Login,
|
||||||
|
Email: user.Email,
|
||||||
|
|
||||||
|
// TODO: Generate proper link to commit
|
||||||
|
Link: repo.Link,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetBuilds(c *gin.Context) {
|
func GetBuilds(c *gin.Context) {
|
||||||
repo := session.Repo(c)
|
repo := session.Repo(c)
|
||||||
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
|
|
@ -17,40 +17,41 @@ package model
|
||||||
|
|
||||||
// swagger:model build
|
// swagger:model build
|
||||||
type Build struct {
|
type Build struct {
|
||||||
ID int64 `json:"id" xorm:"pk autoincr 'build_id'"`
|
ID int64 `json:"id" xorm:"pk autoincr 'build_id'"`
|
||||||
RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'build_repo_id'"`
|
RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'build_repo_id'"`
|
||||||
Number int64 `json:"number" xorm:"UNIQUE(s) 'build_number'"`
|
Number int64 `json:"number" xorm:"UNIQUE(s) 'build_number'"`
|
||||||
Author string `json:"author" xorm:"INDEX 'build_author'"`
|
Author string `json:"author" xorm:"INDEX 'build_author'"`
|
||||||
ConfigID int64 `json:"-" xorm:"build_config_id"`
|
ConfigID int64 `json:"-" xorm:"build_config_id"`
|
||||||
Parent int64 `json:"parent" xorm:"build_parent"`
|
Parent int64 `json:"parent" xorm:"build_parent"`
|
||||||
Event WebhookEvent `json:"event" xorm:"build_event"`
|
Event WebhookEvent `json:"event" xorm:"build_event"`
|
||||||
Status StatusValue `json:"status" xorm:"INDEX 'build_status'"`
|
Status StatusValue `json:"status" xorm:"INDEX 'build_status'"`
|
||||||
Error string `json:"error" xorm:"build_error"`
|
Error string `json:"error" xorm:"build_error"`
|
||||||
Enqueued int64 `json:"enqueued_at" xorm:"build_enqueued"`
|
Enqueued int64 `json:"enqueued_at" xorm:"build_enqueued"`
|
||||||
Created int64 `json:"created_at" xorm:"build_created"`
|
Created int64 `json:"created_at" xorm:"build_created"`
|
||||||
Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"`
|
Updated int64 `json:"updated_at" xorm:"updated NOT NULL DEFAULT 0 'updated'"`
|
||||||
Started int64 `json:"started_at" xorm:"build_started"`
|
Started int64 `json:"started_at" xorm:"build_started"`
|
||||||
Finished int64 `json:"finished_at" xorm:"build_finished"`
|
Finished int64 `json:"finished_at" xorm:"build_finished"`
|
||||||
Deploy string `json:"deploy_to" xorm:"build_deploy"`
|
Deploy string `json:"deploy_to" xorm:"build_deploy"`
|
||||||
Commit string `json:"commit" xorm:"build_commit"`
|
Commit string `json:"commit" xorm:"build_commit"`
|
||||||
Branch string `json:"branch" xorm:"build_branch"`
|
Branch string `json:"branch" xorm:"build_branch"`
|
||||||
Ref string `json:"ref" xorm:"build_ref"`
|
Ref string `json:"ref" xorm:"build_ref"`
|
||||||
Refspec string `json:"refspec" xorm:"build_refspec"`
|
Refspec string `json:"refspec" xorm:"build_refspec"`
|
||||||
Remote string `json:"remote" xorm:"build_remote"`
|
Remote string `json:"remote" xorm:"build_remote"`
|
||||||
Title string `json:"title" xorm:"build_title"`
|
Title string `json:"title" xorm:"build_title"`
|
||||||
Message string `json:"message" xorm:"build_message"`
|
Message string `json:"message" xorm:"build_message"`
|
||||||
Timestamp int64 `json:"timestamp" xorm:"build_timestamp"`
|
Timestamp int64 `json:"timestamp" xorm:"build_timestamp"`
|
||||||
Sender string `json:"sender" xorm:"build_sender"` // uses reported user for webhooks and name of cron for cron pipelines
|
Sender string `json:"sender" xorm:"build_sender"` // uses reported user for webhooks and name of cron for cron pipelines
|
||||||
Avatar string `json:"author_avatar" xorm:"build_avatar"`
|
Avatar string `json:"author_avatar" xorm:"build_avatar"`
|
||||||
Email string `json:"author_email" xorm:"build_email"`
|
Email string `json:"author_email" xorm:"build_email"`
|
||||||
Link string `json:"link_url" xorm:"build_link"`
|
Link string `json:"link_url" xorm:"build_link"`
|
||||||
Signed bool `json:"signed" xorm:"build_signed"` // deprecate
|
Signed bool `json:"signed" xorm:"build_signed"` // deprecate
|
||||||
Verified bool `json:"verified" xorm:"build_verified"` // deprecate
|
Verified bool `json:"verified" xorm:"build_verified"` // deprecate
|
||||||
Reviewer string `json:"reviewed_by" xorm:"build_reviewer"`
|
Reviewer string `json:"reviewed_by" xorm:"build_reviewer"`
|
||||||
Reviewed int64 `json:"reviewed_at" xorm:"build_reviewed"`
|
Reviewed int64 `json:"reviewed_at" xorm:"build_reviewed"`
|
||||||
Procs []*Proc `json:"procs,omitempty" xorm:"-"`
|
Procs []*Proc `json:"procs,omitempty" xorm:"-"`
|
||||||
Files []*File `json:"files,omitempty" xorm:"-"`
|
Files []*File `json:"files,omitempty" xorm:"-"`
|
||||||
ChangedFiles []string `json:"changed_files,omitempty" xorm:"json 'changed_files'"`
|
ChangedFiles []string `json:"changed_files,omitempty" xorm:"json 'changed_files'"`
|
||||||
|
AdditionalVariables map[string]string `json:"variables,omitempty" xorm:"json 'additional_variables'"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName return database table name for xorm
|
// TableName return database table name for xorm
|
||||||
|
@ -61,3 +62,8 @@ func (Build) TableName() string {
|
||||||
type UpdateBuildStore interface {
|
type UpdateBuildStore interface {
|
||||||
UpdateBuild(*Build) error
|
UpdateBuild(*Build) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BuildOptions struct {
|
||||||
|
Branch string `json:"branch"`
|
||||||
|
Variables map[string]string `json:"variables"`
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ const (
|
||||||
EventTag WebhookEvent = "tag"
|
EventTag WebhookEvent = "tag"
|
||||||
EventDeploy WebhookEvent = "deployment"
|
EventDeploy WebhookEvent = "deployment"
|
||||||
EventCron WebhookEvent = "cron"
|
EventCron WebhookEvent = "cron"
|
||||||
|
EventManual WebhookEvent = "manual"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateWebhookEvent(s WebhookEvent) bool {
|
func ValidateWebhookEvent(s WebhookEvent) bool {
|
||||||
|
|
|
@ -60,6 +60,10 @@ func createBuildItems(ctx context.Context, store store.Store, build *model.Build
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, v := range build.AdditionalVariables {
|
||||||
|
envs[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
b := shared.ProcBuilder{
|
b := shared.ProcBuilder{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Curr: build,
|
Curr: build,
|
||||||
|
|
|
@ -76,6 +76,7 @@ func apiRoutes(e *gin.Engine) {
|
||||||
repo.GET("/branches", api.GetRepoBranches)
|
repo.GET("/branches", api.GetRepoBranches)
|
||||||
|
|
||||||
repo.GET("/builds", api.GetBuilds)
|
repo.GET("/builds", api.GetBuilds)
|
||||||
|
repo.POST("/builds", session.MustPush, api.CreateBuild)
|
||||||
repo.GET("/builds/:number", api.GetBuild)
|
repo.GET("/builds/:number", api.GetBuild)
|
||||||
repo.GET("/builds/:number/config", api.GetBuildConfig)
|
repo.GET("/builds/:number/config", api.GetBuildConfig)
|
||||||
|
|
||||||
|
|
4
web/components.d.ts
vendored
4
web/components.d.ts
vendored
|
@ -46,10 +46,12 @@ declare module '@vue/runtime-core' {
|
||||||
IIcRoundLightMode: typeof import('~icons/ic/round-light-mode')['default']
|
IIcRoundLightMode: typeof import('~icons/ic/round-light-mode')['default']
|
||||||
IIcSharpTimelapse: typeof import('~icons/ic/sharp-timelapse')['default']
|
IIcSharpTimelapse: typeof import('~icons/ic/sharp-timelapse')['default']
|
||||||
IIcTwotoneAdd: typeof import('~icons/ic/twotone-add')['default']
|
IIcTwotoneAdd: typeof import('~icons/ic/twotone-add')['default']
|
||||||
|
ILaTimes: typeof import('~icons/la/times')['default']
|
||||||
IMdiBitbucket: typeof import('~icons/mdi/bitbucket')['default']
|
IMdiBitbucket: typeof import('~icons/mdi/bitbucket')['default']
|
||||||
IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||||
IMdiClockTimeEightOutline: typeof import('~icons/mdi/clock-time-eight-outline')['default']
|
IMdiClockTimeEightOutline: typeof import('~icons/mdi/clock-time-eight-outline')['default']
|
||||||
IMdiFormatListBulleted: typeof import('~icons/mdi/format-list-bulleted')['default']
|
IMdiFormatListBulleted: typeof import('~icons/mdi/format-list-bulleted')['default']
|
||||||
|
IMdiGestureTap: typeof import('~icons/mdi/gesture-tap')['default']
|
||||||
IMdiGithub: typeof import('~icons/mdi/github')['default']
|
IMdiGithub: typeof import('~icons/mdi/github')['default']
|
||||||
IMdiLoading: typeof import('~icons/mdi/loading')['default']
|
IMdiLoading: typeof import('~icons/mdi/loading')['default']
|
||||||
IMdiSourceBranch: typeof import('~icons/mdi/source-branch')['default']
|
IMdiSourceBranch: typeof import('~icons/mdi/source-branch')['default']
|
||||||
|
@ -70,10 +72,12 @@ declare module '@vue/runtime-core' {
|
||||||
ITeenyiconsGitSolid: typeof import('~icons/teenyicons/git-solid')['default']
|
ITeenyiconsGitSolid: typeof import('~icons/teenyicons/git-solid')['default']
|
||||||
IVaadinQuestionCircleO: typeof import('~icons/vaadin/question-circle-o')['default']
|
IVaadinQuestionCircleO: typeof import('~icons/vaadin/question-circle-o')['default']
|
||||||
ListItem: typeof import('./src/components/atomic/ListItem.vue')['default']
|
ListItem: typeof import('./src/components/atomic/ListItem.vue')['default']
|
||||||
|
ManualPipelinePopup: typeof import('./src/components/layout/popups/ManualPipelinePopup.vue')['default']
|
||||||
Navbar: typeof import('./src/components/layout/header/Navbar.vue')['default']
|
Navbar: typeof import('./src/components/layout/header/Navbar.vue')['default']
|
||||||
NumberField: typeof import('./src/components/form/NumberField.vue')['default']
|
NumberField: typeof import('./src/components/form/NumberField.vue')['default']
|
||||||
OrgSecretsTab: typeof import('./src/components/org/settings/OrgSecretsTab.vue')['default']
|
OrgSecretsTab: typeof import('./src/components/org/settings/OrgSecretsTab.vue')['default']
|
||||||
Panel: typeof import('./src/components/layout/Panel.vue')['default']
|
Panel: typeof import('./src/components/layout/Panel.vue')['default']
|
||||||
|
Popup: typeof import('./src/components/layout/Popup.vue')['default']
|
||||||
RadioField: typeof import('./src/components/form/RadioField.vue')['default']
|
RadioField: typeof import('./src/components/form/RadioField.vue')['default']
|
||||||
RegistriesTab: typeof import('./src/components/repo/settings/RegistriesTab.vue')['default']
|
RegistriesTab: typeof import('./src/components/repo/settings/RegistriesTab.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|
|
@ -17,56 +17,56 @@
|
||||||
"test": "echo 'No tests configured' && exit 0"
|
"test": "echo 'No tests configured' && exit 0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/vite-plugin-vue-i18n": "^6.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||||
"@kyvg/vue3-notification": "^2.3.6",
|
"@kyvg/vue3-notification": "^2.4.1",
|
||||||
"@meforma/vue-toaster": "^1.3.0",
|
"@meforma/vue-toaster": "^1.3.0",
|
||||||
"@vueuse/core": "^9.1.1",
|
"@vueuse/core": "^9.2.0",
|
||||||
"ansi_up": "^5.1.0",
|
"ansi_up": "^5.1.0",
|
||||||
"dayjs": "^1.11.4",
|
"dayjs": "^1.11.5",
|
||||||
"floating-vue": "^2.0.0-beta.19",
|
"floating-vue": "^2.0.0-beta.20",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"humanize-duration": "^3.27.2",
|
"humanize-duration": "^3.27.3",
|
||||||
"javascript-time-ago": "^2.5.7",
|
"javascript-time-ago": "^2.5.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"node-emoji": "^1.11.0",
|
"node-emoji": "^1.11.0",
|
||||||
"pinia": "^2.0.17",
|
"pinia": "^2.0.22",
|
||||||
"prismjs": "^1.28.0",
|
"prismjs": "^1.29.0",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.39",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.1.3"
|
"vue-router": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "^2.1.88",
|
"@iconify/json": "^2.1.106",
|
||||||
"@types/humanize-duration": "^3.27.1",
|
"@types/humanize-duration": "^3.27.1",
|
||||||
"@types/javascript-time-ago": "^2.0.3",
|
"@types/javascript-time-ago": "^2.0.3",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.185",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^18.7.17",
|
||||||
"@types/node-emoji": "^1.8.1",
|
"@types/node-emoji": "^1.8.1",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||||
"@typescript-eslint/parser": "^5.33.0",
|
"@typescript-eslint/parser": "^5.37.0",
|
||||||
"@vitejs/plugin-vue": "^3.0.1",
|
"@vitejs/plugin-vue": "^3.1.0",
|
||||||
"@vue/compiler-sfc": "^3.2.37",
|
"@vue/compiler-sfc": "^3.2.39",
|
||||||
"eslint": "^8.21.0",
|
"eslint": "^8.23.1",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
"eslint-plugin-promise": "^6.0.1",
|
||||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"eslint-plugin-vue": "^9.3.0",
|
"eslint-plugin-vue": "^9.4.0",
|
||||||
"eslint-plugin-vue-scoped-css": "^2.2.0",
|
"eslint-plugin-vue-scoped-css": "^2.2.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"typescript": "4.4.4",
|
"typescript": "4.8.3",
|
||||||
"unplugin-icons": "^0.14.8",
|
"unplugin-icons": "^0.14.9",
|
||||||
"unplugin-vue-components": "^0.22.3",
|
"unplugin-vue-components": "^0.22.7",
|
||||||
"vite": "^3.0.4",
|
"vite": "^3.1.0",
|
||||||
"vite-plugin-prismjs": "^0.0.8",
|
"vite-plugin-prismjs": "^0.0.8",
|
||||||
"vite-plugin-windicss": "^1.8.7",
|
"vite-plugin-windicss": "^1.8.8",
|
||||||
"vite-svg-loader": "^3.4.0",
|
"vite-svg-loader": "^3.6.0",
|
||||||
"vue-eslint-parser": "^9.0.3",
|
"vue-eslint-parser": "^9.1.0",
|
||||||
"vue-tsc": "^0.39.5",
|
"vue-tsc": "^0.40.13",
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,18 @@
|
||||||
"not_started": "not started yet"
|
"not_started": "not started yet"
|
||||||
},
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
|
"manual_pipeline": {
|
||||||
|
"title": "Trigger a manual pipeline run",
|
||||||
|
"trigger": "Run pipeline",
|
||||||
|
"select_branch": "Select branch",
|
||||||
|
"variables": {
|
||||||
|
"add": "Add variable",
|
||||||
|
"title": "Additional pipeline variables",
|
||||||
|
"desc": "Specify additional variables to use in your pipeline. Variables with the same name will be overwritten.",
|
||||||
|
"name": "Variable name",
|
||||||
|
"value": "Variable value"
|
||||||
|
}
|
||||||
|
},
|
||||||
"activity": "Activity",
|
"activity": "Activity",
|
||||||
"branches": "Branches",
|
"branches": "Branches",
|
||||||
"add": "Add repository",
|
"add": "Add repository",
|
||||||
|
@ -207,7 +219,8 @@
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"pr": "Pull Request",
|
"pr": "Pull Request",
|
||||||
"deploy": "Deploy",
|
"deploy": "Deploy",
|
||||||
"cron": "Cron"
|
"cron": "Cron",
|
||||||
|
"manual": "Manual"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<i-mdi-clock-time-eight-outline v-else-if="name === 'since'" class="h-6 w-6" />
|
<i-mdi-clock-time-eight-outline v-else-if="name === 'since'" class="h-6 w-6" />
|
||||||
<i-mdi-source-branch v-else-if="name === 'push'" class="h-6 w-6" />
|
<i-mdi-source-branch v-else-if="name === 'push'" class="h-6 w-6" />
|
||||||
<i-mdi-source-pull v-else-if="name === 'pull_request'" class="h-6 w-6" />
|
<i-mdi-source-pull v-else-if="name === 'pull_request'" class="h-6 w-6" />
|
||||||
|
<i-mdi-gesture-tap v-else-if="name === 'manual-pipeline'" class="h-6 w-6" />
|
||||||
<i-mdi-tag-outline v-else-if="name === 'tag'" class="h-6 w-6" />
|
<i-mdi-tag-outline v-else-if="name === 'tag'" class="h-6 w-6" />
|
||||||
<i-clarity-deploy-line v-else-if="name === 'deployment'" class="h-6 w-6" />
|
<i-clarity-deploy-line v-else-if="name === 'deployment'" class="h-6 w-6" />
|
||||||
<i-mdisource-commit v-else-if="name === 'commit'" class="h-6 w-6" />
|
<i-mdisource-commit v-else-if="name === 'commit'" class="h-6 w-6" />
|
||||||
|
@ -51,6 +52,7 @@ export type IconNames =
|
||||||
| 'since'
|
| 'since'
|
||||||
| 'push'
|
| 'push'
|
||||||
| 'pull_request'
|
| 'pull_request'
|
||||||
|
| 'manual-pipeline'
|
||||||
| 'tag'
|
| 'tag'
|
||||||
| 'deployment'
|
| 'deployment'
|
||||||
| 'commit'
|
| 'commit'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full border border-gray-200 py-1 px-2 rounded-md bg-white hover:border-gray-300 dark:bg-dark-gray-700 dark:border-dark-400 dark:hover:border-dark-800"
|
class="w-full border border-gray-200 py-1 px-2 rounded-md bg-white hover:border-gray-300 dark:bg-dark-gray-700 dark:border-dark-400 dark:hover:border-dark-800"
|
||||||
|
:class="{ 'bg-gray-200 dark:bg-gray-600': disabled }"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="lines === 1"
|
v-if="lines === 1"
|
||||||
|
|
24
web/src/components/layout/Popup.vue
Normal file
24
web/src/components/layout/Popup.vue
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<!-- overlay -->
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
class="fixed bg-gray-900 opacity-80 left-0 top-0 right-0 bottom-0 z-500 print:hidden"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
<!-- overlay end -->
|
||||||
|
<transition class="print:hidden fixed flex top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||||
|
<div v-if="open" class="m-auto flex flex-col shadow-all z-1000 max-w-3/5 max-h-3/5 h-auto">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
open: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(event: 'close'): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
120
web/src/components/layout/popups/ManualPipelinePopup.vue
Normal file
120
web/src/components/layout/popups/ManualPipelinePopup.vue
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<Popup :open="open" @close="$emit('close')">
|
||||||
|
<Panel v-if="!loading">
|
||||||
|
<span class="text-xl text-color">{{ $t('repo.manual_pipeline.title') }}</span>
|
||||||
|
<InputField :label="$t('repo.manual_pipeline.select_branch')">
|
||||||
|
<SelectField
|
||||||
|
v-model="payload.branch"
|
||||||
|
:options="branches"
|
||||||
|
required
|
||||||
|
class="dark:bg-dark-gray-700 bg-transparent text-color border-gray-200 dark:border-dark-400"
|
||||||
|
/>
|
||||||
|
</InputField>
|
||||||
|
<InputField :label="$t('repo.manual_pipeline.variables.title')">
|
||||||
|
<span class="text-sm text-color-alt mb-2">{{ $t('repo.manual_pipeline.variables.desc') }}</span>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div v-for="(value, name) in payload.variables" :key="name" class="flex gap-4">
|
||||||
|
<TextField :model-value="name" disabled />
|
||||||
|
<TextField :model-value="value" disabled />
|
||||||
|
<div class="w-34 flex-shrink-0">
|
||||||
|
<Button type="submit" class="ml-auto" @click="deleteVar(name)">
|
||||||
|
<i-la-times />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form class="flex gap-4" @submit.prevent="addPipelineVariable">
|
||||||
|
<TextField
|
||||||
|
v-model="newPipelineVariable.name"
|
||||||
|
:placeholder="$t('repo.manual_pipeline.variables.name')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
v-model="newPipelineVariable.value"
|
||||||
|
:placeholder="$t('repo.manual_pipeline.variables.value')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
class="w-34 flex-shrink-0"
|
||||||
|
start-icon="plus"
|
||||||
|
type="submit"
|
||||||
|
:text="$t('repo.manual_pipeline.variables.add')"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</InputField>
|
||||||
|
<Button type="submit" :text="$t('repo.manual_pipeline.trigger')" @click="triggerManualPipeline" />
|
||||||
|
</Panel>
|
||||||
|
</Popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import InputField from '~/components/form/InputField.vue';
|
||||||
|
import SelectField from '~/components/form/SelectField.vue';
|
||||||
|
import TextField from '~/components/form/TextField.vue';
|
||||||
|
import Panel from '~/components/layout/Panel.vue';
|
||||||
|
import Popup from '~/components/layout/Popup.vue';
|
||||||
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
|
import { inject } from '~/compositions/useInjectProvide';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
open: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const apiClient = useApiClient();
|
||||||
|
|
||||||
|
const repo = inject('repo');
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const branches = ref<{ text: string; value: string }[]>([]);
|
||||||
|
const payload = ref<{ branch: string; variables: Record<string, string> }>({
|
||||||
|
branch: 'main',
|
||||||
|
variables: {},
|
||||||
|
});
|
||||||
|
const newPipelineVariable = ref<{ name: string; value: string }>({ name: '', value: '' });
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = await apiClient.getRepoBranches(repo.value.owner, repo.value.name);
|
||||||
|
branches.value = data.map((e) => ({
|
||||||
|
text: e,
|
||||||
|
value: e,
|
||||||
|
}));
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function addPipelineVariable() {
|
||||||
|
if (!newPipelineVariable.value.name || !newPipelineVariable.value.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.value.variables[newPipelineVariable.value.name] = newPipelineVariable.value.value;
|
||||||
|
newPipelineVariable.value.name = '';
|
||||||
|
newPipelineVariable.value.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteVar(key: string) {
|
||||||
|
delete payload.value.variables[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerManualPipeline() {
|
||||||
|
loading.value = true;
|
||||||
|
const build = await apiClient.createBuild(repo.value.owner, repo.value.name, payload.value);
|
||||||
|
|
||||||
|
emit('close');
|
||||||
|
|
||||||
|
await router.push({
|
||||||
|
name: 'repo-build',
|
||||||
|
params: {
|
||||||
|
buildId: build.number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -35,6 +35,7 @@
|
||||||
<Icon v-else-if="build.event === 'deployment'" name="deployment" />
|
<Icon v-else-if="build.event === 'deployment'" name="deployment" />
|
||||||
<Icon v-else-if="build.event === 'tag'" name="tag" />
|
<Icon v-else-if="build.event === 'tag'" name="tag" />
|
||||||
<Icon v-else-if="build.event === 'cron'" name="push" />
|
<Icon v-else-if="build.event === 'cron'" name="push" />
|
||||||
|
<Icon v-else-if="build.event === 'manual'" name="manual-pipeline" />
|
||||||
<Icon v-else name="push" />
|
<Icon v-else name="push" />
|
||||||
<span class="truncate">{{ prettyRef }}</span>
|
<span class="truncate">{{ prettyRef }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<span>{{ build.author }}</span>
|
<span>{{ build.author }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-1 items-center min-w-0">
|
<div class="flex space-x-1 items-center min-w-0">
|
||||||
|
<Icon v-if="build.event === 'manual'" name="manual-pipeline" />
|
||||||
<Icon v-if="build.event === 'push'" name="push" />
|
<Icon v-if="build.event === 'push'" name="push" />
|
||||||
<Icon v-if="build.event === 'deployment'" name="deployment" />
|
<Icon v-if="build.event === 'deployment'" name="deployment" />
|
||||||
<Icon v-else-if="build.event === 'tag'" name="tag" />
|
<Icon v-else-if="build.event === 'tag'" name="tag" />
|
||||||
|
@ -45,7 +46,7 @@
|
||||||
<div class="md:absolute top-0 left-0 w-full">
|
<div class="md:absolute top-0 left-0 w-full">
|
||||||
<div v-for="proc in build.procs" :key="proc.id">
|
<div v-for="proc in build.procs" :key="proc.id">
|
||||||
<div class="p-4 pb-1 flex flex-wrap items-center justify-between">
|
<div class="p-4 pb-1 flex flex-wrap items-center justify-between">
|
||||||
<div v-if="build.procs.length > 1" class="flex items-center">
|
<div v-if="build.procs && build.procs.length > 1" class="flex items-center">
|
||||||
<span class="ml-2">{{ proc.name }}</span>
|
<span class="ml-2">{{ proc.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="proc.environ" class="text-xs">
|
<div v-if="proc.environ" class="text-xs">
|
||||||
|
|
|
@ -31,8 +31,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, defineComponent, PropType, toRef } from 'vue';
|
import { computed, toRef } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import Button from '~/components/atomic/Button.vue';
|
import Button from '~/components/atomic/Button.vue';
|
||||||
|
@ -42,92 +42,58 @@ import InputField from '~/components/form/InputField.vue';
|
||||||
import TextField from '~/components/form/TextField.vue';
|
import TextField from '~/components/form/TextField.vue';
|
||||||
import { Secret, WebhookEvents } from '~/lib/api/types';
|
import { Secret, WebhookEvents } from '~/lib/api/types';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
name: 'SecretEdit',
|
modelValue: Partial<Secret>;
|
||||||
|
isSaving: boolean;
|
||||||
|
i18nPrefix: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
components: {
|
const emit = defineEmits<{
|
||||||
Button,
|
(event: 'update:modelValue', value: Partial<Secret> | undefined): void;
|
||||||
InputField,
|
(event: 'save', value: Partial<Secret>): void;
|
||||||
TextField,
|
}>();
|
||||||
CheckboxesField,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
const i18n = useI18n();
|
||||||
// used by toRef
|
|
||||||
// eslint-disable-next-line vue/no-unused-properties
|
|
||||||
modelValue: {
|
|
||||||
type: Object as PropType<Partial<Secret>>,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
|
|
||||||
isSaving: {
|
const modelValue = toRef(props, 'modelValue');
|
||||||
type: Boolean,
|
const innerValue = computed({
|
||||||
},
|
get: () => modelValue.value,
|
||||||
|
set: (value) => {
|
||||||
i18nPrefix: {
|
emit('update:modelValue', value);
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
'update:modelValue': (_value: Partial<Secret> | undefined): boolean => true,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
save: (_value: Partial<Secret>): boolean => true,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup: (props, ctx) => {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const modelValue = toRef(props, 'modelValue');
|
|
||||||
const innerValue = computed({
|
|
||||||
get: () => modelValue.value,
|
|
||||||
set: (value) => {
|
|
||||||
ctx.emit('update:modelValue', value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const images = computed<string>({
|
|
||||||
get() {
|
|
||||||
return innerValue.value?.image?.join(',') || '';
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
if (innerValue.value) {
|
|
||||||
innerValue.value.image = value
|
|
||||||
.split(',')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter((s) => s !== '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const isEditingSecret = computed(() => !!innerValue.value?.id);
|
|
||||||
|
|
||||||
const secretEventsOptions: CheckboxOption[] = [
|
|
||||||
{ value: WebhookEvents.Push, text: i18n.t('repo.build.event.push') },
|
|
||||||
{ value: WebhookEvents.Tag, text: i18n.t('repo.build.event.tag') },
|
|
||||||
{
|
|
||||||
value: WebhookEvents.PullRequest,
|
|
||||||
text: i18n.t('repo.build.event.pr'),
|
|
||||||
description: i18n.t('repo.settings.secrets.events.pr_warning'),
|
|
||||||
},
|
|
||||||
{ value: WebhookEvents.Deploy, text: i18n.t('repo.build.event.deploy') },
|
|
||||||
{ value: WebhookEvents.Cron, text: i18n.t('repo.build.event.cron') },
|
|
||||||
];
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
if (!innerValue.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.emit('save', innerValue.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
innerValue,
|
|
||||||
isEditingSecret,
|
|
||||||
secretEventsOptions,
|
|
||||||
images,
|
|
||||||
save,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const images = computed<string>({
|
||||||
|
get() {
|
||||||
|
return innerValue.value?.image?.join(',') || '';
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (innerValue.value) {
|
||||||
|
innerValue.value.image = value
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s !== '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const isEditingSecret = computed(() => !!innerValue.value?.id);
|
||||||
|
|
||||||
|
const secretEventsOptions: CheckboxOption[] = [
|
||||||
|
{ value: WebhookEvents.Push, text: i18n.t('repo.build.event.push') },
|
||||||
|
{ value: WebhookEvents.Tag, text: i18n.t('repo.build.event.tag') },
|
||||||
|
{
|
||||||
|
value: WebhookEvents.PullRequest,
|
||||||
|
text: i18n.t('repo.build.event.pr'),
|
||||||
|
description: i18n.t('repo.settings.secrets.events.pr_warning'),
|
||||||
|
},
|
||||||
|
{ value: WebhookEvents.Deploy, text: i18n.t('repo.build.event.deploy') },
|
||||||
|
{ value: WebhookEvents.Cron, text: i18n.t('repo.build.event.cron') },
|
||||||
|
{ value: WebhookEvents.Manual, text: i18n.t('repo.build.event.manual') },
|
||||||
|
];
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!innerValue.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('save', innerValue.value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -24,59 +24,31 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, PropType, toRef } from 'vue';
|
import { toRef } from 'vue';
|
||||||
|
|
||||||
import IconButton from '~/components/atomic/IconButton.vue';
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
import ListItem from '~/components/atomic/ListItem.vue';
|
import ListItem from '~/components/atomic/ListItem.vue';
|
||||||
import { Secret } from '~/lib/api/types';
|
import { Secret } from '~/lib/api/types';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
name: 'SecretList',
|
modelValue: Secret[];
|
||||||
|
isDeleting: boolean;
|
||||||
|
i18nPrefix: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
components: {
|
const emit = defineEmits<{
|
||||||
ListItem,
|
(event: 'edit', secret: Secret): void;
|
||||||
IconButton,
|
(event: 'delete', secret: Secret): void;
|
||||||
},
|
}>();
|
||||||
|
|
||||||
props: {
|
const secrets = toRef(props, 'modelValue');
|
||||||
// used by toRef
|
|
||||||
// eslint-disable-next-line vue/no-unused-properties
|
|
||||||
modelValue: {
|
|
||||||
type: Array as PropType<Secret[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
isDeleting: {
|
function editSecret(secret: Secret) {
|
||||||
type: Boolean,
|
emit('edit', secret);
|
||||||
required: true,
|
}
|
||||||
},
|
|
||||||
|
|
||||||
i18nPrefix: {
|
function deleteSecret(secret: Secret) {
|
||||||
type: String,
|
emit('delete', secret);
|
||||||
required: true,
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
edit: (secret: Secret): boolean => true,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
delete: (secret: Secret): boolean => true,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props, ctx) {
|
|
||||||
const secrets = toRef(props, 'modelValue');
|
|
||||||
|
|
||||||
function editSecret(secret: Secret) {
|
|
||||||
ctx.emit('edit', secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteSecret(secret: Secret) {
|
|
||||||
ctx.emit('delete', secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { secrets, editSecret, deleteSecret };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
19
web/src/compositions/useInjectProvide.ts
Normal file
19
web/src/compositions/useInjectProvide.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { inject as vueInject, provide as vueProvide, Ref } from 'vue';
|
||||||
|
|
||||||
|
import { Repo } from '~/lib/api/types';
|
||||||
|
|
||||||
|
export type InjectKeys = {
|
||||||
|
repo: Ref<Repo>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function inject<T extends keyof InjectKeys>(key: T): InjectKeys[T] {
|
||||||
|
const value = vueInject<InjectKeys[T]>(key);
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(`Please provide a value for ${key}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function provide<T extends keyof InjectKeys>(key: T, value: InjectKeys[T]): void {
|
||||||
|
return vueProvide(key, value);
|
||||||
|
}
|
|
@ -18,6 +18,11 @@ type RepoListOptions = {
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
flush?: boolean;
|
flush?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BuildOptions = {
|
||||||
|
branch: string;
|
||||||
|
variables: Record<string, string>;
|
||||||
|
};
|
||||||
export default class WoodpeckerClient extends ApiClient {
|
export default class WoodpeckerClient extends ApiClient {
|
||||||
getRepoList(opts?: RepoListOptions): Promise<Repo[]> {
|
getRepoList(opts?: RepoListOptions): Promise<Repo[]> {
|
||||||
const query = encodeQueryString(opts);
|
const query = encodeQueryString(opts);
|
||||||
|
@ -58,6 +63,10 @@ export default class WoodpeckerClient extends ApiClient {
|
||||||
return this._get(`/api/repos/${owner}/${repo}/builds?${query}`) as Promise<Build[]>;
|
return this._get(`/api/repos/${owner}/${repo}/builds?${query}`) as Promise<Build[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createBuild(owner: string, repo: string, options: BuildOptions): Promise<Build> {
|
||||||
|
return this._post(`/api/repos/${owner}/${repo}/builds`, options) as Promise<Build>;
|
||||||
|
}
|
||||||
|
|
||||||
getBuild(owner: string, repo: string, number: number | 'latest'): Promise<Build> {
|
getBuild(owner: string, repo: string, number: number | 'latest'): Promise<Build> {
|
||||||
return this._get(`/api/repos/${owner}/${repo}/builds/${number}`) as Promise<Build>;
|
return this._get(`/api/repos/${owner}/${repo}/builds/${number}`) as Promise<Build>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ export type Build = {
|
||||||
|
|
||||||
parent: number;
|
parent: number;
|
||||||
|
|
||||||
event: 'push' | 'tag' | 'pull_request' | 'deployment' | 'cron';
|
event: 'push' | 'tag' | 'pull_request' | 'deployment' | 'cron' | 'manual';
|
||||||
|
|
||||||
// The current status of the build.
|
// The current status of the build.
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
|
|
@ -4,4 +4,5 @@ export enum WebhookEvents {
|
||||||
PullRequest = 'pull_request',
|
PullRequest = 'pull_request',
|
||||||
Deploy = 'deployment',
|
Deploy = 'deployment',
|
||||||
Cron = 'cron',
|
Cron = 'cron',
|
||||||
|
Manual = 'manual',
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,21 @@
|
||||||
</a>
|
</a>
|
||||||
<IconButton v-if="repoPermissions.admin" class="ml-2" :to="{ name: 'repo-settings' }" icon="settings" />
|
<IconButton v-if="repoPermissions.admin" class="ml-2" :to="{ name: 'repo-settings' }" icon="settings" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||||
|
<Tabs v-model="activeTab" disable-hash-mode class="mb-4">
|
||||||
|
<Tab id="activity" :title="$t('repo.activity')" />
|
||||||
|
<Tab id="branches" :title="$t('repo.branches')" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<Tabs v-model="activeTab" disable-hash-mode class="mb-4">
|
<Button
|
||||||
<Tab id="activity" :title="$t('repo.activity')" />
|
v-if="repoPermissions.push"
|
||||||
<Tab id="branches" :title="$t('repo.branches')" />
|
type="submit"
|
||||||
</Tabs>
|
:text="$t('repo.manual_pipeline.trigger')"
|
||||||
|
class="ml-auto"
|
||||||
|
@click="showManualPipelinePopup = true"
|
||||||
|
/>
|
||||||
|
<ManualPipelinePopup :open="showManualPipelinePopup" @close="showManualPipelinePopup = false" />
|
||||||
|
</div>
|
||||||
<router-view />
|
<router-view />
|
||||||
</FluidContainer>
|
</FluidContainer>
|
||||||
<router-view v-else-if="repo && repoPermissions" />
|
<router-view v-else-if="repo && repoPermissions" />
|
||||||
|
@ -42,6 +51,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||||
import Icon from '~/components/atomic/Icon.vue';
|
import Icon from '~/components/atomic/Icon.vue';
|
||||||
import IconButton from '~/components/atomic/IconButton.vue';
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||||
|
import ManualPipelinePopup from '~/components/layout/popups/ManualPipelinePopup.vue';
|
||||||
import Tab from '~/components/tabs/Tab.vue';
|
import Tab from '~/components/tabs/Tab.vue';
|
||||||
import Tabs from '~/components/tabs/Tabs.vue';
|
import Tabs from '~/components/tabs/Tabs.vue';
|
||||||
import useApiClient from '~/compositions/useApiClient';
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
|
@ -53,15 +63,11 @@ import BuildStore from '~/store/builds';
|
||||||
import RepoStore from '~/store/repos';
|
import RepoStore from '~/store/repos';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// used by toRef
|
|
||||||
// eslint-disable-next-line vue/no-unused-properties
|
|
||||||
repoOwner: {
|
repoOwner: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// used by toRef
|
|
||||||
// eslint-disable-next-line vue/no-unused-properties
|
|
||||||
repoName: {
|
repoName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -87,6 +93,8 @@ provide('repo', repo);
|
||||||
provide('repo-permissions', repoPermissions);
|
provide('repo-permissions', repoPermissions);
|
||||||
provide('builds', builds);
|
provide('builds', builds);
|
||||||
|
|
||||||
|
const showManualPipelinePopup = ref(false);
|
||||||
|
|
||||||
async function loadRepo() {
|
async function loadRepo() {
|
||||||
repoPermissions.value = await apiClient.getRepoPermissions(repoOwner.value, repoName.value);
|
repoPermissions.value = await apiClient.getRepoPermissions(repoOwner.value, repoName.value);
|
||||||
if (!repoPermissions.value.pull) {
|
if (!repoPermissions.value.pull) {
|
||||||
|
|
1156
web/yarn.lock
1156
web/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -215,6 +215,13 @@ func (c *client) BuildList(owner, name string) ([]*Build, error) {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *client) BuildCreate(owner, name string, options *BuildOptions) (*Build, error) {
|
||||||
|
var out *Build
|
||||||
|
uri := fmt.Sprintf(pathBuilds, c.addr, owner, name)
|
||||||
|
err := c.post(uri, options, &out)
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
// BuildQueue returns a list of enqueued builds.
|
// BuildQueue returns a list of enqueued builds.
|
||||||
func (c *client) BuildQueue() ([]*Activity, error) {
|
func (c *client) BuildQueue() ([]*Activity, error) {
|
||||||
var out []*Activity
|
var out []*Activity
|
||||||
|
|
|
@ -70,6 +70,9 @@ type Client interface {
|
||||||
// the specified repository.
|
// the specified repository.
|
||||||
BuildList(string, string) ([]*Build, error)
|
BuildList(string, string) ([]*Build, error)
|
||||||
|
|
||||||
|
// BuildCreate returns creates a build on specified branch.
|
||||||
|
BuildCreate(string, string, *BuildOptions) (*Build, error)
|
||||||
|
|
||||||
// BuildQueue returns a list of enqueued builds.
|
// BuildQueue returns a list of enqueued builds.
|
||||||
BuildQueue() ([]*Activity, error)
|
BuildQueue() ([]*Activity, error)
|
||||||
|
|
||||||
|
|
|
@ -175,4 +175,10 @@ type (
|
||||||
Created int64 `json:"created_at"`
|
Created int64 `json:"created_at"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildOptions is the JSON data for forging a new build
|
||||||
|
BuildOptions struct {
|
||||||
|
Branch string `json:"branch"`
|
||||||
|
Variables map[string]string `json:"variables"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue