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 apps.apis import config
|
||||
from apps.apis import deploy
|
||||
|
||||
urlpatterns = [
|
||||
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):
|
||||
git_repo = deploy.extend_obj.git_repo
|
||||
repo_dir = os.path.join(settings.REPOS_DIR, str(deploy.id))
|
||||
try:
|
||||
pkey = AppSetting.get('private_key')
|
||||
except KeyError:
|
||||
pkey = None
|
||||
pkey = AppSetting.get_default('private_key')
|
||||
with Git(git_repo, repo_dir, pkey) as git:
|
||||
return git.fetch_branches_tags()
|
||||
|
||||
|
||||
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:
|
||||
return git.fetch_branches_tags()
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ class DeployRequest(models.Model, ModelMixin):
|
|||
)
|
||||
TYPES = (
|
||||
('1', '正常发布'),
|
||||
('2', '回滚')
|
||||
('2', '回滚'),
|
||||
('3', '自动发布'),
|
||||
)
|
||||
deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE)
|
||||
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 libs.utils import AttrDict, human_time
|
||||
from apps.repository.models import Repository
|
||||
from apps.app.utils import fetch_repo
|
||||
import subprocess
|
||||
import json
|
||||
import uuid
|
||||
|
@ -51,6 +52,7 @@ def dispatch(rep: Repository):
|
|||
rds.expire(rds_key, 14 * 24 * 60 * 60)
|
||||
rds.close()
|
||||
rep.save()
|
||||
return rep
|
||||
|
||||
|
||||
def _build(rep: Repository, helper, env):
|
||||
|
@ -66,6 +68,7 @@ def _build(rep: Repository, helper, env):
|
|||
else:
|
||||
tree_ish = extras[1]
|
||||
env.update(SPUG_GIT_TAG=extras[1])
|
||||
fetch_repo(rep.deploy_id, extend.git_repo)
|
||||
helper.send_info('local', '完成\r\n')
|
||||
|
||||
if extend.hook_pre_server:
|
||||
|
@ -115,7 +118,6 @@ class Helper:
|
|||
return files
|
||||
|
||||
def _send(self, message):
|
||||
print(message)
|
||||
self.rds.rpush(self.key, json.dumps(message))
|
||||
|
||||
def send_info(self, key, message):
|
||||
|
|
|
@ -21,7 +21,7 @@ class Git:
|
|||
self.repo.archive(f, commit)
|
||||
|
||||
def fetch_branches_tags(self):
|
||||
self._fetch()
|
||||
self.fetch()
|
||||
branches, tags = {}, {}
|
||||
for ref in self.repo.references:
|
||||
if isinstance(ref, RemoteReference):
|
||||
|
@ -42,7 +42,7 @@ class Git:
|
|||
tags = sorted(tags.items(), key=lambda x: x[1]['date'], reverse=True)
|
||||
return branches, dict(tags)
|
||||
|
||||
def _fetch(self):
|
||||
def fetch(self):
|
||||
try:
|
||||
self.repo.remotes.origin.fetch(p=True, P=True)
|
||||
except GitCommandError as e:
|
||||
|
|
|
@ -35,8 +35,7 @@ class AuthenticationMiddleware(MiddlewareMixin):
|
|||
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:
|
||||
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()
|
||||
return None
|
||||
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.Button
|
||||
auth="deploy.app.config"
|
||||
onClick={e => store.showExtForm(e, record.id, info, false, true)}>查看</Action.Button>
|
||||
<Action.Button auth="deploy.app.edit"
|
||||
onClick={e => store.showExtForm(e, record.id, info)}>编辑</Action.Button>
|
||||
<Action.Button auth="deploy.app.edit" onClick={() => handleDeployDelete(info)}>删除</Action.Button>
|
||||
onClick={e => store.showAutoDeploy(info)}>Webhook</Action.Button>
|
||||
{hasPermission('deploy.app.edit') ? (
|
||||
<Action.Button onClick={e => store.showExtForm(e, record.id, 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>
|
||||
)}/>
|
||||
)}
|
||||
|
@ -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 => 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.del" onClick={e => handleDelete(e, info)}>删除</Action.Button>
|
||||
<Action.Button danger auth="deploy.app.del" onClick={e => handleDelete(e, info)}>删除</Action.Button>
|
||||
</Action>
|
||||
)}/>
|
||||
)}
|
||||
|
|
|
@ -12,6 +12,7 @@ import ComForm from './Form';
|
|||
import Ext1Form from './Ext1Form';
|
||||
import Ext2Form from './Ext2Form';
|
||||
import AddSelect from './AddSelect';
|
||||
import AutoDeploy from './AutoDeploy';
|
||||
import store from './store';
|
||||
import envStore from 'pages/config/environment/store';
|
||||
|
||||
|
@ -42,6 +43,7 @@ export default observer(function () {
|
|||
{store.addVisible && <AddSelect/>}
|
||||
{store.ext1Visible && <Ext1Form/>}
|
||||
{store.ext2Visible && <Ext2Form/>}
|
||||
{store.autoVisible && <AutoDeploy/>}
|
||||
</AuthDiv>
|
||||
);
|
||||
})
|
||||
|
|
|
@ -71,4 +71,9 @@
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.webhook {
|
||||
cursor: pointer;
|
||||
color: #1890ff;
|
||||
}
|
|
@ -19,6 +19,7 @@ class Store {
|
|||
@observable addVisible = false;
|
||||
@observable ext1Visible = false;
|
||||
@observable ext2Visible = false;
|
||||
@observable autoVisible = false;
|
||||
@observable selectorVisible = false;
|
||||
|
||||
@observable f_name;
|
||||
|
@ -79,6 +80,11 @@ class Store {
|
|||
}
|
||||
};
|
||||
|
||||
showAutoDeploy = (deploy) => {
|
||||
this.deploy = deploy;
|
||||
this.autoVisible = true
|
||||
}
|
||||
|
||||
addHost = () => {
|
||||
this.deploy['host_ids'].push(undefined)
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ function ComTable() {
|
|||
render: info => (
|
||||
<div>
|
||||
{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.name}
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue