This commit is contained in:
张臻 2020-09-25 20:05:25 +08:00
parent b5d77dce93
commit eec45897b8
22 changed files with 849 additions and 164 deletions

View File

@ -2,12 +2,16 @@ package controller
import (
"database/sql"
"errors"
"github.com/zhenorzz/goploy/core"
"github.com/zhenorzz/goploy/model"
"github.com/zhenorzz/goploy/service"
"github.com/zhenorzz/goploy/utils"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
@ -16,7 +20,7 @@ import (
type Deploy Controller
// GetList -
func (deploy Deploy) GetList(gp *core.Goploy) *core.Response {
func (Deploy) GetList(gp *core.Goploy) *core.Response {
type RespData struct {
Project model.Projects `json:"list"`
}
@ -34,7 +38,7 @@ func (deploy Deploy) GetList(gp *core.Goploy) *core.Response {
}
// GetPreview deploy detail
func (deploy Deploy) GetPreview(gp *core.Goploy) *core.Response {
func (Deploy) GetPreview(gp *core.Goploy) *core.Response {
type RespData struct {
GitTraceList model.PublishTraces `json:"gitTraceList"`
Pagination model.Pagination `json:"pagination"`
@ -70,7 +74,7 @@ func (deploy Deploy) GetPreview(gp *core.Goploy) *core.Response {
}
// GetDetail deploy detail
func (deploy Deploy) GetDetail(gp *core.Goploy) *core.Response {
func (Deploy) GetDetail(gp *core.Goploy) *core.Response {
type RespData struct {
PublishTraceList model.PublishTraces `json:"publishTraceList"`
}
@ -87,7 +91,7 @@ func (deploy Deploy) GetDetail(gp *core.Goploy) *core.Response {
}
// GetCommitList get latest 10 commit list
func (deploy Deploy) GetCommitList(gp *core.Goploy) *core.Response {
func (Deploy) GetCommitList(gp *core.Goploy) *core.Response {
type RespData struct {
CommitList []utils.Commit `json:"commitList"`
}
@ -103,19 +107,19 @@ func (deploy Deploy) GetCommitList(gp *core.Goploy) *core.Response {
}
srcPath := core.GetProjectPath(project.ID)
git := utils.GIT{Dir: srcPath}
if err := git.Clean([]string{"-f"}); err != nil {
if err := git.Clean("-f"); err != nil {
return &core.Response{Code: core.Error, Message: err.Error() + ", detail: " + git.Err.String()}
}
if err := git.Checkout([]string{"--", "."}); err != nil {
if err := git.Checkout("--", "."); err != nil {
return &core.Response{Code: core.Error, Message: err.Error() + ", detail: " + git.Err.String()}
}
if err := git.Pull([]string{}); err != nil {
if err := git.Pull(); err != nil {
return &core.Response{Code: core.Error, Message: err.Error() + ", detail: " + git.Err.String()}
}
if err := git.Log([]string{"--stat", "--pretty=format:`start`%H`%an`%at`%s`", "-n", "10"}); err != nil {
if err := git.Log("--stat", "--pretty=format:`start`%H`%an`%at`%s`", "-n", "10"); err != nil {
return &core.Response{Code: core.Error, Message: err.Error() + ", detail: " + git.Err.String()}
}
@ -125,7 +129,7 @@ func (deploy Deploy) GetCommitList(gp *core.Goploy) *core.Response {
}
// Publish the project
func (deploy Deploy) Publish(gp *core.Goploy) *core.Response {
func (Deploy) Publish(gp *core.Goploy) *core.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"gt=0"`
Commit string `json:"commit"`
@ -134,41 +138,65 @@ func (deploy Deploy) Publish(gp *core.Goploy) *core.Response {
if err := verify(gp.Body, &reqData); err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
var err error
project, err := model.Project{ID: reqData.ProjectID}.GetData()
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
if project.DeployState == model.ProjectDeploying {
return &core.Response{Code: core.Deny, Message: "Project is being build by other"}
if project.Review == model.Enable && gp.Namespace.Role == core.RoleMember {
err = projectReview(gp, project, reqData.Commit)
} else {
err = projectDeploy(gp, project, reqData.Commit)
}
projectServers, err := model.ProjectServer{ProjectID: reqData.ProjectID}.GetBindServerListByProjectID()
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
project.PublisherID = gp.UserInfo.ID
project.PublisherName = gp.UserInfo.Name
project.DeployState = model.ProjectDeploying
project.LastPublishToken = uuid.New().String()
err = project.Publish()
return &core.Response{}
}
func (Deploy) Review(gp *core.Goploy) *core.Response {
type ReqData struct {
ProjectReviewID int64 `json:"projectReviewId" validate:"gt=0"`
State uint8 `json:"state" validate:"gt=0"`
}
var reqData ReqData
if err := verify(gp.Body, &reqData); err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
projectReviewModel := model.ProjectReview{
ID: reqData.ProjectReviewID,
State: reqData.State,
Editor: gp.UserInfo.Name,
EditorID: gp.UserInfo.ID,
}
projectReview, err := projectReviewModel.GetData()
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
go service.Sync{
UserInfo: gp.UserInfo,
Project: project,
ProjectServers: projectServers,
CommitID: reqData.Commit,
}.Exec()
return &core.Response{Message: "deploying"}
if projectReview.State != model.PENDING {
return &core.Response{Code: core.Error, Message: "Project review state is invalid"}
}
// Webhook connect
func (deploy Deploy) Webhook(gp *core.Goploy) *core.Response {
if reqData.State == model.APPROVE {
project, err := model.Project{ID: projectReview.ProjectID}.GetData()
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
if err := projectDeploy(gp, project, projectReview.CommitID); err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
}
projectReviewModel.EditRow()
return &core.Response{}
}
// Webhook -
func (Deploy) Webhook(gp *core.Goploy) *core.Response {
projectID, err := strconv.ParseInt(gp.URLQuery.Get("project_id"), 10, 64)
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
@ -228,3 +256,109 @@ func (deploy Deploy) Webhook(gp *core.Goploy) *core.Response {
}.Exec()
return &core.Response{Message: "receive push signal"}
}
// Callback -
func (Deploy) Callback(gp *core.Goploy) *core.Response {
projectReviewID, err := strconv.ParseInt(gp.URLQuery.Get("project_review_id"), 10, 64)
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
projectReviewModel := model.ProjectReview{
ID: projectReviewID,
State: model.APPROVE,
Editor: "admin",
EditorID: 1,
}
projectReview, err := projectReviewModel.GetData()
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
if projectReview.State != model.PENDING {
return &core.Response{Code: core.Error, Message: "Project review state is invalid"}
}
project, err := model.Project{ID: projectReview.ProjectID}.GetData()
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
if err := projectDeploy(gp, project, projectReview.CommitID); err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
projectReviewModel.EditRow()
return &core.Response{}
}
func projectDeploy(gp *core.Goploy, project model.Project, commitID string) error {
if project.DeployState == model.ProjectDeploying {
return errors.New("project is being build by other")
}
projectServers, err := model.ProjectServer{ProjectID: project.ID}.GetBindServerListByProjectID()
if err != nil {
return err
}
project.PublisherID = gp.UserInfo.ID
project.PublisherName = gp.UserInfo.Name
project.DeployState = model.ProjectDeploying
project.LastPublishToken = uuid.New().String()
err = project.Publish()
if err != nil {
return err
}
go service.Sync{
UserInfo: gp.UserInfo,
Project: project,
ProjectServers: projectServers,
CommitID: commitID,
}.Exec()
return nil
}
func projectReview(gp *core.Goploy, project model.Project, commitID string) error {
if len(commitID) == 0 {
return errors.New("commit id is required")
}
projectReviewModel := model.ProjectReview{
ProjectID: project.ID,
CommitID: commitID,
Creator: gp.UserInfo.Name,
CreatorID: gp.UserInfo.ID,
}
reviewURL := project.ReviewURL
if len(reviewURL) > 0 {
reviewURL = strings.Replace(reviewURL, "__PROJECT_ID__", strconv.FormatInt(project.ID, 10), 1)
reviewURL = strings.Replace(reviewURL, "__PROJECT_NAME__", project.Name, 1)
reviewURL = strings.Replace(reviewURL, "__BRANCH__", project.Branch, 1)
reviewURL = strings.Replace(reviewURL, "__ENVIRONMENT__", strconv.Itoa(int(project.Environment)), 1)
reviewURL = strings.Replace(reviewURL, "__COMMIT_ID__", commitID, 1)
reviewURL = strings.Replace(reviewURL, "__PUBLISH_TIME__", strconv.FormatInt(time.Now().Unix(), 10), 1)
reviewURL = strings.Replace(reviewURL, "__PUBLISHER_ID__", gp.UserInfo.Name, 1)
reviewURL = strings.Replace(reviewURL, "__PUBLISHER_NAME__", strconv.FormatInt(gp.UserInfo.ID, 10), 1)
projectReviewModel.ReviewURL = reviewURL
}
id, err := projectReviewModel.AddRow()
if err != nil {
return err
}
if len(reviewURL) > 0 {
callback := "http://"
if gp.Request.TLS != nil {
callback = "https://"
}
callback += gp.Request.Host + "/deploy/callback?project_review_id=" + strconv.FormatInt(id, 10)
callback = url.QueryEscape(callback)
reviewURL = strings.Replace(reviewURL, "__CALLBACK__", callback, 1)
resp, err := http.Get(reviewURL)
if err != nil {
return err
}
defer resp.Body.Close()
}
return nil
}

View File

@ -114,6 +114,8 @@ func (project Project) Add(gp *core.Goploy) *core.Response {
Environment uint8 `json:"Environment" validate:"required"`
Branch string `json:"branch" validate:"required"`
SymlinkPath string `json:"symlinkPath"`
Review uint8 `json:"review"`
ReviewURL string `json:"reviewURL"`
AfterPullScriptMode string `json:"afterPullScriptMode"`
AfterPullScript string `json:"afterPullScript"`
AfterDeployScriptMode string `json:"afterDeployScriptMode"`
@ -138,9 +140,11 @@ func (project Project) Add(gp *core.Goploy) *core.Response {
Name: reqData.Name,
URL: reqData.URL,
Path: reqData.Path,
SymlinkPath: reqData.SymlinkPath,
Environment: reqData.Environment,
Branch: reqData.Branch,
SymlinkPath: reqData.SymlinkPath,
Review: reqData.Review,
ReviewURL: reqData.ReviewURL,
AfterPullScriptMode: reqData.AfterPullScriptMode,
AfterPullScript: reqData.AfterPullScript,
AfterDeployScriptMode: reqData.AfterDeployScriptMode,
@ -201,6 +205,8 @@ func (project Project) Edit(gp *core.Goploy) *core.Response {
URL string `json:"url"`
Path string `json:"path"`
SymlinkPath string `json:"symlinkPath"`
Review uint8 `json:"review"`
ReviewURL string `json:"reviewURL"`
Environment uint8 `json:"Environment"`
Branch string `json:"branch"`
AfterPullScriptMode string `json:"afterPullScriptMode"`
@ -230,9 +236,11 @@ func (project Project) Edit(gp *core.Goploy) *core.Response {
Name: reqData.Name,
URL: reqData.URL,
Path: reqData.Path,
SymlinkPath: reqData.SymlinkPath,
Environment: reqData.Environment,
Branch: reqData.Branch,
SymlinkPath: reqData.SymlinkPath,
Review: reqData.Review,
ReviewURL: reqData.ReviewURL,
AfterPullScriptMode: reqData.AfterPullScriptMode,
AfterPullScript: reqData.AfterPullScript,
AfterDeployScriptMode: reqData.AfterDeployScriptMode,
@ -413,7 +421,7 @@ func (project Project) RemoveUser(gp *core.Goploy) *core.Response {
// GetTaskList -
func (project Project) GetTaskList(gp *core.Goploy) *core.Response {
type RespData struct {
ProjectTask model.ProjectTasks `json:"projectTaskList"`
ProjectTasks model.ProjectTasks `json:"list"`
Pagination model.Pagination `json:"pagination"`
}
pagination, err := model.PaginationFrom(gp.URLQuery)
@ -429,7 +437,29 @@ func (project Project) GetTaskList(gp *core.Goploy) *core.Response {
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
return &core.Response{Data: RespData{ProjectTask: projectTaskList, Pagination: pagination}}
return &core.Response{Data: RespData{ProjectTasks: projectTaskList, Pagination: pagination}}
}
// GetReviewList -
func (project Project) GetReviewList(gp *core.Goploy) *core.Response {
type RespData struct {
ProjectReviews model.ProjectReviews `json:"list"`
Pagination model.Pagination `json:"pagination"`
}
pagination, err := model.PaginationFrom(gp.URLQuery)
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
id, err := strconv.ParseInt(gp.URLQuery.Get("id"), 10, 64)
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
ProjectReviews, pagination, err := model.ProjectReview{ProjectID: id}.GetListByProjectID(pagination)
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
return &core.Response{Data: RespData{ProjectReviews: ProjectReviews, Pagination: pagination}}
}
// AddTask to project
@ -454,7 +484,6 @@ func (project Project) AddTask(gp *core.Goploy) *core.Response {
if err != nil {
return &core.Response{Code: core.Error, Message: err.Error()}
}
type RespData struct {
ID int64 `json:"id"`

View File

@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS `goploy`.`log` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`type` tinyint(3) UNSIGNED NOT NULL DEFAULT 1 COMMENT 'log type',
`ip` int(10) UNSIGNED NOT NULL DEFAULT 0,
`desc` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'description',
`desc` varchar(30) NOT NULL DEFAULT '' COMMENT 'description',
`user_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '',
`create_time` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '',
PRIMARY KEY (`id`) USING BTREE,
@ -14,25 +14,27 @@ CREATE TABLE IF NOT EXISTS `goploy`.`log` (
CREATE TABLE IF NOT EXISTS `goploy`.`project` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`namespace_id` int(10) UNSIGNED NOT NULL DEFAULT 0,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'project name',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'repository url',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'project deploy path',
`symlink_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '(ln -sfn symlink_path/uuid project_path)',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT 'project name',
`url` varchar(255) NOT NULL DEFAULT '' COMMENT 'repository url',
`path` varchar(255) NOT NULL DEFAULT '' COMMENT 'project deploy path',
`symlink_path` varchar(255) NOT NULL DEFAULT '' COMMENT '(ln -sfn symlink_path/uuid project_path)',
`environment` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '1.production 2.pre-release 3.test 4.development',
`branch` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'master' COMMENT 'repository branch',
`after_pull_script_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'sh|php|py|...',
`after_pull_script` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '',
`after_deploy_script_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'sh|php|py|...',
`after_deploy_script` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '',
`rsync_option` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'rsync options',
`branch` varchar(255) NOT NULL DEFAULT 'master' COMMENT 'repository branch',
`review` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '0.disable 1.enable',
`review_url` varchar(1000) NOT NULL DEFAULT '' COMMENT 'review notification link',
`after_pull_script_mode` varchar(20) NOT NULL DEFAULT '' COMMENT 'sh|php|py|...',
`after_pull_script` text NOT NULL COMMENT '',
`after_deploy_script_mode` varchar(20) NOT NULL DEFAULT '' COMMENT 'sh|php|py|...',
`after_deploy_script` text NOT NULL COMMENT '',
`rsync_option` varchar(255) NOT NULL DEFAULT '' COMMENT 'rsync options',
`auto_deploy` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '0.disable 1.webhook',
`state` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '0.disable 1.enable',
`deploy_state` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '0.not deploy 1.deploying 2.success 3.fail',
`publisher_id` int(10) UNSIGNED NOT NULL DEFAULT 0,
`publisher_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`publisher_name` varchar(255) NOT NULL DEFAULT '',
`last_publish_token` char(36) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
`notify_type` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '1.weixin 2.ding talk 3.feishu 255.custom',
`notify_target` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '',
`notify_target` varchar(255) NOT NULL DEFAULT '' COMMENT '',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
@ -75,6 +77,21 @@ CREATE TABLE IF NOT EXISTS `goploy`.`project_task` (
KEY `index_project_update` (`project_id`,`update_time`) USING BTREE COMMENT 'project_id,update_time'
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `goploy`.`project_review` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`project_id` int(10) unsigned NOT NULL DEFAULT '0',
`commit_id` char(40) NOT NULL DEFAULT '',
`review_url` varchar(1000) NOT NULL DEFAULT '' COMMENT 'review notification link',
`state` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '0.Pending 1.Approve 2.Deny',
`creator_id` int(10) unsigned NOT NULL DEFAULT '0',
`creator` varchar(255) NOT NULL DEFAULT '',
`editor_id` int(10) unsigned NOT NULL DEFAULT '0',
`editor` varchar(255) NOT NULL DEFAULT '',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `goploy`.`publish_trace` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`token` char(36) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
@ -96,17 +113,17 @@ CREATE TABLE IF NOT EXISTS `goploy`.`publish_trace` (
CREATE TABLE IF NOT EXISTS `goploy`.`monitor` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`namespace_id` int(10) unsigned NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`domain` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(255) NOT NULL,
`domain` varchar(50) NOT NULL,
`port` smallint(5) unsigned NOT NULL DEFAULT '80',
`second` int(10) unsigned NOT NULL DEFAULT '1' COMMENT 'How many seconds to run',
`times` smallint(5) unsigned NOT NULL DEFAULT '1' COMMENT 'How many times of failures',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`description` varchar(255) NOT NULL DEFAULT '',
`notify_type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '1.weixin 2.ding talk 3.feishu 255.custom',
`notify_target` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`notify_target` varchar(255) NOT NULL DEFAULT '',
`notify_times` smallint(5) unsigned NOT NULL DEFAULT '1' COMMENT 'Notify times',
`state` tinyint(4) unsigned NOT NULL DEFAULT '1' COMMENT '0.disable 1.enable',
`error_content` text COLLATE utf8mb4_general_ci NOT NULL,
`error_content` varchar(1000) NOT NULL DEFAULT '',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
@ -115,12 +132,12 @@ CREATE TABLE IF NOT EXISTS `goploy`.`monitor` (
CREATE TABLE IF NOT EXISTS `goploy`.`server` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`namespace_id` int(10) UNSIGNED NOT NULL DEFAULT 0,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`name` varchar(255) NOT NULL DEFAULT '',
`ip` varchar(15) NOT NULL DEFAULT '',
`port` smallint(10) UNSIGNED NOT NULL DEFAULT 22,
`owner` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`last_publish_token` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`owner` varchar(255) NOT NULL DEFAULT '',
`description` varchar(255) NOT NULL DEFAULT '',
`last_publish_token` char(36) NOT NULL DEFAULT '',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`state` tinyint(4) UNSIGNED NOT NULL DEFAULT 1 COMMENT '0.disable 1.enable',
@ -131,12 +148,12 @@ CREATE TABLE IF NOT EXISTS `goploy`.`server` (
CREATE TABLE IF NOT EXISTS `goploy`.`crontab` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`namespace_id` int(10) unsigned NOT NULL DEFAULT 0,
`command` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`command_md5` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'command md5 for replace',
`command` varchar(255) NOT NULL DEFAULT '',
`command_md5` char(32) NOT NULL DEFAULT '' COMMENT 'command md5 for replace',
`creator_id` int(10) unsigned NOT NULL DEFAULT '0',
`creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`creator` varchar(255) NOT NULL DEFAULT '',
`editor_id` int(10) unsigned NOT NULL DEFAULT '0',
`editor` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`editor` varchar(255) NOT NULL DEFAULT '',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
@ -155,10 +172,10 @@ CREATE TABLE IF NOT EXISTS `goploy`.`crontab_server` (
CREATE TABLE IF NOT EXISTS `goploy`.`template` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`package_id_str` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`script` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`name` varchar(255) NOT NULL DEFAULT '',
`package_id_str` text NOT NULL,
`script` text NOT NULL,
`remark` varchar(255) NOT NULL DEFAULT '',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
@ -166,7 +183,7 @@ CREATE TABLE IF NOT EXISTS `goploy`.`template` (
CREATE TABLE IF NOT EXISTS `goploy`.`package` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`name` varchar(255) NOT NULL DEFAULT '',
`size` int(10) unsigned NOT NULL DEFAULT '0',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@ -175,13 +192,13 @@ CREATE TABLE IF NOT EXISTS `goploy`.`package` (
CREATE TABLE IF NOT EXISTS `goploy`.`install_trace` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`token` char(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`token` char(36) NOT NULL DEFAULT '',
`server_id` int(10) unsigned NOT NULL DEFAULT '0',
`server_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`server_name` varchar(255) NOT NULL DEFAULT '',
`detail` longtext NOT NULL,
`state` tinyint(4) unsigned NOT NULL DEFAULT '1',
`operator_id` int(10) unsigned NOT NULL DEFAULT '0',
`operator_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`operator_name` varchar(255) NOT NULL DEFAULT '',
`type` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '1.rsync 2.ssh 3.script',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@ -192,10 +209,10 @@ CREATE TABLE IF NOT EXISTS `goploy`.`install_trace` (
CREATE TABLE IF NOT EXISTS `goploy`.`user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`account` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`password` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`contact` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`account` varchar(30) NOT NULL DEFAULT '',
`password` varchar(60) NOT NULL DEFAULT '',
`name` varchar(30) NOT NULL DEFAULT '',
`contact` varchar(255) NOT NULL DEFAULT '',
`state` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0.disable 1.enable',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@ -206,7 +223,7 @@ CREATE TABLE IF NOT EXISTS `goploy`.`user` (
CREATE TABLE IF NOT EXISTS `goploy`.`namespace` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`name` varchar(20) NOT NULL DEFAULT '',
`insert_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,

16
main.go
View File

@ -2,6 +2,7 @@ package main
import (
"bufio"
"bytes"
"database/sql"
"fmt"
"github.com/joho/godotenv"
@ -14,6 +15,7 @@ import (
"log"
"net/http"
"os"
"os/exec"
"time"
_ "github.com/go-sql-driver/mysql"
@ -49,6 +51,20 @@ func install() {
println("The configuration file already exists, no need to reinstall (if you need to reinstall, please back up the database goploy first, delete the .env file)")
return
}
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("rsync", "--version")
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
println(err.Error() + ", detail: " + stderr.String())
panic("Please check if rsync is installed correctly, see https://rsync.samba.org/download.html")
}
git := utils.GIT{}
if err := git.Run("--version"); err != nil {
println(err.Error() + ", detail: " + git.Err.String())
panic("Please check if git is installed correctly, see https://git-scm.com/downloads")
}
inputReader := bufio.NewReader(os.Stdin)
println("Installation guidelines (Enter to confirm input)")
println("Please enter the mysql user:")

File diff suppressed because one or more lines are too long

View File

@ -148,8 +148,8 @@ func (m Monitor) GetAllByState() (Monitors, error) {
func (m Monitor) AddRow() (int64, error) {
result, err := sq.
Insert(monitorTable).
Columns("namespace_id", "name", "domain", "port", "second", "times", "notify_type", "notify_target", "notify_times", "description").
Values(m.NamespaceID, m.Name, m.Domain, m.Port, m.Second, m.Times, m.NotifyType, m.NotifyTarget, m.NotifyTimes, m.Description).
Columns("namespace_id", "name", "domain", "port", "second", "times", "notify_type", "notify_target", "notify_times", "description", "error_content").
Values(m.NamespaceID, m.Name, m.Domain, m.Port, m.Second, m.Times, m.NotifyType, m.NotifyTarget, m.NotifyTimes, m.Description, "").
RunWith(DB).
Exec()
if err != nil {

View File

@ -15,9 +15,11 @@ type Project struct {
Name string `json:"name"`
URL string `json:"url"`
Path string `json:"path"`
SymlinkPath string `json:"symlinkPath"`
Environment uint8 `json:"environment"`
Branch string `json:"branch"`
SymlinkPath string `json:"symlinkPath"`
Review uint8 `json:"review"`
ReviewURL string `json:"reviewURL"`
AfterPullScriptMode string `json:"afterPullScriptMode"`
AfterPullScript string `json:"afterPullScript"`
AfterDeployScriptMode string `json:"afterDeployScriptMode"`
@ -65,8 +67,42 @@ type Projects []Project
func (p Project) AddRow() (int64, error) {
result, err := sq.
Insert(projectTable).
Columns("namespace_id", "name", "url", "path", "symlink_path", "environment", "branch", "after_pull_script_mode", "after_pull_script", "after_deploy_script_mode", "after_deploy_script", "rsync_option", "notify_type", "notify_target").
Values(p.NamespaceID, p.Name, p.URL, p.Path, p.SymlinkPath, p.Environment, p.Branch, p.AfterPullScriptMode, p.AfterPullScript, p.AfterDeployScriptMode, p.AfterDeployScript, p.RsyncOption, p.NotifyType, p.NotifyTarget).
Columns(
"namespace_id",
"name",
"url",
"path",
"environment",
"branch",
"symlink_path",
"review",
"review_url",
"after_pull_script_mode",
"after_pull_script",
"after_deploy_script_mode",
"after_deploy_script",
"rsync_option",
"notify_type",
"notify_target",
).
Values(
p.NamespaceID,
p.Name,
p.URL,
p.Path,
p.Environment,
p.Branch,
p.SymlinkPath,
p.Review,
p.ReviewURL,
p.AfterPullScriptMode,
p.AfterPullScript,
p.AfterDeployScriptMode,
p.AfterDeployScript,
p.RsyncOption,
p.NotifyType,
p.NotifyTarget,
).
RunWith(DB).
Exec()
if err != nil {
@ -84,9 +120,11 @@ func (p Project) EditRow() error {
"name": p.Name,
"url": p.URL,
"path": p.Path,
"symlink_path": p.SymlinkPath,
"environment": p.Environment,
"branch": p.Branch,
"symlink_path": p.SymlinkPath,
"review": p.Review,
"review_url": p.ReviewURL,
"after_pull_script_mode": p.AfterPullScriptMode,
"after_pull_script": p.AfterPullScript,
"after_deploy_script_mode": p.AfterDeployScriptMode,
@ -175,7 +213,27 @@ func (p Project) DeployFail() error {
// GetList -
func (p Project) GetList(pagination Pagination) (Projects, error) {
builder := sq.
Select("project.id, name, url, path, symlink_path, environment, branch, after_pull_script_mode, after_pull_script, after_deploy_script_mode, after_deploy_script, rsync_option, auto_deploy, notify_type, notify_target, project.insert_time, project.update_time").
Select(`
project.id,
name,
url,
path,
environment,
branch,
symlink_path,
review,
review_url,
after_pull_script_mode,
after_pull_script,
after_deploy_script_mode,
after_deploy_script,
rsync_option,
auto_deploy,
notify_type,
notify_target,
project.insert_time,
project.update_time
`).
From(projectTable).
Join(projectUserTable + " ON project_user.project_id = project.id").
Where(sq.Eq{
@ -206,9 +264,11 @@ func (p Project) GetList(pagination Pagination) (Projects, error) {
&project.Name,
&project.URL,
&project.Path,
&project.SymlinkPath,
&project.Environment,
&project.Branch,
&project.SymlinkPath,
&project.Review,
&project.ReviewURL,
&project.AfterPullScriptMode,
&project.AfterPullScript,
&project.AfterDeployScriptMode,
@ -265,6 +325,7 @@ func (p Project) GetUserProjectList() (Projects, error) {
IFNULL(publish_trace.ext, '{}'),
project.environment,
project.branch,
project.review,
project.last_publish_token,
project.deploy_state,
project.update_time`).
@ -299,6 +360,7 @@ func (p Project) GetUserProjectList() (Projects, error) {
&project.PublishExt,
&project.Environment,
&project.Branch,
&project.Review,
&project.LastPublishToken,
&project.DeployState,
&project.UpdateTime); err != nil {
@ -315,7 +377,7 @@ func (p Project) GetUserProjectList() (Projects, error) {
func (p Project) GetData() (Project, error) {
var project Project
err := sq.
Select("id, namespace_id, name, url, path, symlink_path, environment, branch, after_pull_script_mode, after_pull_script, after_deploy_script_mode, after_deploy_script, rsync_option, auto_deploy, deploy_state, notify_type, notify_target, insert_time, update_time").
Select("id, namespace_id, name, url, path, environment, branch, symlink_path, review, review_url, after_pull_script_mode, after_pull_script, after_deploy_script_mode, after_deploy_script, rsync_option, auto_deploy, deploy_state, notify_type, notify_target, insert_time, update_time").
From(projectTable).
Where(sq.Eq{"id": p.ID}).
RunWith(DB).
@ -326,9 +388,11 @@ func (p Project) GetData() (Project, error) {
&project.Name,
&project.URL,
&project.Path,
&project.SymlinkPath,
&project.Environment,
&project.Branch,
&project.SymlinkPath,
&project.Review,
&project.ReviewURL,
&project.AfterPullScriptMode,
&project.AfterPullScript,
&project.AfterDeployScriptMode,

125
model/ProjectReviewModel.go Normal file
View File

@ -0,0 +1,125 @@
package model
import (
sq "github.com/Masterminds/squirrel"
)
const projectReviewTable = "`project_review`"
// ProjectReview -
type ProjectReview struct {
ID int64 `json:"id"`
ProjectID int64 `json:"projectId"`
CommitID string `json:"commitId"`
ReviewURL string `json:"reviewURL"`
State uint8 `json:"state"`
Creator string `json:"creator"`
CreatorID int64 `json:"creatorId"`
Editor string `json:"editor"`
EditorID int64 `json:"editorId"`
InsertTime string `json:"insertTime"`
UpdateTime string `json:"updateTime"`
}
// ProjectReviews -
type ProjectReviews []ProjectReview
// GetListByProjectID -
func (pr ProjectReview) GetListByProjectID(pagination Pagination) (ProjectReviews, Pagination, error) {
rows, err := sq.
Select("id, project_id, commit_id, state, creator, creator_id, editor, editor_id, insert_time, update_time").
From(projectReviewTable).
Where(sq.Eq{"project_id": pr.ProjectID}).
Limit(pagination.Rows).
Offset((pagination.Page - 1) * pagination.Rows).
OrderBy("id DESC").
RunWith(DB).
Query()
if err != nil {
return nil, pagination, err
}
projectReviews := ProjectReviews{}
for rows.Next() {
var projectReview ProjectReview
if err := rows.Scan(
&projectReview.ID,
&projectReview.ProjectID,
&projectReview.CommitID,
&projectReview.State,
&projectReview.Creator,
&projectReview.CreatorID,
&projectReview.Editor,
&projectReview.EditorID,
&projectReview.InsertTime,
&projectReview.UpdateTime,
); err != nil {
return projectReviews, pagination, err
}
projectReviews = append(projectReviews, projectReview)
}
err = sq.
Select("COUNT(*) AS count").
From(projectReviewTable).
Where(sq.Eq{"project_id": pr.ProjectID}).
RunWith(DB).
QueryRow().
Scan(&pagination.Total)
if err != nil {
return projectReviews, pagination, err
}
return projectReviews, pagination, nil
}
// GetData -
func (pr ProjectReview) GetData() (ProjectReview, error) {
var projectReview ProjectReview
err := sq.
Select("id, project_id, commit_id, state, insert_time, update_time").
From(projectReviewTable).
Where(sq.Eq{"id": pr.ID}).
RunWith(DB).
QueryRow().
Scan(
&projectReview.ID,
&projectReview.ProjectID,
&projectReview.CommitID,
&projectReview.State,
&projectReview.InsertTime,
&projectReview.UpdateTime)
if err != nil {
return projectReview, err
}
return projectReview, nil
}
// AddRow -
func (pr ProjectReview) AddRow() (int64, error) {
result, err := sq.
Insert(projectReviewTable).
Columns("project_id", "commit_id", "review_url", "creator", "creator_id").
Values(pr.ProjectID, pr.CommitID, pr.ReviewURL, pr.Creator, pr.CreatorID).
RunWith(DB).
Exec()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
return id, err
}
// EditRow -
func (pr ProjectReview) EditRow() error {
_, err := sq.
Update(projectReviewTable).
SetMap(sq.Eq{
"state": pr.State,
"editor": pr.Editor,
"editor_id": pr.EditorID,
}).
Where(sq.Eq{"id": pr.ID}).
RunWith(DB).
Exec()
return err
}

View File

@ -64,7 +64,7 @@ func (pt ProjectTask) GetListByProjectID(pagination Pagination) (ProjectTasks, P
&projectTask.InsertTime,
&projectTask.UpdateTime,
); err != nil {
return nil, pagination, err
return projectTasks, pagination, err
}
projectTasks = append(projectTasks, projectTask)
}
@ -76,7 +76,7 @@ func (pt ProjectTask) GetListByProjectID(pagination Pagination) (ProjectTasks, P
QueryRow().
Scan(&pagination.Total)
if err != nil {
return nil, pagination, err
return projectTasks, pagination, err
}
return projectTasks, pagination, nil
}

View File

@ -16,8 +16,8 @@ func Init() *router.Router {
// no need to check login
rt.RegisterWhiteList(map[string]struct{}{
"/user/login": {},
"/user/isShowPhrase": {},
"/deploy/webhook": {},
"/deploy/callback": {},
})
// websocket route
rt.Add("/ws/connect", http.MethodGet, ws.GetHub().Connect)
@ -61,6 +61,7 @@ func Init() *router.Router {
rt.Add("/project/editTask", http.MethodPost, controller.Project{}.EditTask).Roles([]string{core.RoleAdmin, core.RoleManager, core.RoleGroupManager})
rt.Add("/project/removeTask", http.MethodPost, controller.Project{}.RemoveTask).Roles([]string{core.RoleAdmin, core.RoleManager, core.RoleGroupManager})
rt.Add("/project/getTaskList", http.MethodGet, controller.Project{}.GetTaskList).Roles([]string{core.RoleAdmin, core.RoleManager, core.RoleGroupManager})
rt.Add("/project/getReviewList", http.MethodGet, controller.Project{}.GetReviewList)
// monitor route
rt.Add("/monitor/getList", http.MethodGet, controller.Monitor{}.GetList)
@ -76,8 +77,10 @@ func Init() *router.Router {
rt.Add("/deploy/getDetail", http.MethodGet, controller.Deploy{}.GetDetail)
rt.Add("/deploy/getCommitList", http.MethodGet, controller.Deploy{}.GetCommitList)
rt.Add("/deploy/getPreview", http.MethodGet, controller.Deploy{}.GetPreview)
rt.Add("/deploy/review", http.MethodPost, controller.Deploy{}.Review).Roles([]string{core.RoleAdmin, core.RoleManager, core.RoleGroupManager})
rt.Add("/deploy/publish", http.MethodPost, controller.Deploy{}.Publish, middleware.HasPublishAuth)
rt.Add("/deploy/webhook", http.MethodPost, controller.Deploy{}.Webhook, middleware.FilterEvent)
rt.Add("/deploy/callback", http.MethodGet, controller.Deploy{}.Callback)
// server route
rt.Add("/server/getList", http.MethodGet, controller.Server{}.GetList)

View File

@ -29,12 +29,12 @@ func (repository Repository) Create() error {
return err
}
git := utils.GIT{}
if err := git.Clone([]string{project.URL, srcPath}); err != nil {
if err := git.Clone(project.URL, srcPath); err != nil {
core.Log(core.ERROR, "The project fail to initialize, projectID:"+strconv.FormatInt(project.ID, 10)+" ,error: "+err.Error()+", detail: "+git.Err.String())
return err
}
if project.Branch != "master" {
if err := git.Checkout([]string{"-b", project.Branch, "origin/" + project.Branch}); err != nil {
if err := git.Checkout("-b", project.Branch, "origin/" + project.Branch); err != nil {
core.Log(core.ERROR, "The project fail to switch branch, projectID:"+strconv.FormatInt(project.ID, 10)+" ,error: "+err.Error()+", detail: "+git.Err.String())
os.RemoveAll(srcPath)
return err

View File

@ -210,7 +210,7 @@ func gitPull(project model.Project) error {
Message: ws.ProjectMessage{ProjectID: project.ID, ProjectName: project.Name, State: ws.GitClean, Message: "git clean"},
}
core.Log(core.TRACE, "projectID:"+strconv.FormatInt(project.ID, 10)+" git clean -f")
if err := git.Clean([]string{"-f"}); err != nil {
if err := git.Clean("-f"); err != nil {
core.Log(core.ERROR, err.Error()+", detail: "+git.Err.String())
return errors.New(git.Err.String())
}
@ -221,7 +221,7 @@ func gitPull(project model.Project) error {
Message: ws.ProjectMessage{ProjectID: project.ID, ProjectName: project.Name, State: ws.GitCheckout, Message: "git checkout"},
}
core.Log(core.TRACE, "projectID:"+strconv.FormatInt(project.ID, 10)+" git checkout -- .")
if err := git.Checkout([]string{"--", "."}); err != nil {
if err := git.Checkout("--", "."); err != nil {
core.Log(core.ERROR, err.Error()+", detail: "+git.Err.String())
return errors.New(git.Err.String())
}
@ -231,7 +231,7 @@ func gitPull(project model.Project) error {
Message: ws.ProjectMessage{ProjectID: project.ID, ProjectName: project.Name, State: ws.GitPull, Message: "git pull"},
}
core.Log(core.TRACE, "projectID:"+strconv.FormatInt(project.ID, 10)+" git pull")
if err := git.Pull([]string{}); err != nil {
if err := git.Pull(); err != nil {
core.Log(core.ERROR, err.Error()+", detail: "+git.Err.String())
return errors.New(git.Err.String())
}
@ -262,7 +262,7 @@ func gitReset(commit string, project model.Project) error {
func gitCommitLog(project model.Project) (utils.Commit, error) {
git := utils.GIT{Dir: core.GetProjectPath(project.ID)}
if err := git.Log([]string{"--stat", "--pretty=format:`start`%H`%an`%at`%s`", "-n", "1"}); err != nil {
if err := git.Log("--stat", "--pretty=format:`start`%H`%an`%at`%s`", "-n", "1"); err != nil {
core.Log(core.ERROR, err.Error()+", detail: "+git.Err.String())
return utils.Commit{}, errors.New(git.Err.String())
}

View File

@ -13,7 +13,7 @@ type GIT struct {
Err bytes.Buffer
}
func (git *GIT) Run(operator string,options []string) error {
func (git *GIT) Run(operator string, options ...string) error {
git.Output.Reset()
git.Err.Reset()
cmd := exec.Command("git", append([]string{operator}, options...)...)
@ -28,44 +28,41 @@ func (git *GIT) Run(operator string,options []string) error {
return nil
}
func (git *GIT) Clone(options []string) error {
if err := git.Run("clone", options); err != nil {
func (git *GIT) Clone(options ...string) error {
if err := git.Run("clone", options...); err != nil {
return err
}
return nil
}
func (git *GIT) Clean(options []string) error {
if err := git.Run("clean", options); err != nil {
func (git *GIT) Clean(options ...string) error {
if err := git.Run("clean", options...); err != nil {
return err
}
return nil
}
func (git *GIT) Checkout(options []string) error {
if err := git.Run("checkout", options); err != nil {
func (git *GIT) Checkout(options ...string) error {
if err := git.Run("checkout", options...); err != nil {
return err
}
return nil
}
func (git *GIT) Pull(options []string) error {
if err := git.Run("pull", options); err != nil {
func (git *GIT) Pull(options ...string) error {
if err := git.Run("pull", options...); err != nil {
return err
}
return nil
}
func (git *GIT) Log(options []string) error {
if err := git.Run("log", options); err != nil {
func (git *GIT) Log(options ...string) error {
if err := git.Run("log", options...); err != nil {
return err
}
return nil
}
type Commit struct {
Commit string `json:"commit"`
Author string `json:"author"`

View File

@ -1,3 +1,3 @@
ALTER TABLE `goploy`.`monitor`
ADD COLUMN `notify_times` smallint(5) UNSIGNED NOT NULL DEFAULT 1 AFTER `notify_target`,
ADD COLUMN `error_content` text NOT NULL AFTER `notify_times`;
ADD COLUMN `error_content` varchar(1000) NOT NULL DEFAULT '' AFTER `notify_times`;

3
v1.0.4.sql Normal file
View File

@ -0,0 +1,3 @@
ALTER TABLE `goploy`.`project`
ADD COLUMN `review` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '0.disable 1.enable' AFTER `symlink_path`,
ADD COLUMN `review_url` varchar(1000) NOT NULL DEFAULT '' COMMENT 'review notification link' AFTER `review`;

View File

@ -64,6 +64,18 @@ export function publish(projectId, commit) {
})
}
/**
* @param {int} id
* @return {Promise}
*/
export function review(projectReviewId, state) {
return request({
url: '/deploy/review',
method: 'post',
data: { projectReviewId, state }
})
}
/**
* @param {int} id
* @return {Promise}

View File

@ -175,3 +175,15 @@ export function getTaskList({ page, rows }, id) {
params: { page, rows, id }
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getReviewList({ page, rows }, id) {
return request({
url: '/project/getReviewList',
method: 'get',
params: { page, rows, id }
})
}

View File

@ -25,11 +25,15 @@ export default {
crontab: 'Crontab',
command: 'Command',
directory: 'Directory',
func: 'Func',
param: 'Param',
deploy: 'Deploy',
search: 'Search',
tips: 'Tips',
view: 'View',
detail: 'Detail',
review: 'Review',
reject: 'Reject',
manage: 'Manage',
interval: 'Interval',
desc: 'Description',
@ -38,7 +42,6 @@ export default {
branch: 'Branch',
setting: 'Setting',
baseSetting: 'Base setting',
advancedSetting: 'Advanced setting',
notice: 'Notice',
task: 'Task',
date: 'Date',
@ -52,9 +55,12 @@ export default {
creator: 'Creator',
editor: 'Editor',
op: 'Operation',
submit: 'Submit',
add: 'Add',
edit: 'Edit',
copy: 'Copy',
approve: 'Approve',
deny: 'Deny',
upload: 'Upload',
uploading: 'Uploading',
reUpload: 'Reupload',
@ -166,6 +172,10 @@ export default {
removeUserTips: `This action will delete the user's({userName}) binding relationship, continue?`
},
deployPage: {
taskDeploy: 'Crontab deploy',
reviewDeploy: 'Review deploy',
reviewTips: 'This action will approve commit, continue?',
reviewStateOption: ['Wait', 'Reviewed', 'Rejected'],
removeProjectTaskTips: 'This action will delete the crontab task in {projectName}, continue?',
rollbackTips: 'This action will rebuild {commit}, continue?'
}

View File

@ -25,11 +25,15 @@ export default {
crontab: '定时',
command: '命令',
directory: '目录',
func: '功能',
param: '参数',
deploy: '构建',
search: '搜索',
tips: '提示',
view: '查看',
detail: '详情',
review: '审核',
reject: '拒绝',
manage: '管理',
interval: '间隔',
desc: '描述',
@ -38,7 +42,6 @@ export default {
branch: '分支',
setting: '设置',
baseSetting: '基本配置',
advancedSetting: '高级配置',
notice: '通知',
task: '任务',
date: '日期',
@ -46,14 +49,18 @@ export default {
today: '今天',
m1d: '减一天',
p1d: '加一天',
time: '时间',
insertTime: '创建时间',
updateTime: '更新时间',
creator: '创建人',
editor: '修改人',
op: '操作',
submit: '提交',
add: '添加',
edit: '编辑',
copy: '复制',
approve: '同意',
deny: '拒绝',
upload: '上传',
uploading: '上传中',
reUpload: '重传',
@ -143,6 +150,15 @@ export default {
lishBranch: '列出分支',
scriptMode: '脚本类型',
deployNotice: '构建通知',
publishReview: '发布审核',
reviewFooterTips: `
<p>只有成员构建项目才会触发审核</p>
审核方式
<p>1. 前往构建发布页面进行审核</p>
<p>2. 推送到URL:http(s)://domain?custom-param=1&callback=***</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;http get callback的值即可完成审核</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;重复访问callback只会发布一次并且发布过不会再次发布</p>
`,
symlinkLabel: '软链部署(推荐)',
symlinkHeaderTips: `<p>项目先同步到指定目录(rsync 软链目录)然后ln -s 部署路径 软链目录</p>
<p>可以避免项目在同步传输文件的过程中外部访问到部分正在同步的文件</p>
@ -167,6 +183,10 @@ export default {
removeUserTips: '此操作将永久删除用户({userName})的绑定关系, 是否继续?'
},
deployPage: {
taskDeploy: '定时构建',
reviewDeploy: '审核构建',
reviewTips: '此操作将通过该次提交, 是否继续?',
reviewStateOption: ['待审', '已审', '拒审'],
removeProjectTaskTips: '此操作删除{projectName}的定时任务, 是否继续?',
rollbackTips: '此操作将重新构建{commit}, 是否继续?'
}

View File

@ -11,6 +11,9 @@ const mixin = {
},
hasGroupManagerPermission() {
return [this.$global.Admin, this.$global.Manager, this.$global.GroupManager].indexOf(getNamespace()['role']) !== -1
},
isMember() {
return this.$global.member === getNamespace()['role']
}
}
}

View File

@ -59,30 +59,32 @@
<template slot-scope="scope">
<el-row class="operation-btn">
<el-dropdown
v-if="hasGroupManagerPermission() || scope.row.review === 0"
split-button
trigger="click"
:disabled="scope.row.deployState === 1"
type="primary"
@click="publish(scope.row)"
@command="handlePublishCommand"
@command="(commandFunc) => commandFunc(scope.row)"
>
{{ $t('deploy') }}
{{ isMember() && scope.row.review === 1 ? $t('submit') : $t('deploy') }}
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="scope.row">List commit</el-dropdown-item>
<el-dropdown-item :command="getCommitList">Commit list</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button v-else type="primary" @click="getCommitList(scope.row)">{{ $t('deploy') }}</el-button>
<el-dropdown
v-if="hasGroupManagerPermission()"
split-button
trigger="click"
v-if="hasGroupManagerPermission() || scope.row.review === 1"
:disabled="scope.row.deployState === 1"
type="warning"
@click="handleAddProjectTask(scope.row)"
@command="handleProjectTaskCommand"
trigger="click"
@command="(commandFunc) => commandFunc(scope.row)"
>
{{ $t('crontab') }}
<el-button type="warning">
{{ $t('func') }}<i class="el-icon-arrow-down el-icon--right" />
</el-button>
<el-dropdown-menu slot="dropdown" style="min-width:84px;text-align:center;">
<el-dropdown-item :command="scope.row">{{ $t('manage') }}</el-dropdown-item>
<el-dropdown-item v-if="hasGroupManagerPermission()" :command="handleTaskCommand">{{ $t('deployPage.taskDeploy') }}</el-dropdown-item>
<el-dropdown-item v-if="scope.row.review === 1" :command="handleReviewCommand">{{ $t('deployPage.reviewDeploy') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button type="success" @click="handleDetail(scope.row)">{{ $t('detail') }}</el-button>
@ -262,6 +264,9 @@
</div>
</el-dialog>
<el-dialog :title="$t('manage')" :visible.sync="taskListDialogVisible">
<el-row class="app-bar" type="flex" justify="end">
<el-button type="primary" icon="el-icon-plus" @click="handleAddProjectTask(selectedItem)" />
</el-row>
<el-table
v-loading="taskTableLoading"
border
@ -272,7 +277,19 @@
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="projectName" :label="$t('projectName')" width="150" />
<el-table-column prop="commitId" label="commit" width="290" />
<el-table-column prop="commitId" label="commit" width="290">
<template slot-scope="scope">
<el-link
type="primary"
style="font-size: 12px"
:underline="false"
:href="parseGitURL(selectedItem.url) + '/commit/' + scope.row.commitId"
target="_blank"
>
{{ scope.row.commitId }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="date" :label="$t('date')" width="150" />
<el-table-column prop="isRun" :label="$t('task')" width="60">
<template slot-scope="scope">
@ -284,8 +301,8 @@
{{ $t(`stateOption[${scope.row.state}]`) }}
</template>
</el-table-column>
<el-table-column prop="creator" :label="$t('creator')" />
<el-table-column prop="editor" :label="$t('editor')" />
<el-table-column prop="creator" :label="$t('creator')" align="center" />
<el-table-column prop="editor" :label="$t('editor')" align="center" />
<el-table-column prop="insertTime" :label="$t('insertTime')" width="135" align="center" />
<el-table-column prop="updateTime" :label="$t('updateTime')" width="135" align="center" />
<el-table-column prop="operation" :label="$t('op')" width="150" align="center" fixed="right">
@ -295,10 +312,74 @@
</template>
</el-table-column>
</el-table>
<el-pagination
hide-on-single-page
:total="taskPagination.total"
:page-size="taskPagination.rows"
:current-page.sync="taskPagination.page"
style="margin-top:10px; text-align:right;"
background
layout="sizes, total, prev, pager, next"
@current-change="handleTaskPageChange"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="taskListDialogVisible = false">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
<el-dialog :title="$t('review')" :visible.sync="reviewListDialogVisible">
<el-table
v-loading="reviewTableLoading"
border
stripe
highlight-current-row
max-height="447px"
:data="reviewTableData"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="projectName" :label="$t('projectName')" width="150" />
<el-table-column prop="commitId" label="commit" width="290">
<template slot-scope="scope">
<el-link
type="primary"
style="font-size: 12px"
:underline="false"
:href="parseGitURL(selectedItem.url) + '/commit/' + scope.row.commitId"
target="_blank"
>
{{ scope.row.commitId }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="state" :label="$t('state')" width="50">
<template slot-scope="scope">
{{ $t(`deployPage.reviewStateOption[${scope.row.state}]`) }}
</template>
</el-table-column>
<el-table-column prop="creator" :label="$t('creator')" align="center" />
<el-table-column prop="editor" :label="$t('editor')" align="center" />
<el-table-column prop="insertTime" :label="$t('insertTime')" width="135" align="center" />
<el-table-column prop="updateTime" :label="$t('updateTime')" width="135" align="center" />
<el-table-column prop="operation" :label="$t('op')" width="150" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="success" :disabled="scope.row.state !== 0" @click="handleProjectReview(scope.row, 1)">{{ $t('approve') }}</el-button>
<el-button type="danger" :disabled="scope.row.state !== 0" @click="handleProjectReview(scope.row, 2)">{{ $t('deny') }}</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
hide-on-single-page
:total="reviewPagination.total"
:page-size="reviewPagination.rows"
:current-page.sync="reviewPagination.page"
style="margin-top:10px; text-align:right;"
background
layout="sizes, total, prev, pager, next"
@current-change="handleReviewPageChange"
/>
<div slot="footer" class="dialog-footer">
<el-button @click="reviewListDialogVisible = false">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
<el-dialog :title="$t('setting')" :visible.sync="taskDialogVisible" width="600px">
<el-form ref="taskForm" :rules="taskFormRules" :model="taskFormData" label-width="120px">
<el-form-item :label="$t('projectName')">
@ -333,8 +414,8 @@
</template>
<script>
import tableHeight from '@/mixin/tableHeight'
import { getList, getDetail, getPreview, getCommitList, publish } from '@/api/deploy'
import { addTask, editTask, removeTask, getTaskList } from '@/api/project'
import { getList, getDetail, getPreview, getCommitList, publish, review } from '@/api/deploy'
import { addTask, editTask, removeTask, getTaskList, getReviewList } from '@/api/project'
import { getUserOption } from '@/api/namespace'
import { parseTime, parseGitURL } from '@/utils'
@ -350,6 +431,8 @@ export default {
commitDialogVisible: false,
taskDialogVisible: false,
taskListDialogVisible: false,
reviewDialogVisible: false,
reviewListDialogVisible: false,
dialogVisible: false,
tableloading: false,
tableData: [],
@ -389,6 +472,13 @@ export default {
{ required: true, message: 'Date required', trigger: 'change' }
]
},
reviewTableLoading: false,
reviewTableData: [],
reviewPagination: {
total: 0,
page: 1,
rows: 20
},
searchPreview: {
loading: false,
projectId: '',
@ -608,7 +698,7 @@ export default {
this.getDetail()
},
handlePublishCommand(data) {
getCommitList(data) {
const id = data.id
this.commitDialogVisible = true
this.commitTableLoading = true
@ -652,18 +742,29 @@ export default {
}
},
handleProjectTaskCommand(data) {
this.taskListDialogVisible = true
getTaskList() {
this.taskTableLoading = true
getTaskList(this.taskPagination, data.id).then(response => {
const projectTaskList = response.data.projectTaskList || []
getTaskList(this.taskPagination, this.selectedItem.id).then(response => {
const projectTaskList = response.data.list
this.taskTableData = projectTaskList.map(element => {
return Object.assign(element, { projectId: data.id, projectName: data.name })
return Object.assign(element, { projectId: this.selectedItem.id, projectName: this.selectedItem.name })
})
this.taskPagination.total = response.data.pagination.total
}).finally(() => { this.taskTableLoading = false })
},
handleTaskCommand(data) {
this.selectedItem = data
this.taskPagination.page = 1
this.taskListDialogVisible = true
this.getTaskList()
},
handleTaskPageChange(page) {
this.taskPagination.page = page
this.getTaskList()
},
submitTask() {
this.$refs.taskForm.validate((valid) => {
if (valid) {
@ -713,6 +814,53 @@ export default {
})
},
getReviewList() {
this.reviewTableLoading = true
getReviewList(this.reviewPagination, this.selectedItem.id).then(response => {
const projectTaskList = response.data.list
this.reviewTableData = projectTaskList.map(element => {
return Object.assign(element, { projectId: this.selectedItem.id, projectName: this.selectedItem.name })
})
this.reviewPagination.total = response.data.pagination.total
}).finally(() => { this.reviewTableLoading = false })
},
handleReviewCommand(data) {
this.selectedItem = data
this.reviewPagination.page = 1
this.reviewListDialogVisible = true
this.getReviewList()
},
handleReviewPageChange(page) {
this.reviewPagination.page = page
this.getReviewList()
},
handleProjectReview(data, state) {
if (state === 1) {
this.$confirm(this.$i18n.t('deployPage.reviewTips'), this.$i18n.t('tips'), {
confirmButtonText: this.$i18n.t('confirm'),
cancelButtonText: this.$i18n.t('cancel'),
type: 'warning'
}).then(() => {
review(data.id, state).then((response) => {
const projectIndex = this.tableData.findIndex(element => element.id === data.projectId)
this.tableData[projectIndex].state = state
this.reviewListDialogVisible = false
})
}).catch(() => {
this.$message.info('Cancel')
})
} else {
review(data.id, state).then((response) => {
const projectIndex = this.tableData.findIndex(element => element.id === data.projectId)
this.tableData[projectIndex].state = state
this.reviewListDialogVisible = false
})
}
},
rollback(data) {
this.$confirm(this.$i18n.t('deployPage.rollbackTips', { commit: data.commit }), this.$i18n.t('tips'), {
confirmButtonText: this.$i18n.t('confirm'),

View File

@ -46,6 +46,12 @@
<el-button type="text" icon="el-icon-edit" @click="handleAutoDeploy(scope.row)" />
</template>
</el-table-column>
<el-table-column width="60" :label="$t('review')" align="center">
<template slot-scope="scope">
<span v-if="scope.row.review === 0">{{ $t('close') }}</span>
<span v-else>{{ $t('open') }}</span>
</template>
</el-table-column>
<el-table-column prop="server" width="80" :label="$t('server')" align="center">
<template slot-scope="scope">
<el-button type="text" @click="handleServer(scope.row)">{{ $t('view') }}</el-button>
@ -131,6 +137,18 @@
<span slot="label">Rsync<br> [OPTION...]</span>
<el-input v-model.trim="formData.rsyncOption" type="textarea" :rows="3" autocomplete="off" placeholder="-rtv --exclude .git --delete-after" />
</el-form-item>
<el-form-item :label="$t('projectPage.deployNotice')" prop="notifyTarget">
<el-row type="flex">
<el-select v-model="formData.notifyType" clearable>
<el-option :label="$t('webhookOption[0]')" :value="0" />
<el-option :label="$t('webhookOption[1]')" :value="1" />
<el-option :label="$t('webhookOption[2]')" :value="2" />
<el-option :label="$t('webhookOption[3]')" :value="3" />
<el-option :label="$t('webhookOption[255]')" :value="255" />
</el-select>
<el-input v-model.trim="formData.notifyTarget" autocomplete="off" placeholder="webhook" />
</el-row>
</el-form-item>
<el-form-item v-show="formProps.showServers" :label="$t('server')" prop="serverIds">
<el-select v-model="formData.serverIds" multiple style="width:100%">
<el-option
@ -152,6 +170,28 @@
</el-select>
</el-form-item>
</el-tab-pane>
<el-tab-pane :label="$t('projectPage.publishReview')" name="review">
<el-form-item label="" label-width="10px">
<el-radio-group v-model="formData.review">
<el-radio :label="0">{{ $t('close') }}</el-radio>
<el-radio :label="1">{{ $t('open') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-show="formData.review" label="URL" label-width="50px">
<el-input v-model.trim="formProps.reviewURL" autocomplete="off" placeholder="http(s)://domain?custom-param=1" />
</el-form-item>
<el-form-item v-show="formData.review" :label="$t('param')" label-width="50px">
<el-checkbox-group v-model="formProps.reviewURLParam">
<el-checkbox
v-for="(item, key) in formProps.reviewURLParamOption"
:key="key"
:label="item.value"
:disabled="item['disabled']"
>{{ item.label }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-row v-show="formData.review" style="margin: 0 10px" v-html="$t('projectPage.reviewFooterTips')" />
</el-tab-pane>
<el-tab-pane :label="$t('projectPage.symlinkLabel')" name="symlink">
<el-row style="margin: 0 10px" v-html="$t('projectPage.symlinkHeaderTips')" />
<el-form-item label="" label-width="10px">
@ -209,20 +249,6 @@
<codemirror ref="afterDeployScript" v-model="formData.afterDeployScript" :options="cmOption" />
</el-form-item>
</el-tab-pane>
<el-tab-pane :label="$t('advancedSetting')" name="advance">
<el-form-item :label="$t('projectPage.deployNotice')" prop="notifyTarget">
<el-row type="flex">
<el-select v-model="formData.notifyType" clearable>
<el-option :label="$t('webhookOption[0]')" :value="0" />
<el-option :label="$t('webhookOption[1]')" :value="1" />
<el-option :label="$t('webhookOption[2]')" :value="2" />
<el-option :label="$t('webhookOption[3]')" :value="3" />
<el-option :label="$t('webhookOption[255]')" :value="255" />
</el-select>
<el-input v-model.trim="formData.notifyTarget" autocomplete="off" placeholder="webhook" />
</el-row>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<div slot="footer" class="dialog-footer">
@ -415,6 +441,47 @@ export default {
tableServerData: [],
tableUserData: [],
formProps: {
reviewURLParamOption: [
{
label: 'project_id',
value: 'project_id=__PROJECT_ID__'
},
{
label: 'project_name',
value: 'project_name=__PROJECT_NAME__'
},
{
label: 'branch',
value: 'branch=__BRANCH__'
},
{
label: 'environment',
value: 'environment=__ENVIRONMENT__'
},
{
label: 'commit_id',
value: 'commit_id=__COMMIT_ID__'
},
{
label: 'publish_time',
value: 'publish_time=__PUBLISH_TIME__'
},
{
label: 'publisher_id',
value: 'publisher_id=__PUBLISHER_ID__'
},
{
label: 'publisher_name',
value: 'publisher_name=__PUBLISHER_NAME__'
},
{
label: 'callback',
value: 'callback=__CALLBACK__',
disabled: true
}
],
reviewURL: '',
reviewURLParam: ['callback=__CALLBACK__'],
symlink: false,
disabled: false,
branch: [],
@ -439,6 +506,8 @@ export default {
rsyncOption: '-rtv --exclude .git --delete-after',
serverIds: [],
userIds: [],
review: 0,
reviewURL: '',
notifyType: 0,
notifyTarget: ''
},
@ -447,7 +516,7 @@ export default {
{ required: true, message: 'Name required', trigger: ['blur'] }
],
url: [
{ required: true, message: 'Repository url required', trigger: ['blur'] }
{ required: true, type: 'url', message: 'Repository url required', trigger: ['blur'] }
],
path: [
{ required: true, message: 'Path required', trigger: ['blur'] }
@ -517,18 +586,24 @@ export default {
this.formProps.symlink = this.formData.symlinkPath !== ''
this.formProps.showServers = this.formProps.showUsers = false
this.formProps.branch = []
this.formProps.reviewURL = ''
this.formProps.reviewURLParam = []
if (this.formData.review === 1 && this.formData.reviewURL.length > 0) {
const url = new URL(this.formData.reviewURL)
this.formProps.reviewURLParamOption.forEach(item => {
if (url.searchParams.has(item.value.split('=')[0])) {
url.searchParams.delete(item.value.split('=')[0])
this.formProps.reviewURLParam.push(item.value)
}
})
this.formProps.reviewURL = url.href
}
this.dialogVisible = true
},
handleCopy(data) {
this.formData = Object.assign({}, data)
this.handleEdit(data)
this.formData.id = 0
this.formData.serverIds = []
this.formData.userIds = []
this.formProps.symlink = this.formData.symlinkPath !== ''
this.formProps.showServers = this.formProps.showUsers = false
this.formProps.branch = []
this.dialogVisible = true
},
handleRemove(data) {
@ -602,6 +677,15 @@ export default {
if (this.formProps.symlink === false) {
this.formData.symlinkPath = ''
}
if (this.formData.review === 1 && this.formProps.reviewURL.length > 0) {
const url = new URL(this.formProps.reviewURL)
this.formProps.reviewURLParam.forEach(param => {
url.searchParams.set(...param.split('='))
})
this.formData.reviewURL = url.href
} else {
this.formData.reviewURL = ''
}
if (this.formData.id === 0) {
this.add()
} else {