mirror of https://github.com/openspug/spug
add webhook for auto deploy
parent
d428933b8f
commit
20f28fd64c
|
@ -0,0 +1,96 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from django.http.response import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
|
||||||
|
from apps.setting.utils import AppSetting
|
||||||
|
from apps.deploy.models import Deploy, DeployRequest
|
||||||
|
from apps.repository.models import Repository
|
||||||
|
from apps.repository.utils import dispatch as build_dispatch
|
||||||
|
from apps.deploy.utils import dispatch as deploy_dispatch
|
||||||
|
from threading import Thread
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def auto_deploy(request, deploy_id, kind):
|
||||||
|
token = request.headers.get('X-Gitlab-Token') or request.headers.get('X-Gitee-Token')
|
||||||
|
if token and _is_valid_token(token):
|
||||||
|
try:
|
||||||
|
body = json.loads(request.body)
|
||||||
|
commit_id = body['after']
|
||||||
|
_, _kind, ref = body['ref'].split('/', 2)
|
||||||
|
if commit_id != '0000000000000000000000000000000000000000':
|
||||||
|
if kind == 'branch':
|
||||||
|
if _kind == 'heads' and ref == request.GET.get('name'):
|
||||||
|
Thread(target=_dispatch, args=(deploy_id, ref, commit_id)).start()
|
||||||
|
return HttpResponse(status=202)
|
||||||
|
elif kind == 'tag':
|
||||||
|
if _kind == 'tags':
|
||||||
|
Thread(target=_dispatch, args=(deploy_id, ref)).start()
|
||||||
|
return HttpResponse(status=202)
|
||||||
|
return HttpResponse(status=204)
|
||||||
|
except Exception as e:
|
||||||
|
return HttpResponseBadRequest(e)
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_token(token):
|
||||||
|
api_key = AppSetting.get_default('api_key')
|
||||||
|
return api_key == token
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch(deploy_id, ref, commit_id=None):
|
||||||
|
deploy = Deploy.objects.filter(pk=deploy_id).first()
|
||||||
|
if not deploy:
|
||||||
|
raise Exception(f'no such deploy id for {deploy_id}')
|
||||||
|
if deploy.extend == '1':
|
||||||
|
_deploy_extend_1(deploy, ref, commit_id)
|
||||||
|
else:
|
||||||
|
_deploy_extend_2(deploy, ref, commit_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _deploy_extend_1(deploy, ref, commit_id=None):
|
||||||
|
if commit_id:
|
||||||
|
extra = ['branch', ref, commit_id]
|
||||||
|
version = f'#_b_{commit_id[:6]}'
|
||||||
|
else:
|
||||||
|
extra = ['tag', ref, None]
|
||||||
|
version = f'#_t_{ref}'
|
||||||
|
rep = Repository.objects.create(
|
||||||
|
deploy=deploy,
|
||||||
|
app_id=deploy.app_id,
|
||||||
|
env_id=deploy.env_id,
|
||||||
|
version=version,
|
||||||
|
status='1',
|
||||||
|
extra=json.dumps(extra),
|
||||||
|
spug_version=Repository.make_spug_version(deploy.id),
|
||||||
|
created_by=deploy.created_by)
|
||||||
|
rep = build_dispatch(rep)
|
||||||
|
if rep.status == '5':
|
||||||
|
req = DeployRequest.objects.create(
|
||||||
|
type='3',
|
||||||
|
status='2',
|
||||||
|
deploy=deploy,
|
||||||
|
repository=rep,
|
||||||
|
name=rep.version,
|
||||||
|
version=rep.version,
|
||||||
|
spug_version=rep.spug_version,
|
||||||
|
host_ids=deploy.host_ids,
|
||||||
|
created_by=deploy.created_by
|
||||||
|
)
|
||||||
|
deploy_dispatch(req)
|
||||||
|
|
||||||
|
|
||||||
|
def _deploy_extend_2(deploy, ref, commit_id=None):
|
||||||
|
# 创建 环境变量 分支 commit-id tag
|
||||||
|
version = f'#_b_{commit_id[:6]}' if commit_id else f'#_t_{ref}'
|
||||||
|
req = DeployRequest.objects.create(
|
||||||
|
type='3',
|
||||||
|
status='2',
|
||||||
|
deploy=deploy,
|
||||||
|
name=version,
|
||||||
|
version=version,
|
||||||
|
spug_version=Repository.make_spug_version(deploy.id),
|
||||||
|
host_ids=deploy.host_ids,
|
||||||
|
created_by=deploy.created_by
|
||||||
|
)
|
||||||
|
deploy_dispatch(req)
|
|
@ -4,7 +4,9 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from apps.apis import config
|
from apps.apis import config
|
||||||
|
from apps.apis import deploy
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('config/', config.get_configs),
|
path('config/', config.get_configs),
|
||||||
|
path('deploy/<int:deploy_id>/<str:kind>/', deploy.auto_deploy)
|
||||||
]
|
]
|
||||||
|
|
|
@ -23,10 +23,14 @@ def parse_envs(text):
|
||||||
def fetch_versions(deploy: Deploy):
|
def fetch_versions(deploy: Deploy):
|
||||||
git_repo = deploy.extend_obj.git_repo
|
git_repo = deploy.extend_obj.git_repo
|
||||||
repo_dir = os.path.join(settings.REPOS_DIR, str(deploy.id))
|
repo_dir = os.path.join(settings.REPOS_DIR, str(deploy.id))
|
||||||
try:
|
pkey = AppSetting.get_default('private_key')
|
||||||
pkey = AppSetting.get('private_key')
|
with Git(git_repo, repo_dir, pkey) as git:
|
||||||
except KeyError:
|
return git.fetch_branches_tags()
|
||||||
pkey = None
|
|
||||||
|
|
||||||
|
def fetch_repo(deploy_id, git_repo):
|
||||||
|
repo_dir = os.path.join(settings.REPOS_DIR, str(deploy_id))
|
||||||
|
pkey = AppSetting.get_default('private_key')
|
||||||
with Git(git_repo, repo_dir, pkey) as git:
|
with Git(git_repo, repo_dir, pkey) as git:
|
||||||
return git.fetch_branches_tags()
|
return git.fetch_branches_tags()
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,8 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
)
|
)
|
||||||
TYPES = (
|
TYPES = (
|
||||||
('1', '正常发布'),
|
('1', '正常发布'),
|
||||||
('2', '回滚')
|
('2', '回滚'),
|
||||||
|
('3', '自动发布'),
|
||||||
)
|
)
|
||||||
deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE)
|
deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE)
|
||||||
repository = models.ForeignKey(Repository, null=True, on_delete=models.SET_NULL)
|
repository = models.ForeignKey(Repository, null=True, on_delete=models.SET_NULL)
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.conf import settings
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
from libs.utils import AttrDict, human_time
|
from libs.utils import AttrDict, human_time
|
||||||
from apps.repository.models import Repository
|
from apps.repository.models import Repository
|
||||||
|
from apps.app.utils import fetch_repo
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -51,6 +52,7 @@ def dispatch(rep: Repository):
|
||||||
rds.expire(rds_key, 14 * 24 * 60 * 60)
|
rds.expire(rds_key, 14 * 24 * 60 * 60)
|
||||||
rds.close()
|
rds.close()
|
||||||
rep.save()
|
rep.save()
|
||||||
|
return rep
|
||||||
|
|
||||||
|
|
||||||
def _build(rep: Repository, helper, env):
|
def _build(rep: Repository, helper, env):
|
||||||
|
@ -66,6 +68,7 @@ def _build(rep: Repository, helper, env):
|
||||||
else:
|
else:
|
||||||
tree_ish = extras[1]
|
tree_ish = extras[1]
|
||||||
env.update(SPUG_GIT_TAG=extras[1])
|
env.update(SPUG_GIT_TAG=extras[1])
|
||||||
|
fetch_repo(rep.deploy_id, extend.git_repo)
|
||||||
helper.send_info('local', '完成\r\n')
|
helper.send_info('local', '完成\r\n')
|
||||||
|
|
||||||
if extend.hook_pre_server:
|
if extend.hook_pre_server:
|
||||||
|
@ -115,7 +118,6 @@ class Helper:
|
||||||
return files
|
return files
|
||||||
|
|
||||||
def _send(self, message):
|
def _send(self, message):
|
||||||
print(message)
|
|
||||||
self.rds.rpush(self.key, json.dumps(message))
|
self.rds.rpush(self.key, json.dumps(message))
|
||||||
|
|
||||||
def send_info(self, key, message):
|
def send_info(self, key, message):
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Git:
|
||||||
self.repo.archive(f, commit)
|
self.repo.archive(f, commit)
|
||||||
|
|
||||||
def fetch_branches_tags(self):
|
def fetch_branches_tags(self):
|
||||||
self._fetch()
|
self.fetch()
|
||||||
branches, tags = {}, {}
|
branches, tags = {}, {}
|
||||||
for ref in self.repo.references:
|
for ref in self.repo.references:
|
||||||
if isinstance(ref, RemoteReference):
|
if isinstance(ref, RemoteReference):
|
||||||
|
@ -42,7 +42,7 @@ class Git:
|
||||||
tags = sorted(tags.items(), key=lambda x: x[1]['date'], reverse=True)
|
tags = sorted(tags.items(), key=lambda x: x[1]['date'], reverse=True)
|
||||||
return branches, dict(tags)
|
return branches, dict(tags)
|
||||||
|
|
||||||
def _fetch(self):
|
def fetch(self):
|
||||||
try:
|
try:
|
||||||
self.repo.remotes.origin.fetch(p=True, P=True)
|
self.repo.remotes.origin.fetch(p=True, P=True)
|
||||||
except GitCommandError as e:
|
except GitCommandError as e:
|
||||||
|
|
|
@ -35,8 +35,7 @@ class AuthenticationMiddleware(MiddlewareMixin):
|
||||||
user = User.objects.filter(access_token=access_token).first()
|
user = User.objects.filter(access_token=access_token).first()
|
||||||
if user and x_real_ip == user.last_ip and user.token_expired >= time.time() and user.is_active:
|
if user and x_real_ip == user.last_ip and user.token_expired >= time.time() and user.is_active:
|
||||||
request.user = user
|
request.user = user
|
||||||
if request.path != '/notify/':
|
user.token_expired = time.time() + 8 * 60 * 60
|
||||||
user.token_expired = time.time() + 8 * 60 * 60
|
|
||||||
user.save()
|
user.save()
|
||||||
return None
|
return None
|
||||||
response = json_response(error="验证失败,请重新登录")
|
response = json_response(error="验证失败,请重新登录")
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Modal, Form, Input, Select, Radio, Button, message } from 'antd';
|
||||||
|
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
|
import { http } from 'libs';
|
||||||
|
import store from './store';
|
||||||
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
|
||||||
|
export default observer(function AutoDeploy() {
|
||||||
|
const [type, setType] = useState('branch');
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
|
const [branches, setBranches] = useState([]);
|
||||||
|
const [branch, setBranch] = useState();
|
||||||
|
const [url, setURL] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (store.deploy.extend === '1') {
|
||||||
|
fetchVersions()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const prefix = window.location.origin;
|
||||||
|
let tmp = `${prefix}/api/apis/deploy/${store.deploy.id}/${type}/`;
|
||||||
|
if (type === 'branch') {
|
||||||
|
tmp += `?name=${branch}`
|
||||||
|
}
|
||||||
|
setURL(tmp)
|
||||||
|
}, [type, branch])
|
||||||
|
|
||||||
|
function fetchVersions() {
|
||||||
|
setFetching(true);
|
||||||
|
http.get(`/api/app/deploy/${store.deploy.id}/versions/`)
|
||||||
|
.then(res => setBranches(Object.keys(res.branches)))
|
||||||
|
.finally(() => setFetching(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipBoard() {
|
||||||
|
const t = document.createElement('input');
|
||||||
|
t.value = url;
|
||||||
|
document.body.appendChild(t);
|
||||||
|
t.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
t.remove();
|
||||||
|
message.success('已复制')
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagMode = type === 'tag';
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible
|
||||||
|
width={540}
|
||||||
|
title="Webhook"
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => store.autoVisible = false}>
|
||||||
|
<Form labelCol={{span: 6}} wrapperCol={{span: 16}}>
|
||||||
|
<Form.Item required label="触发方式">
|
||||||
|
<Radio.Group value={type} onChange={e => setType(e.target.value)}>
|
||||||
|
<Radio.Button value="branch">Branch</Radio.Button>
|
||||||
|
<Radio.Button value="tag">Tag</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
{store.deploy.extend === '1' ? (
|
||||||
|
<Form.Item required={!tagMode} label="选择分支" extra={<span>
|
||||||
|
根据你的网络情况,首次刷新可能会很慢,请耐心等待。
|
||||||
|
<a target="_blank" rel="noopener noreferrer"
|
||||||
|
href="https://spug.dev/docs/install-error/#%E6%96%B0%E5%BB%BA%E5%B8%B8%E8%A7%84%E5%8F%91%E5%B8%83%E7%94%B3%E8%AF%B7-git-clone-%E9%94%99%E8%AF%AF">刷新失败?</a>
|
||||||
|
</span>}>
|
||||||
|
<Form.Item style={{display: 'inline-block', marginBottom: 0, width: '246px'}}>
|
||||||
|
<Select disabled={tagMode} placeholder="仅指定分支的事件触发自动发布" value={branch} onChange={setBranch}>
|
||||||
|
{branches.map(item => (
|
||||||
|
<Select.Option key={item} value={item}>{item}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item style={{display: 'inline-block', width: 82, textAlign: 'center', marginBottom: 0}}>
|
||||||
|
{fetching ? <LoadingOutlined style={{fontSize: 18, color: '#1890ff'}}/> :
|
||||||
|
<Button type="link" icon={<SyncOutlined/>} disabled={fetching || tagMode}
|
||||||
|
onClick={fetchVersions}>刷新</Button>
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item label="指定分支">
|
||||||
|
<Input
|
||||||
|
disabled={tagMode}
|
||||||
|
value={branch}
|
||||||
|
onChange={e => setBranch(e.target.value)}
|
||||||
|
placeholder="仅指定分支的事件触发自动发布"/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{type === 'branch' && !branch ? (
|
||||||
|
<Form.Item label="Webhook URL">
|
||||||
|
<div style={{color: '#ff4d4f'}}>请指定分支名称。</div>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item label="Webhook URL" extra="点击复制链接,目前仅支持Gitee和Gitlab。">
|
||||||
|
<div className={styles.webhook} onClick={copyToClipBoard}>{url}</div>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
})
|
|
@ -102,10 +102,13 @@ function ComTable() {
|
||||||
<Action>
|
<Action>
|
||||||
<Action.Button
|
<Action.Button
|
||||||
auth="deploy.app.config"
|
auth="deploy.app.config"
|
||||||
onClick={e => store.showExtForm(e, record.id, info, false, true)}>查看</Action.Button>
|
onClick={e => store.showAutoDeploy(info)}>Webhook</Action.Button>
|
||||||
<Action.Button auth="deploy.app.edit"
|
{hasPermission('deploy.app.edit') ? (
|
||||||
onClick={e => store.showExtForm(e, record.id, info)}>编辑</Action.Button>
|
<Action.Button onClick={e => store.showExtForm(e, record.id, info)}>编辑</Action.Button>
|
||||||
<Action.Button auth="deploy.app.edit" onClick={() => handleDeployDelete(info)}>删除</Action.Button>
|
) : hasPermission('deploy.app.config') ? (
|
||||||
|
<Action.Button onClick={e => store.showExtForm(e, record.id, info, false, true)}>查看</Action.Button>
|
||||||
|
) : null}
|
||||||
|
<Action.Button danger auth="deploy.app.edit" onClick={() => handleDeployDelete(info)}>删除</Action.Button>
|
||||||
</Action>
|
</Action>
|
||||||
)}/>
|
)}/>
|
||||||
)}
|
)}
|
||||||
|
@ -155,7 +158,7 @@ function ComTable() {
|
||||||
<Action.Button auth="deploy.app.edit" onClick={e => store.showExtForm(e, info.id)}>新建发布</Action.Button>
|
<Action.Button auth="deploy.app.edit" onClick={e => store.showExtForm(e, info.id)}>新建发布</Action.Button>
|
||||||
<Action.Button auth="deploy.app.edit" onClick={e => handleClone(e, info.id)}>克隆发布</Action.Button>
|
<Action.Button auth="deploy.app.edit" onClick={e => handleClone(e, info.id)}>克隆发布</Action.Button>
|
||||||
<Action.Button auth="deploy.app.edit" onClick={e => store.showForm(e, info)}>编辑</Action.Button>
|
<Action.Button auth="deploy.app.edit" onClick={e => store.showForm(e, info)}>编辑</Action.Button>
|
||||||
<Action.Button auth="deploy.app.del" onClick={e => handleDelete(e, info)}>删除</Action.Button>
|
<Action.Button danger auth="deploy.app.del" onClick={e => handleDelete(e, info)}>删除</Action.Button>
|
||||||
</Action>
|
</Action>
|
||||||
)}/>
|
)}/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import ComForm from './Form';
|
||||||
import Ext1Form from './Ext1Form';
|
import Ext1Form from './Ext1Form';
|
||||||
import Ext2Form from './Ext2Form';
|
import Ext2Form from './Ext2Form';
|
||||||
import AddSelect from './AddSelect';
|
import AddSelect from './AddSelect';
|
||||||
|
import AutoDeploy from './AutoDeploy';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import envStore from 'pages/config/environment/store';
|
import envStore from 'pages/config/environment/store';
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ export default observer(function () {
|
||||||
{store.addVisible && <AddSelect/>}
|
{store.addVisible && <AddSelect/>}
|
||||||
{store.ext1Visible && <Ext1Form/>}
|
{store.ext1Visible && <Ext1Form/>}
|
||||||
{store.ext2Visible && <Ext2Form/>}
|
{store.ext2Visible && <Ext2Form/>}
|
||||||
|
{store.autoVisible && <AutoDeploy/>}
|
||||||
</AuthDiv>
|
</AuthDiv>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
@ -71,4 +71,9 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1890ff;
|
||||||
}
|
}
|
|
@ -19,6 +19,7 @@ class Store {
|
||||||
@observable addVisible = false;
|
@observable addVisible = false;
|
||||||
@observable ext1Visible = false;
|
@observable ext1Visible = false;
|
||||||
@observable ext2Visible = false;
|
@observable ext2Visible = false;
|
||||||
|
@observable autoVisible = false;
|
||||||
@observable selectorVisible = false;
|
@observable selectorVisible = false;
|
||||||
|
|
||||||
@observable f_name;
|
@observable f_name;
|
||||||
|
@ -79,6 +80,11 @@ class Store {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
showAutoDeploy = (deploy) => {
|
||||||
|
this.deploy = deploy;
|
||||||
|
this.autoVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
addHost = () => {
|
addHost = () => {
|
||||||
this.deploy['host_ids'].push(undefined)
|
this.deploy['host_ids'].push(undefined)
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,6 +18,7 @@ function ComTable() {
|
||||||
render: info => (
|
render: info => (
|
||||||
<div>
|
<div>
|
||||||
{info.type === '2' && <Tooltip title="回滚发布"><Tag color="#f50">R</Tag></Tooltip>}
|
{info.type === '2' && <Tooltip title="回滚发布"><Tag color="#f50">R</Tag></Tooltip>}
|
||||||
|
{info.type === '3' && <Tooltip title="Webhook触发"><Tag color="#87d068">A</Tag></Tooltip>}
|
||||||
{info.plan && <Tooltip title={`定时发布(${info.plan})`}> <Tag color="#108ee9">P</Tag></Tooltip>}
|
{info.plan && <Tooltip title={`定时发布(${info.plan})`}> <Tag color="#108ee9">P</Tag></Tooltip>}
|
||||||
{info.name}
|
{info.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue