mirror of https://github.com/openspug/spug
				
				
				
			fix issue
							parent
							
								
									083452dd90
								
							
						
					
					
						commit
						6c77c699aa
					
				| 
						 | 
				
			
			@ -0,0 +1,212 @@
 | 
			
		|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
 | 
			
		||||
# Copyright: (c) <spug.dev@gmail.com>
 | 
			
		||||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django_redis import get_redis_connection
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from libs.utils import human_datetime
 | 
			
		||||
from apps.host.models import Host
 | 
			
		||||
from apps.notify.models import Notify
 | 
			
		||||
import requests
 | 
			
		||||
import subprocess
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpugError(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Helper:
 | 
			
		||||
    def __init__(self, rds, key):
 | 
			
		||||
        self.rds = rds
 | 
			
		||||
        self.key = key
 | 
			
		||||
        self.rds.delete(self.key)
 | 
			
		||||
        self.by_deploy = key.startswith(settings.REQUEST_KEY)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _make_dd_notify(cls, action, req, version, host_str):
 | 
			
		||||
        texts = [
 | 
			
		||||
            f'**申请标题:** {req.name}',
 | 
			
		||||
            f'**应用名称:** {req.deploy.app.name}',
 | 
			
		||||
            f'**应用版本:** {version}',
 | 
			
		||||
            f'**发布环境:** {req.deploy.env.name}',
 | 
			
		||||
            f'**发布主机:** {host_str}',
 | 
			
		||||
        ]
 | 
			
		||||
        if action == 'approve_req':
 | 
			
		||||
            texts.insert(0, '## %s ## ' % '发布审核申请')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'**申请人员:** {req.created_by.nickname}',
 | 
			
		||||
                f'**申请时间:** {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        elif action == 'approve_rst':
 | 
			
		||||
            color, text = ('#008000', '通过') if req.status == '1' else ('#f90202', '驳回')
 | 
			
		||||
            texts.insert(0, '## %s ## ' % '发布审核结果')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'**审核人员:** {req.approve_by.nickname}',
 | 
			
		||||
                f'**审核结果:** <font color="{color}">{text}</font>',
 | 
			
		||||
                f'**审核意见:** {req.reason or ""}',
 | 
			
		||||
                f'**审核时间:** {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        else:
 | 
			
		||||
            color, text = ('#008000', '成功') if req.status == '3' else ('#f90202', '失败')
 | 
			
		||||
            texts.insert(0, '## %s ## ' % '发布结果通知')
 | 
			
		||||
            if req.approve_at:
 | 
			
		||||
                texts.append(f'**审核人员:** {req.approve_by.nickname}')
 | 
			
		||||
            do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'**执行人员:** {do_user}',
 | 
			
		||||
                f'**发布结果:** <font color="{color}">{text}</font>',
 | 
			
		||||
                f'**发布时间:** {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        return {
 | 
			
		||||
            'msgtype': 'markdown',
 | 
			
		||||
            'markdown': {
 | 
			
		||||
                'title': 'Spug 发布消息通知',
 | 
			
		||||
                'text': '\n\n'.join(texts)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _make_wx_notify(cls, action, req, version, host_str):
 | 
			
		||||
        texts = [
 | 
			
		||||
            f'申请标题: {req.name}',
 | 
			
		||||
            f'应用名称: {req.deploy.app.name}',
 | 
			
		||||
            f'应用版本: {version}',
 | 
			
		||||
            f'发布环境: {req.deploy.env.name}',
 | 
			
		||||
            f'发布主机: {host_str}',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        if action == 'approve_req':
 | 
			
		||||
            texts.insert(0, '## %s' % '发布审核申请')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'申请人员: {req.created_by.nickname}',
 | 
			
		||||
                f'申请时间: {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        elif action == 'approve_rst':
 | 
			
		||||
            color, text = ('info', '通过') if req.status == '1' else ('warning', '驳回')
 | 
			
		||||
            texts.insert(0, '## %s' % '发布审核结果')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'审核人员: {req.approve_by.nickname}',
 | 
			
		||||
                f'审核结果: <font color="{color}">{text}</font>',
 | 
			
		||||
                f'审核意见: {req.reason or ""}',
 | 
			
		||||
                f'审核时间: {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        else:
 | 
			
		||||
            color, text = ('info', '成功') if req.status == '3' else ('warning', '失败')
 | 
			
		||||
            texts.insert(0, '## %s' % '发布结果通知')
 | 
			
		||||
            if req.approve_at:
 | 
			
		||||
                texts.append(f'审核人员: {req.approve_by.nickname}')
 | 
			
		||||
            do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'执行人员: {do_user}',
 | 
			
		||||
                f'发布结果: <font color="{color}">{text}</font>',
 | 
			
		||||
                f'发布时间: {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        return {
 | 
			
		||||
            'msgtype': 'markdown',
 | 
			
		||||
            'markdown': {
 | 
			
		||||
                'content': '\n'.join(texts)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def send_deploy_notify(cls, req, action=None):
 | 
			
		||||
        rst_notify = json.loads(req.deploy.rst_notify)
 | 
			
		||||
        host_ids = json.loads(req.host_ids)
 | 
			
		||||
        if rst_notify['mode'] != '0' and rst_notify.get('value'):
 | 
			
		||||
            version = req.version
 | 
			
		||||
            hosts = [{'id': x.id, 'name': x.name} for x in Host.objects.filter(id__in=host_ids)]
 | 
			
		||||
            host_str = ', '.join(x['name'] for x in hosts[:2])
 | 
			
		||||
            if len(hosts) > 2:
 | 
			
		||||
                host_str += f'等{len(hosts)}台主机'
 | 
			
		||||
            if rst_notify['mode'] == '1':
 | 
			
		||||
                data = cls._make_dd_notify(action, req, version, host_str)
 | 
			
		||||
            elif rst_notify['mode'] == '2':
 | 
			
		||||
                data = {
 | 
			
		||||
                    'action': action,
 | 
			
		||||
                    'req_id': req.id,
 | 
			
		||||
                    'req_name': req.name,
 | 
			
		||||
                    'app_id': req.deploy.app_id,
 | 
			
		||||
                    'app_name': req.deploy.app.name,
 | 
			
		||||
                    'env_id': req.deploy.env_id,
 | 
			
		||||
                    'env_name': req.deploy.env.name,
 | 
			
		||||
                    'status': req.status,
 | 
			
		||||
                    'reason': req.reason,
 | 
			
		||||
                    'version': version,
 | 
			
		||||
                    'targets': hosts,
 | 
			
		||||
                    'is_success': req.status == '3',
 | 
			
		||||
                    'created_at': human_datetime()
 | 
			
		||||
                }
 | 
			
		||||
            elif rst_notify['mode'] == '3':
 | 
			
		||||
                data = cls._make_wx_notify(action, req, version, host_str)
 | 
			
		||||
            else:
 | 
			
		||||
                raise NotImplementedError
 | 
			
		||||
            res = requests.post(rst_notify['value'], json=data)
 | 
			
		||||
            if res.status_code != 200:
 | 
			
		||||
                Notify.make_notify('flag', '1', '发布通知发送失败', f'返回状态码:{res.status_code}, 请求URL:{res.url}')
 | 
			
		||||
            if rst_notify['mode'] in ['1', '3']:
 | 
			
		||||
                res = res.json()
 | 
			
		||||
                if res.get('errcode') != 0:
 | 
			
		||||
                    Notify.make_notify('flag', '1', '发布通知发送失败', f'返回数据:{res}')
 | 
			
		||||
 | 
			
		||||
    def parse_filter_rule(self, data: str, sep='\n'):
 | 
			
		||||
        data, files = data.strip(), []
 | 
			
		||||
        if data:
 | 
			
		||||
            for line in data.split(sep):
 | 
			
		||||
                line = line.strip()
 | 
			
		||||
                if line and not line.startswith('#'):
 | 
			
		||||
                    files.append(line)
 | 
			
		||||
        return files
 | 
			
		||||
 | 
			
		||||
    def _send(self, message):
 | 
			
		||||
        self.rds.rpush(self.key, json.dumps(message))
 | 
			
		||||
 | 
			
		||||
    def send_info(self, key, message):
 | 
			
		||||
        if message:
 | 
			
		||||
            self._send({'key': key, 'data': message})
 | 
			
		||||
 | 
			
		||||
    def send_error(self, key, message, with_break=True):
 | 
			
		||||
        message = f'\r\n\033[31m{message}\033[0m'
 | 
			
		||||
        self._send({'key': key, 'status': 'error', 'data': message})
 | 
			
		||||
        if with_break:
 | 
			
		||||
            raise SpugError
 | 
			
		||||
 | 
			
		||||
    def send_step(self, key, step, data):
 | 
			
		||||
        self._send({'key': key, 'step': step, 'data': data})
 | 
			
		||||
 | 
			
		||||
    def clear(self):
 | 
			
		||||
        # save logs for two weeks
 | 
			
		||||
        self.rds.expire(self.key, 14 * 24 * 60 * 60)
 | 
			
		||||
        self.rds.close()
 | 
			
		||||
 | 
			
		||||
    def local(self, command, env=None):
 | 
			
		||||
        if env:
 | 
			
		||||
            env = dict(env.items())
 | 
			
		||||
            env.update(os.environ)
 | 
			
		||||
        task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
 | 
			
		||||
        while True:
 | 
			
		||||
            message = task.stdout.readline()
 | 
			
		||||
            if not message:
 | 
			
		||||
                break
 | 
			
		||||
            message = message.decode().rstrip('\r\n')
 | 
			
		||||
            self.send_info('local', message + '\r\n')
 | 
			
		||||
        if task.wait() != 0:
 | 
			
		||||
            self.send_error('local', f'exit code: {task.returncode}')
 | 
			
		||||
 | 
			
		||||
    def remote(self, key, ssh, command, env=None):
 | 
			
		||||
        code = -1
 | 
			
		||||
        for code, out in ssh.exec_command_with_stream(command, environment=env):
 | 
			
		||||
            self.send_info(key, out)
 | 
			
		||||
        if code != 0:
 | 
			
		||||
            self.send_error(key, f'exit code: {code}')
 | 
			
		||||
 | 
			
		||||
    def remote_raw(self, key, ssh, command):
 | 
			
		||||
        code, out = ssh.exec_command_raw(command)
 | 
			
		||||
        if code != 0:
 | 
			
		||||
            self.send_error(key, f'exit code: {code}')
 | 
			
		||||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ from libs import ModelMixin, human_datetime
 | 
			
		|||
from apps.account.models import User
 | 
			
		||||
from apps.app.models import Deploy
 | 
			
		||||
from apps.repository.models import Repository
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeployRequest(models.Model, ModelMixin):
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +43,13 @@ class DeployRequest(models.Model, ModelMixin):
 | 
			
		|||
    do_at = models.CharField(max_length=20, null=True)
 | 
			
		||||
    do_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_quick_deploy(self):
 | 
			
		||||
        if self.extra:
 | 
			
		||||
            extra = json.loads(self.extra)
 | 
			
		||||
            return extra[0] in ('branch', 'tag')
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return f'<DeployRequest name={self.name}>'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,13 +3,14 @@
 | 
			
		|||
# Released under the AGPL-3.0 License.
 | 
			
		||||
from django_redis import get_redis_connection
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from libs.utils import AttrDict, human_time, human_datetime
 | 
			
		||||
from django.db import close_old_connections
 | 
			
		||||
from libs.utils import AttrDict, human_time
 | 
			
		||||
from apps.host.models import Host
 | 
			
		||||
from apps.notify.models import Notify
 | 
			
		||||
from apps.config.utils import compose_configs
 | 
			
		||||
from apps.repository.models import Repository
 | 
			
		||||
from apps.repository.utils import dispatch as build_repository
 | 
			
		||||
from apps.deploy.helper import Helper, SpugError
 | 
			
		||||
from concurrent import futures
 | 
			
		||||
import requests
 | 
			
		||||
import subprocess
 | 
			
		||||
import json
 | 
			
		||||
import uuid
 | 
			
		||||
import os
 | 
			
		||||
| 
						 | 
				
			
			@ -17,17 +18,13 @@ import os
 | 
			
		|||
REPOS_DIR = settings.REPOS_DIR
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpugError(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def dispatch(req):
 | 
			
		||||
    rds = get_redis_connection()
 | 
			
		||||
    rds_key = f'{settings.REQUEST_KEY}:{req.id}'
 | 
			
		||||
    helper = Helper(rds, rds_key)
 | 
			
		||||
    try:
 | 
			
		||||
        api_token = uuid.uuid4().hex
 | 
			
		||||
        rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
 | 
			
		||||
        helper = Helper(rds, rds_key)
 | 
			
		||||
        env = AttrDict(
 | 
			
		||||
            SPUG_APP_NAME=req.deploy.app.name,
 | 
			
		||||
            SPUG_APP_ID=str(req.deploy.app_id),
 | 
			
		||||
| 
						 | 
				
			
			@ -55,13 +52,25 @@ def dispatch(req):
 | 
			
		|||
        req.status = '-3'
 | 
			
		||||
        raise e
 | 
			
		||||
    finally:
 | 
			
		||||
        rds.expire(rds_key, 14 * 24 * 60 * 60)
 | 
			
		||||
        rds.close()
 | 
			
		||||
        close_old_connections()
 | 
			
		||||
        req.save()
 | 
			
		||||
        helper.clear()
 | 
			
		||||
        Helper.send_deploy_notify(req)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _ext1_deploy(req, helper, env):
 | 
			
		||||
    if not req.repository_id:
 | 
			
		||||
        rep = Repository(
 | 
			
		||||
            app_id=req.deploy.app_id,
 | 
			
		||||
            env_id=req.deploy.env_id,
 | 
			
		||||
            deploy_id=req.deploy_id,
 | 
			
		||||
            version=req.version,
 | 
			
		||||
            spug_version=req.spug_version,
 | 
			
		||||
            extra=req.extra,
 | 
			
		||||
            created_by_id=req.created_by_id
 | 
			
		||||
        )
 | 
			
		||||
        build_repository(rep, helper)
 | 
			
		||||
        req.repository = rep
 | 
			
		||||
    extend = req.deploy.extend_obj
 | 
			
		||||
    env.update(SPUG_DST_DIR=extend.dst_dir)
 | 
			
		||||
    threads, latest_exception = [], None
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +92,7 @@ def _ext1_deploy(req, helper, env):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def _ext2_deploy(req, helper, env):
 | 
			
		||||
    helper.send_info('local', f'完成\r\n')
 | 
			
		||||
    helper.send_info('local', f'\033[32m完成√\033[0m\r\n')
 | 
			
		||||
    extend, step = req.deploy.extend_obj, 1
 | 
			
		||||
    host_actions = json.loads(extend.host_actions)
 | 
			
		||||
    server_actions = json.loads(extend.server_actions)
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +134,7 @@ def _ext2_deploy(req, helper, env):
 | 
			
		|||
                        exclude = ' '.join(excludes)
 | 
			
		||||
            tar_gz_file = f'{req.spug_version}.tar.gz'
 | 
			
		||||
            helper.local(f'cd {sp_dir} && tar -zcf {tar_gz_file} {exclude} {contain}')
 | 
			
		||||
            helper.send_info('local', f'{human_time()} 打包完成\r\n')
 | 
			
		||||
            helper.send_info('local', f'{human_time()} \033[32m完成√\033[0m\r\n')
 | 
			
		||||
            tmp_transfer_file = os.path.join(sp_dir, tar_gz_file)
 | 
			
		||||
            break
 | 
			
		||||
    if host_actions:
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +162,7 @@ def _ext2_deploy(req, helper, env):
 | 
			
		|||
 | 
			
		||||
def _deploy_ext1_host(req, helper, h_id, env):
 | 
			
		||||
    extend = req.deploy.extend_obj
 | 
			
		||||
    helper.send_step(h_id, 1, f'就绪\r\n{human_time()} 数据准备...        ')
 | 
			
		||||
    helper.send_step(h_id, 1, f'\033[32m就绪√\033[0m\r\n{human_time()} 数据准备...        ')
 | 
			
		||||
    host = Host.objects.filter(pk=h_id).first()
 | 
			
		||||
    if not host:
 | 
			
		||||
        helper.send_error(h_id, 'no such host')
 | 
			
		||||
| 
						 | 
				
			
			@ -175,7 +184,7 @@ def _deploy_ext1_host(req, helper, h_id, env):
 | 
			
		|||
 | 
			
		||||
        command = f'cd {extend.dst_repo} && rm -rf {req.spug_version} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
 | 
			
		||||
        helper.remote_raw(host.id, ssh, command)
 | 
			
		||||
        helper.send_step(h_id, 1, '完成\r\n')
 | 
			
		||||
        helper.send_step(h_id, 1, '\033[32m完成√\033[0m\r\n')
 | 
			
		||||
 | 
			
		||||
        # pre host
 | 
			
		||||
        repo_dir = os.path.join(extend.dst_repo, req.spug_version)
 | 
			
		||||
| 
						 | 
				
			
			@ -187,7 +196,7 @@ def _deploy_ext1_host(req, helper, h_id, env):
 | 
			
		|||
        # do deploy
 | 
			
		||||
        helper.send_step(h_id, 3, f'{human_time()} 执行发布...        ')
 | 
			
		||||
        helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
 | 
			
		||||
        helper.send_step(h_id, 3, '完成\r\n')
 | 
			
		||||
        helper.send_step(h_id, 3, '\033[32m完成√\033[0m\r\n')
 | 
			
		||||
 | 
			
		||||
        # post host
 | 
			
		||||
        if extend.hook_post_host:
 | 
			
		||||
| 
						 | 
				
			
			@ -195,11 +204,11 @@ def _deploy_ext1_host(req, helper, h_id, env):
 | 
			
		|||
            command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
 | 
			
		||||
            helper.remote(host.id, ssh, command)
 | 
			
		||||
 | 
			
		||||
        helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
 | 
			
		||||
        helper.send_step(h_id, 100, f'\r\n{human_time()} ** \033[32m发布成功\033[0m **')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
 | 
			
		||||
    helper.send_info(h_id, '就绪\r\n')
 | 
			
		||||
    helper.send_info(h_id, '\033[32m就绪√\033[0m\r\n')
 | 
			
		||||
    host = Host.objects.filter(pk=h_id).first()
 | 
			
		||||
    if not host:
 | 
			
		||||
        helper.send_error(h_id, 'no such host')
 | 
			
		||||
| 
						 | 
				
			
			@ -230,194 +239,4 @@ def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
 | 
			
		|||
                command = f'cd /tmp ; {action["data"]}'
 | 
			
		||||
            helper.remote(host.id, ssh, command)
 | 
			
		||||
 | 
			
		||||
    helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Helper:
 | 
			
		||||
    def __init__(self, rds, key):
 | 
			
		||||
        self.rds = rds
 | 
			
		||||
        self.key = key
 | 
			
		||||
        self.rds.delete(self.key)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _make_dd_notify(cls, action, req, version, host_str):
 | 
			
		||||
        texts = [
 | 
			
		||||
            f'**申请标题:** {req.name}',
 | 
			
		||||
            f'**应用名称:** {req.deploy.app.name}',
 | 
			
		||||
            f'**应用版本:** {version}',
 | 
			
		||||
            f'**发布环境:** {req.deploy.env.name}',
 | 
			
		||||
            f'**发布主机:** {host_str}',
 | 
			
		||||
        ]
 | 
			
		||||
        if action == 'approve_req':
 | 
			
		||||
            texts.insert(0, '## %s ## ' % '发布审核申请')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'**申请人员:** {req.created_by.nickname}',
 | 
			
		||||
                f'**申请时间:** {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        elif action == 'approve_rst':
 | 
			
		||||
            color, text = ('#008000', '通过') if req.status == '1' else ('#f90202', '驳回')
 | 
			
		||||
            texts.insert(0, '## %s ## ' % '发布审核结果')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'**审核人员:** {req.approve_by.nickname}',
 | 
			
		||||
                f'**审核结果:** <font color="{color}">{text}</font>',
 | 
			
		||||
                f'**审核意见:** {req.reason or ""}',
 | 
			
		||||
                f'**审核时间:** {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        else:
 | 
			
		||||
            color, text = ('#008000', '成功') if req.status == '3' else ('#f90202', '失败')
 | 
			
		||||
            texts.insert(0, '## %s ## ' % '发布结果通知')
 | 
			
		||||
            if req.approve_at:
 | 
			
		||||
                texts.append(f'**审核人员:** {req.approve_by.nickname}')
 | 
			
		||||
            do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'**执行人员:** {do_user}',
 | 
			
		||||
                f'**发布结果:** <font color="{color}">{text}</font>',
 | 
			
		||||
                f'**发布时间:** {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        return {
 | 
			
		||||
            'msgtype': 'markdown',
 | 
			
		||||
            'markdown': {
 | 
			
		||||
                'title': 'Spug 发布消息通知',
 | 
			
		||||
                'text': '\n\n'.join(texts)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _make_wx_notify(cls, action, req, version, host_str):
 | 
			
		||||
        texts = [
 | 
			
		||||
            f'申请标题: {req.name}',
 | 
			
		||||
            f'应用名称: {req.deploy.app.name}',
 | 
			
		||||
            f'应用版本: {version}',
 | 
			
		||||
            f'发布环境: {req.deploy.env.name}',
 | 
			
		||||
            f'发布主机: {host_str}',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        if action == 'approve_req':
 | 
			
		||||
            texts.insert(0, '## %s' % '发布审核申请')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'申请人员: {req.created_by.nickname}',
 | 
			
		||||
                f'申请时间: {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        elif action == 'approve_rst':
 | 
			
		||||
            color, text = ('info', '通过') if req.status == '1' else ('warning', '驳回')
 | 
			
		||||
            texts.insert(0, '## %s' % '发布审核结果')
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'审核人员: {req.approve_by.nickname}',
 | 
			
		||||
                f'审核结果: <font color="{color}">{text}</font>',
 | 
			
		||||
                f'审核意见: {req.reason or ""}',
 | 
			
		||||
                f'审核时间: {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        else:
 | 
			
		||||
            color, text = ('info', '成功') if req.status == '3' else ('warning', '失败')
 | 
			
		||||
            texts.insert(0, '## %s' % '发布结果通知')
 | 
			
		||||
            if req.approve_at:
 | 
			
		||||
                texts.append(f'审核人员: {req.approve_by.nickname}')
 | 
			
		||||
            do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
 | 
			
		||||
            texts.extend([
 | 
			
		||||
                f'执行人员: {do_user}',
 | 
			
		||||
                f'发布结果: <font color="{color}">{text}</font>',
 | 
			
		||||
                f'发布时间: {human_datetime()}',
 | 
			
		||||
                '> 来自 Spug运维平台'
 | 
			
		||||
            ])
 | 
			
		||||
        return {
 | 
			
		||||
            'msgtype': 'markdown',
 | 
			
		||||
            'markdown': {
 | 
			
		||||
                'content': '\n'.join(texts)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def send_deploy_notify(cls, req, action=None):
 | 
			
		||||
        rst_notify = json.loads(req.deploy.rst_notify)
 | 
			
		||||
        host_ids = json.loads(req.host_ids)
 | 
			
		||||
        if rst_notify['mode'] != '0' and rst_notify.get('value'):
 | 
			
		||||
            version = req.version
 | 
			
		||||
            hosts = [{'id': x.id, 'name': x.name} for x in Host.objects.filter(id__in=host_ids)]
 | 
			
		||||
            host_str = ', '.join(x['name'] for x in hosts[:2])
 | 
			
		||||
            if len(hosts) > 2:
 | 
			
		||||
                host_str += f'等{len(hosts)}台主机'
 | 
			
		||||
            if rst_notify['mode'] == '1':
 | 
			
		||||
                data = cls._make_dd_notify(action, req, version, host_str)
 | 
			
		||||
            elif rst_notify['mode'] == '2':
 | 
			
		||||
                data = {
 | 
			
		||||
                    'action': action,
 | 
			
		||||
                    'req_id': req.id,
 | 
			
		||||
                    'req_name': req.name,
 | 
			
		||||
                    'app_id': req.deploy.app_id,
 | 
			
		||||
                    'app_name': req.deploy.app.name,
 | 
			
		||||
                    'env_id': req.deploy.env_id,
 | 
			
		||||
                    'env_name': req.deploy.env.name,
 | 
			
		||||
                    'status': req.status,
 | 
			
		||||
                    'reason': req.reason,
 | 
			
		||||
                    'version': version,
 | 
			
		||||
                    'targets': hosts,
 | 
			
		||||
                    'is_success': req.status == '3',
 | 
			
		||||
                    'created_at': human_datetime()
 | 
			
		||||
                }
 | 
			
		||||
            elif rst_notify['mode'] == '3':
 | 
			
		||||
                data = cls._make_wx_notify(action, req, version, host_str)
 | 
			
		||||
            else:
 | 
			
		||||
                raise NotImplementedError
 | 
			
		||||
            res = requests.post(rst_notify['value'], json=data)
 | 
			
		||||
            if res.status_code != 200:
 | 
			
		||||
                Notify.make_notify('flag', '1', '发布通知发送失败', f'返回状态码:{res.status_code}, 请求URL:{res.url}')
 | 
			
		||||
            if rst_notify['mode'] in ['1', '3']:
 | 
			
		||||
                res = res.json()
 | 
			
		||||
                if res.get('errcode') != 0:
 | 
			
		||||
                    Notify.make_notify('flag', '1', '发布通知发送失败', f'返回数据:{res}')
 | 
			
		||||
 | 
			
		||||
    def parse_filter_rule(self, data: str, sep='\n'):
 | 
			
		||||
        data, files = data.strip(), []
 | 
			
		||||
        if data:
 | 
			
		||||
            for line in data.split(sep):
 | 
			
		||||
                line = line.strip()
 | 
			
		||||
                if line and not line.startswith('#'):
 | 
			
		||||
                    files.append(line)
 | 
			
		||||
        return files
 | 
			
		||||
 | 
			
		||||
    def _send(self, message):
 | 
			
		||||
        self.rds.rpush(self.key, json.dumps(message))
 | 
			
		||||
 | 
			
		||||
    def send_info(self, key, message):
 | 
			
		||||
        if message:
 | 
			
		||||
            self._send({'key': key, 'data': message})
 | 
			
		||||
 | 
			
		||||
    def send_error(self, key, message, with_break=True):
 | 
			
		||||
        message = '\r\n' + message
 | 
			
		||||
        self._send({'key': key, 'status': 'error', 'data': message})
 | 
			
		||||
        if with_break:
 | 
			
		||||
            raise SpugError
 | 
			
		||||
 | 
			
		||||
    def send_step(self, key, step, data):
 | 
			
		||||
        self._send({'key': key, 'step': step, 'data': data})
 | 
			
		||||
 | 
			
		||||
    def local(self, command, env=None):
 | 
			
		||||
        if env:
 | 
			
		||||
            env = dict(env.items())
 | 
			
		||||
            env.update(os.environ)
 | 
			
		||||
        task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
 | 
			
		||||
        while True:
 | 
			
		||||
            message = task.stdout.readline()
 | 
			
		||||
            if not message:
 | 
			
		||||
                break
 | 
			
		||||
            message = message.decode().rstrip('\r\n')
 | 
			
		||||
            self.send_info('local', message + '\r\n')
 | 
			
		||||
        if task.wait() != 0:
 | 
			
		||||
            self.send_error('local', f'exit code: {task.returncode}')
 | 
			
		||||
 | 
			
		||||
    def remote(self, key, ssh, command, env=None):
 | 
			
		||||
        code = -1
 | 
			
		||||
        for code, out in ssh.exec_command_with_stream(command, environment=env):
 | 
			
		||||
            self.send_info(key, out)
 | 
			
		||||
        if code != 0:
 | 
			
		||||
            self.send_error(key, f'exit code: {code}')
 | 
			
		||||
 | 
			
		||||
    def remote_raw(self, key, ssh, command):
 | 
			
		||||
        code, out = ssh.exec_command_raw(command)
 | 
			
		||||
        if code != 0:
 | 
			
		||||
            self.send_error(key, f'exit code: {code}')
 | 
			
		||||
    helper.send_step(h_id, 100, f'\r\n{human_time()} ** \033[32m发布成功\033[0m **')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -126,12 +126,15 @@ class RequestDetailView(View):
 | 
			
		|||
        hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
 | 
			
		||||
        outputs = {x.id: {'id': x.id, 'title': x.name, 'data': f'{human_time()} 读取数据...        '} for x in hosts}
 | 
			
		||||
        response = {'outputs': outputs, 'status': req.status}
 | 
			
		||||
        if req.is_quick_deploy:
 | 
			
		||||
            outputs['local'] = {'id': 'local', 'data': ''}
 | 
			
		||||
        if req.deploy.extend == '2':
 | 
			
		||||
            outputs['local'] = {'id': 'local', 'data': f'{human_time()} 读取数据...        '}
 | 
			
		||||
            response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
 | 
			
		||||
            response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
 | 
			
		||||
            if not response['h_actions']:
 | 
			
		||||
                response['outputs'] = {'local': outputs['local']}
 | 
			
		||||
            if req.deploy.extend == '2':
 | 
			
		||||
                response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
 | 
			
		||||
                response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
 | 
			
		||||
                if not response['h_actions']:
 | 
			
		||||
                    response['outputs'] = {'local': outputs['local']}
 | 
			
		||||
        rds, key, counter = get_redis_connection(), f'{settings.REQUEST_KEY}:{r_id}', 0
 | 
			
		||||
        data = rds.lrange(key, counter, counter + 9)
 | 
			
		||||
        while data:
 | 
			
		||||
| 
						 | 
				
			
			@ -146,6 +149,11 @@ class RequestDetailView(View):
 | 
			
		|||
                    outputs[item['key']]['status'] = item['status']
 | 
			
		||||
            data = rds.lrange(key, counter, counter + 9)
 | 
			
		||||
        response['index'] = counter
 | 
			
		||||
        if req.is_quick_deploy:
 | 
			
		||||
            if outputs['local']['data']:
 | 
			
		||||
                outputs['local']['data'] = f'{human_time()} 读取数据...        ' + outputs['local']['data']
 | 
			
		||||
            else:
 | 
			
		||||
                outputs['local'].update(step=100, data=f'{human_time()} 已构建完成忽略执行。')
 | 
			
		||||
        return json_response(response)
 | 
			
		||||
 | 
			
		||||
    def post(self, request, r_id):
 | 
			
		||||
| 
						 | 
				
			
			@ -167,14 +175,19 @@ class RequestDetailView(View):
 | 
			
		|||
        req.do_by = request.user
 | 
			
		||||
        req.save()
 | 
			
		||||
        Thread(target=dispatch, args=(req,)).start()
 | 
			
		||||
        if req.is_quick_deploy:
 | 
			
		||||
            if req.repository_id:
 | 
			
		||||
                outputs['local'] = {'id': 'local', 'step': 100, 'data': f'{human_time()} 已构建完成忽略执行。'}
 | 
			
		||||
            else:
 | 
			
		||||
                outputs['local'] = {'id': 'local', 'step': 0, 'data': f'{human_time()} 建立连接...        '}
 | 
			
		||||
        if req.deploy.extend == '2':
 | 
			
		||||
            message = f'{human_time()} 建立连接...        '
 | 
			
		||||
            outputs['local'] = {'id': 'local', 'step': 0, 'data': message}
 | 
			
		||||
            s_actions = json.loads(req.deploy.extend_obj.server_actions)
 | 
			
		||||
            h_actions = json.loads(req.deploy.extend_obj.host_actions)
 | 
			
		||||
            if not h_actions:
 | 
			
		||||
                outputs = {'local': outputs['local']}
 | 
			
		||||
            return json_response({'s_actions': s_actions, 'h_actions': h_actions, 'outputs': outputs})
 | 
			
		||||
            outputs['local'] = {'id': 'local', 'step': 0, 'data': f'{human_time()} 建立连接...        '}
 | 
			
		||||
            if req.deploy.extend == '2':
 | 
			
		||||
                s_actions = json.loads(req.deploy.extend_obj.server_actions)
 | 
			
		||||
                h_actions = json.loads(req.deploy.extend_obj.host_actions)
 | 
			
		||||
                if not h_actions:
 | 
			
		||||
                    outputs = {'local': outputs['local']}
 | 
			
		||||
                return json_response({'s_actions': s_actions, 'h_actions': h_actions, 'outputs': outputs})
 | 
			
		||||
        return json_response({'outputs': outputs})
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, r_id):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ from libs.utils import AttrDict, human_time
 | 
			
		|||
from apps.repository.models import Repository
 | 
			
		||||
from apps.app.utils import fetch_repo
 | 
			
		||||
from apps.config.utils import compose_configs
 | 
			
		||||
import subprocess
 | 
			
		||||
from apps.deploy.helper import Helper
 | 
			
		||||
import json
 | 
			
		||||
import uuid
 | 
			
		||||
import os
 | 
			
		||||
| 
						 | 
				
			
			@ -16,20 +16,18 @@ import os
 | 
			
		|||
REPOS_DIR = settings.REPOS_DIR
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpugError(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def dispatch(rep: Repository):
 | 
			
		||||
    rds = get_redis_connection()
 | 
			
		||||
    rds_key = f'{settings.BUILD_KEY}:{rep.spug_version}'
 | 
			
		||||
def dispatch(rep: Repository, helper=None):
 | 
			
		||||
    rep.status = '1'
 | 
			
		||||
    rep.save()
 | 
			
		||||
    helper = Helper(rds, rds_key)
 | 
			
		||||
    alone_build = helper is None
 | 
			
		||||
    if not helper:
 | 
			
		||||
        rds = get_redis_connection()
 | 
			
		||||
        rds_key = f'{settings.BUILD_KEY}:{rep.spug_version}'
 | 
			
		||||
        helper = Helper(rds, rds_key)
 | 
			
		||||
        rep.save()
 | 
			
		||||
    try:
 | 
			
		||||
        api_token = uuid.uuid4().hex
 | 
			
		||||
        rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}')
 | 
			
		||||
        helper.send_info('local', f'完成\r\n{human_time()} 构建准备...        ')
 | 
			
		||||
        helper.rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}')
 | 
			
		||||
        helper.send_info('local', f'\033[32m完成√\033[0m\r\n{human_time()} 构建准备...        ')
 | 
			
		||||
        env = AttrDict(
 | 
			
		||||
            SPUG_APP_NAME=rep.app.name,
 | 
			
		||||
            SPUG_APP_ID=str(rep.app_id),
 | 
			
		||||
| 
						 | 
				
			
			@ -54,11 +52,11 @@ def dispatch(rep: Repository):
 | 
			
		|||
    finally:
 | 
			
		||||
        helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')
 | 
			
		||||
        close_old_connections()
 | 
			
		||||
        # save the build log for two weeks
 | 
			
		||||
        rds.expire(rds_key, 14 * 24 * 60 * 60)
 | 
			
		||||
        rds.close()
 | 
			
		||||
        rep.save()
 | 
			
		||||
        return rep
 | 
			
		||||
        if alone_build:
 | 
			
		||||
            helper.clear()
 | 
			
		||||
            rep.save()
 | 
			
		||||
        elif rep.status == '5':
 | 
			
		||||
            rep.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _build(rep: Repository, helper, env):
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +73,7 @@ def _build(rep: Repository, helper, env):
 | 
			
		|||
        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')
 | 
			
		||||
    helper.send_info('local', '\033[32m完成√\033[0m\r\n')
 | 
			
		||||
 | 
			
		||||
    if extend.hook_pre_server:
 | 
			
		||||
        helper.send_step('local', 1, f'{human_time()} 检出前任务...\r\n')
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +82,7 @@ def _build(rep: Repository, helper, env):
 | 
			
		|||
    helper.send_step('local', 2, f'{human_time()} 执行检出...        ')
 | 
			
		||||
    command = f'cd {git_dir} && git archive --prefix={rep.spug_version}/ {tree_ish} | (cd .. && tar xf -)'
 | 
			
		||||
    helper.local(command)
 | 
			
		||||
    helper.send_info('local', '完成\r\n')
 | 
			
		||||
    helper.send_info('local', '\033[32m完成√\033[0m\r\n')
 | 
			
		||||
 | 
			
		||||
    if extend.hook_post_server:
 | 
			
		||||
        helper.send_step('local', 3, f'{human_time()} 检出后任务...\r\n')
 | 
			
		||||
| 
						 | 
				
			
			@ -105,49 +103,5 @@ def _build(rep: Repository, helper, env):
 | 
			
		|||
        else:
 | 
			
		||||
            contain = ' '.join(f'{rep.spug_version}/{x}' for x in files)
 | 
			
		||||
    helper.local(f'cd {REPOS_DIR} && tar zcf {tar_file} {exclude} {contain}')
 | 
			
		||||
    helper.send_step('local', 5, f'完成')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Helper:
 | 
			
		||||
    def __init__(self, rds, key):
 | 
			
		||||
        self.rds = rds
 | 
			
		||||
        self.key = key
 | 
			
		||||
        self.rds.delete(self.key)
 | 
			
		||||
 | 
			
		||||
    def parse_filter_rule(self, data: str, sep='\n'):
 | 
			
		||||
        data, files = data.strip(), []
 | 
			
		||||
        if data:
 | 
			
		||||
            for line in data.split(sep):
 | 
			
		||||
                line = line.strip()
 | 
			
		||||
                if line and not line.startswith('#'):
 | 
			
		||||
                    files.append(line)
 | 
			
		||||
        return files
 | 
			
		||||
 | 
			
		||||
    def _send(self, message):
 | 
			
		||||
        self.rds.rpush(self.key, json.dumps(message))
 | 
			
		||||
 | 
			
		||||
    def send_info(self, key, message):
 | 
			
		||||
        self._send({'key': key, 'data': message})
 | 
			
		||||
 | 
			
		||||
    def send_error(self, key, message, with_break=True):
 | 
			
		||||
        message = '\r\n' + message
 | 
			
		||||
        self._send({'key': key, 'status': 'error', 'data': message})
 | 
			
		||||
        if with_break:
 | 
			
		||||
            raise SpugError
 | 
			
		||||
 | 
			
		||||
    def send_step(self, key, step, data):
 | 
			
		||||
        self._send({'key': key, 'step': step, 'data': data})
 | 
			
		||||
 | 
			
		||||
    def local(self, command, env=None):
 | 
			
		||||
        if env:
 | 
			
		||||
            env = dict(env.items())
 | 
			
		||||
            env.update(os.environ)
 | 
			
		||||
        task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
 | 
			
		||||
        while True:
 | 
			
		||||
            message = task.stdout.readline()
 | 
			
		||||
            if not message:
 | 
			
		||||
                break
 | 
			
		||||
            message = message.decode().rstrip('\r\n')
 | 
			
		||||
            self.send_info('local', message + '\r\n')
 | 
			
		||||
        if task.wait() != 0:
 | 
			
		||||
            self.send_error('local', f'exit code: {task.returncode}')
 | 
			
		||||
    helper.send_step('local', 5, f'\033[32m完成√\033[0m')
 | 
			
		||||
    helper.send_step('local', 100, f'\r\n\r\n{human_time()} ** \033[32m构建成功\033[0m **')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,6 +86,7 @@ function Ext1Console(props) {
 | 
			
		|||
    terms[key] = term
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let {local, ...hosts} = outputs;
 | 
			
		||||
  return store.tabModes[props.request.id] ? (
 | 
			
		||||
    <Card
 | 
			
		||||
      className={styles.item}
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +96,12 @@ function Ext1Console(props) {
 | 
			
		|||
        <div className={styles.title}>{props.request.name}</div>
 | 
			
		||||
        <CloseOutlined onClick={() => store.showConsole(props.request, true)}/>
 | 
			
		||||
      </div>
 | 
			
		||||
      {Object.values(outputs).map(item => (
 | 
			
		||||
      {local && (
 | 
			
		||||
        <Progress
 | 
			
		||||
          percent={(local.step + 1) * 18}
 | 
			
		||||
          status={local.step === 100 ? 'success' : outputs.local.status === 'error' ? 'exception' : 'active'}/>
 | 
			
		||||
      )}
 | 
			
		||||
      {Object.values(hosts).map(item => (
 | 
			
		||||
        <Progress
 | 
			
		||||
          key={item.id}
 | 
			
		||||
          percent={(item.step + 1) * 18}
 | 
			
		||||
| 
						 | 
				
			
			@ -117,11 +123,30 @@ function Ext1Console(props) {
 | 
			
		|||
        </div>
 | 
			
		||||
      ]}>
 | 
			
		||||
      <Skeleton loading={fetching} active>
 | 
			
		||||
        {local && (
 | 
			
		||||
          <Collapse defaultActiveKey={['0']} className={styles.collapse} style={{marginBottom: 24}}>
 | 
			
		||||
            <Collapse.Panel header={(
 | 
			
		||||
              <div className={styles.header}>
 | 
			
		||||
                <b className={styles.title}/>
 | 
			
		||||
                <Steps size="small" className={styles.step} current={local.step} status={local.status}>
 | 
			
		||||
                  <StepItem title="构建准备" item={local} step={0}/>
 | 
			
		||||
                  <StepItem title="检出前任务" item={local} step={1}/>
 | 
			
		||||
                  <StepItem title="执行检出" item={local} step={2}/>
 | 
			
		||||
                  <StepItem title="检出后任务" item={local} step={3}/>
 | 
			
		||||
                  <StepItem title="执行打包" item={local} step={4}/>
 | 
			
		||||
                </Steps>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}>
 | 
			
		||||
              <OutView setTerm={term => handleSetTerm(term, 'local')}/>
 | 
			
		||||
            </Collapse.Panel>
 | 
			
		||||
          </Collapse>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <Collapse
 | 
			
		||||
          defaultActiveKey="0"
 | 
			
		||||
          className={styles.collapse}
 | 
			
		||||
          expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
 | 
			
		||||
          {Object.entries(outputs).map(([key, item], index) => (
 | 
			
		||||
          {Object.entries(hosts).map(([key, item], index) => (
 | 
			
		||||
            <Collapse.Panel
 | 
			
		||||
              key={index}
 | 
			
		||||
              header={
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue