mirror of
https://github.com/opsre/LiteOps.git
synced 2026-06-09 20:57:25 +08:00
✨ feat: 新增gitlab push事件触发webhooks自动构建功能
This commit is contained in:
22
Dockerfile
22
Dockerfile
@@ -52,14 +52,14 @@ RUN set -eux; \
|
|||||||
# SSH客户端基础配置
|
# SSH客户端基础配置
|
||||||
mkdir -p /root/.ssh && \
|
mkdir -p /root/.ssh && \
|
||||||
chmod 700 /root/.ssh && \
|
chmod 700 /root/.ssh && \
|
||||||
# 轻量化安装NVM
|
# 安装NVM
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash && \
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash && \
|
||||||
echo 'export NVM_DIR="$HOME/.nvm"' >> /root/.bashrc && \
|
echo 'export NVM_DIR="$HOME/.nvm"' >> /root/.bashrc && \
|
||||||
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> /root/.bashrc && \
|
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> /root/.bashrc && \
|
||||||
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use' >> /root/.profile && \
|
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use' >> /root/.profile && \
|
||||||
# 创建Java和Maven安装目录
|
# 创建Java和Maven安装目录
|
||||||
mkdir -p /usr/local/java /usr/local/maven && \
|
mkdir -p /usr/local/java /usr/local/maven && \
|
||||||
# 安装精简版Docker Engine
|
# 安装Docker Engine
|
||||||
(curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
(curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
||||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian bullseye stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null) || \
|
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian bullseye stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null) || \
|
||||||
(curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
(curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
||||||
@@ -75,7 +75,7 @@ RUN set -eux; \
|
|||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/apt/* /root/.cache/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/apt/* /root/.cache/*
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 精简Java环境安装
|
# Java环境安装
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
COPY jdk-8u211-linux-x64.tar.gz apache-maven-3.8.8-bin.tar.gz /tmp/
|
COPY jdk-8u211-linux-x64.tar.gz apache-maven-3.8.8-bin.tar.gz /tmp/
|
||||||
|
|
||||||
@@ -85,12 +85,12 @@ RUN set -eux; \
|
|||||||
tar -xzf /tmp/apache-maven-3.8.8-bin.tar.gz -C /usr/local/maven && \
|
tar -xzf /tmp/apache-maven-3.8.8-bin.tar.gz -C /usr/local/maven && \
|
||||||
# 立即清理压缩包
|
# 立即清理压缩包
|
||||||
rm -f /tmp/jdk-8u211-linux-x64.tar.gz /tmp/apache-maven-3.8.8-bin.tar.gz && \
|
rm -f /tmp/jdk-8u211-linux-x64.tar.gz /tmp/apache-maven-3.8.8-bin.tar.gz && \
|
||||||
# 极度精简JDK - 删除所有不必要的文件
|
# 删除所有不必要的文件
|
||||||
cd /usr/local/java/jdk1.8.0_211 && \
|
cd /usr/local/java/jdk1.8.0_211 && \
|
||||||
rm -rf src.zip javafx-src.zip man sample demo \
|
rm -rf src.zip javafx-src.zip man sample demo \
|
||||||
COPYRIGHT LICENSE README.html THIRDPARTYLICENSEREADME.txt \
|
COPYRIGHT LICENSE README.html THIRDPARTYLICENSEREADME.txt \
|
||||||
release ASSEMBLY_EXCEPTION && \
|
release ASSEMBLY_EXCEPTION && \
|
||||||
# 删除不常用的JDK工具(保留核心编译和运行工具)
|
# 删除不常用的JDK工具
|
||||||
cd bin && \
|
cd bin && \
|
||||||
rm -f appletviewer extcheck jarsigner java-rmi.cgi \
|
rm -f appletviewer extcheck jarsigner java-rmi.cgi \
|
||||||
javadoc javah javap javaws jcmd jconsole jdb jhat \
|
javadoc javah javap javaws jcmd jconsole jdb jhat \
|
||||||
@@ -105,7 +105,7 @@ RUN set -eux; \
|
|||||||
cd bin && \
|
cd bin && \
|
||||||
rm -f javaws jvisualvm orbd policytool rmid \
|
rm -f javaws jvisualvm orbd policytool rmid \
|
||||||
rmiregistry servertool tnameserv && \
|
rmiregistry servertool tnameserv && \
|
||||||
# 精简Maven安装,删除文档和示例
|
# Maven安装,删除文档和示例
|
||||||
cd /usr/local/maven/apache-maven-3.8.8 && \
|
cd /usr/local/maven/apache-maven-3.8.8 && \
|
||||||
rm -rf LICENSE NOTICE README.txt
|
rm -rf LICENSE NOTICE README.txt
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ RUN set -eux; \
|
|||||||
# 配置pip镜像源
|
# 配置pip镜像源
|
||||||
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
||||||
pip config set install.trusted-host mirrors.aliyun.com && \
|
pip config set install.trusted-host mirrors.aliyun.com && \
|
||||||
# 安装精简版Docker Engine
|
# 安装Docker Engine
|
||||||
(curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
(curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
||||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian bullseye stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null) || \
|
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian bullseye stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null) || \
|
||||||
(curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
(curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
||||||
@@ -194,17 +194,17 @@ RUN set -eux; \
|
|||||||
/usr/share/man/* /usr/share/locale/* /usr/share/info/*
|
/usr/share/man/* /usr/share/locale/* /usr/share/info/*
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 从构建阶段复制精简的文件
|
# 从构建阶段复制文件
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 复制SSH配置
|
# 复制SSH配置
|
||||||
COPY --from=builder /root/.ssh /root/.ssh
|
COPY --from=builder /root/.ssh /root/.ssh
|
||||||
|
|
||||||
# 复制精简的NVM环境
|
# 复制NVM环境
|
||||||
COPY --from=builder /root/.nvm /root/.nvm
|
COPY --from=builder /root/.nvm /root/.nvm
|
||||||
COPY --from=builder /root/.bashrc /root/.bashrc
|
COPY --from=builder /root/.bashrc /root/.bashrc
|
||||||
COPY --from=builder /root/.profile /root/.profile
|
COPY --from=builder /root/.profile /root/.profile
|
||||||
|
|
||||||
# 复制精简后的Java环境
|
# 复制Java环境
|
||||||
COPY --from=builder /usr/local/java /usr/local/java
|
COPY --from=builder /usr/local/java /usr/local/java
|
||||||
COPY --from=builder /usr/local/maven /usr/local/maven
|
COPY --from=builder /usr/local/maven /usr/local/maven
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ RUN ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
|
|||||||
# 复制前端构建文件到Nginx静态文件目录
|
# 复制前端构建文件到Nginx静态文件目录
|
||||||
COPY web/dist/ /usr/share/nginx/html/
|
COPY web/dist/ /usr/share/nginx/html/
|
||||||
|
|
||||||
# 优化Python依赖安装
|
# Python依赖安装
|
||||||
COPY backend/requirements.txt /app/
|
COPY backend/requirements.txt /app/
|
||||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||||
# 清理pip缓存和不必要的文件
|
# 清理pip缓存和不必要的文件
|
||||||
|
|||||||
@@ -226,47 +226,23 @@ class BuildTask(models.Model):
|
|||||||
stages = models.JSONField(default=list, verbose_name='构建阶段')
|
stages = models.JSONField(default=list, verbose_name='构建阶段')
|
||||||
|
|
||||||
# 构建参数配置
|
# 构建参数配置
|
||||||
parameters = models.JSONField(default=list, verbose_name='构建参数配置', help_text='''
|
parameters = models.JSONField(default=list, verbose_name='构建参数配置')
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "MY_SERVICES",
|
|
||||||
"description": "选择要部署的服务",
|
|
||||||
"choices": ["user-service", "order-service", "payment-service"],
|
|
||||||
"default_values": ["user-service"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
''')
|
|
||||||
|
|
||||||
# 外部脚本库配置
|
# 外部脚本库配置
|
||||||
use_external_script = models.BooleanField(default=False, verbose_name='使用外部脚本库')
|
use_external_script = models.BooleanField(default=False, verbose_name='使用外部脚本库')
|
||||||
external_script_config = models.JSONField(default=dict, verbose_name='外部脚本库配置', help_text='''
|
external_script_config = models.JSONField(default=dict, verbose_name='外部脚本库配置')
|
||||||
{
|
|
||||||
"repo_url": "https://github.com/example/scripts.git", # Git仓库地址
|
|
||||||
"directory": "/data/scripts", # 存放目录
|
|
||||||
"branch": "main", # 分支名称(可选)
|
|
||||||
"token_id": "credential_id" # Git Token凭证ID(私有仓库)
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
|
|
||||||
# 构建时间信息(使用JSON存储)
|
# 构建时间信息(使用JSON存储)
|
||||||
build_time = models.JSONField(default=dict, verbose_name='构建时间信息', help_text='''
|
build_time = models.JSONField(default=dict, verbose_name='构建时间信息')
|
||||||
{
|
|
||||||
"total_duration": "300", # 总耗时(秒)
|
|
||||||
"start_time": "2024-03-06 12:00:00", # 开始时间
|
|
||||||
"end_time": "2024-03-06 12:05:00", # 结束时间
|
|
||||||
"stages_time": [ # 各阶段时间信息
|
|
||||||
{
|
|
||||||
"name": "代码拉取",
|
|
||||||
"start_time": "2024-03-06 12:00:00",
|
|
||||||
"duration": "60" # 耗时(秒)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
|
|
||||||
# 构建后操作
|
# 构建后操作
|
||||||
notification_channels = models.JSONField(default=list, verbose_name='通知方式')
|
notification_channels = models.JSONField(default=list, verbose_name='通知方式')
|
||||||
|
|
||||||
|
# 自动构建配置
|
||||||
|
auto_build_enabled = models.BooleanField(default=False, verbose_name='启用自动构建')
|
||||||
|
auto_build_branches = models.JSONField(default=list, verbose_name='自动构建分支')
|
||||||
|
webhook_token = models.CharField(max_length=64, null=True, blank=True, verbose_name='Webhook验证Token')
|
||||||
|
|
||||||
# 状态和统计
|
# 状态和统计
|
||||||
status = models.CharField(max_length=20, default='created', null=True, verbose_name='任务状态') # created, disabled
|
status = models.CharField(max_length=20, default='created', null=True, verbose_name='任务状态') # created, disabled
|
||||||
building_status = models.CharField(max_length=20, default='idle', null=True, verbose_name='构建状态') # idle, building
|
building_status = models.CharField(max_length=20, default='idle', null=True, verbose_name='构建状态') # idle, building
|
||||||
@@ -299,26 +275,8 @@ class BuildHistory(models.Model):
|
|||||||
requirement = models.TextField(null=True, blank=True, verbose_name='构建需求描述')
|
requirement = models.TextField(null=True, blank=True, verbose_name='构建需求描述')
|
||||||
build_log = models.TextField(null=True, blank=True, verbose_name='构建日志')
|
build_log = models.TextField(null=True, blank=True, verbose_name='构建日志')
|
||||||
stages = models.JSONField(default=list, verbose_name='构建阶段')
|
stages = models.JSONField(default=list, verbose_name='构建阶段')
|
||||||
parameter_values = models.JSONField(default=dict, verbose_name='构建参数值', help_text='''
|
parameter_values = models.JSONField(default=dict, verbose_name='构建参数值')
|
||||||
{
|
build_time = models.JSONField(default=dict, verbose_name='构建时间信息')
|
||||||
"MY_SERVICES": ["user-service", "order-service"],
|
|
||||||
"FEATURE_FLAGS": ["enable-cache"]
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
build_time = models.JSONField(default=dict, verbose_name='构建时间信息', help_text='''
|
|
||||||
{
|
|
||||||
"total_duration": "300", # 总耗时(秒)
|
|
||||||
"start_time": "2024-03-06 12:00:00", # 开始时间
|
|
||||||
"end_time": "2024-03-06 12:05:00", # 结束时间
|
|
||||||
"stages_time": [ # 各阶段时间信息
|
|
||||||
{
|
|
||||||
"name": "代码拉取",
|
|
||||||
"start_time": "2024-03-06 12:00:00",
|
|
||||||
"duration": "60" # 耗时(秒)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
|
|
||||||
operator = models.ForeignKey('User', on_delete=models.SET_NULL, to_field='user_id', null=True, verbose_name='构建人')
|
operator = models.ForeignKey('User', on_delete=models.SET_NULL, to_field='user_id', null=True, verbose_name='构建人')
|
||||||
create_time = models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')
|
create_time = models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')
|
||||||
@@ -329,7 +287,7 @@ class BuildHistory(models.Model):
|
|||||||
verbose_name = '构建历史'
|
verbose_name = '构建历史'
|
||||||
verbose_name_plural = verbose_name
|
verbose_name_plural = verbose_name
|
||||||
ordering = ['-create_time']
|
ordering = ['-create_time']
|
||||||
unique_together = ['task', 'build_number'] # 确保任务和构建号的组合唯一
|
unique_together = ['task', 'build_number']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.task.name} #{self.build_number}"
|
return f"{self.task.name} #{self.build_number}"
|
||||||
|
|||||||
@@ -154,6 +154,10 @@ class BuildTaskView(View):
|
|||||||
'external_script_directory': task.external_script_config.get('directory', '') if task.external_script_config else '',
|
'external_script_directory': task.external_script_config.get('directory', '') if task.external_script_config else '',
|
||||||
'external_script_branch': task.external_script_config.get('branch', '') if task.external_script_config else '',
|
'external_script_branch': task.external_script_config.get('branch', '') if task.external_script_config else '',
|
||||||
'external_script_token_id': task.external_script_config.get('token_id') if task.external_script_config else None,
|
'external_script_token_id': task.external_script_config.get('token_id') if task.external_script_config else None,
|
||||||
|
# 自动构建配置
|
||||||
|
'auto_build_enabled': task.auto_build_enabled,
|
||||||
|
'auto_build_branches': task.auto_build_branches,
|
||||||
|
'webhook_token': task.webhook_token,
|
||||||
'status': task.status,
|
'status': task.status,
|
||||||
'building_status': task.building_status, # 添加构建状态字段
|
'building_status': task.building_status, # 添加构建状态字段
|
||||||
'version': task.version,
|
'version': task.version,
|
||||||
@@ -339,6 +343,11 @@ class BuildTaskView(View):
|
|||||||
parameters = data.get('parameters', [])
|
parameters = data.get('parameters', [])
|
||||||
notification_channels = data.get('notification_channels', [])
|
notification_channels = data.get('notification_channels', [])
|
||||||
|
|
||||||
|
# 自动构建配置
|
||||||
|
auto_build_enabled = data.get('auto_build_enabled', False)
|
||||||
|
auto_build_branches = data.get('auto_build_branches', [])
|
||||||
|
webhook_token = data.get('webhook_token', '')
|
||||||
|
|
||||||
# 外部脚本库配置
|
# 外部脚本库配置
|
||||||
use_external_script = data.get('use_external_script')
|
use_external_script = data.get('use_external_script')
|
||||||
external_script_config = None
|
external_script_config = None
|
||||||
@@ -447,6 +456,11 @@ class BuildTaskView(View):
|
|||||||
'message': 'GitLab Token凭证不存在'
|
'message': 'GitLab Token凭证不存在'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 如果启用自动构建但没有webhook_token,生成一个
|
||||||
|
if auto_build_enabled and not webhook_token:
|
||||||
|
import secrets
|
||||||
|
webhook_token = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
# 创建构建任务
|
# 创建构建任务
|
||||||
creator = User.objects.get(user_id=request.user_id)
|
creator = User.objects.get(user_id=request.user_id)
|
||||||
task = BuildTask.objects.create(
|
task = BuildTask.objects.create(
|
||||||
@@ -462,6 +476,9 @@ class BuildTaskView(View):
|
|||||||
notification_channels=notification_channels,
|
notification_channels=notification_channels,
|
||||||
use_external_script=use_external_script,
|
use_external_script=use_external_script,
|
||||||
external_script_config=external_script_config,
|
external_script_config=external_script_config,
|
||||||
|
auto_build_enabled=auto_build_enabled,
|
||||||
|
auto_build_branches=auto_build_branches,
|
||||||
|
webhook_token=webhook_token,
|
||||||
creator=creator
|
creator=creator
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -505,6 +522,11 @@ class BuildTaskView(View):
|
|||||||
notification_channels = data.get('notification_channels')
|
notification_channels = data.get('notification_channels')
|
||||||
status = data.get('status')
|
status = data.get('status')
|
||||||
|
|
||||||
|
# 自动构建配置
|
||||||
|
auto_build_enabled = data.get('auto_build_enabled')
|
||||||
|
auto_build_branches = data.get('auto_build_branches')
|
||||||
|
webhook_token = data.get('webhook_token')
|
||||||
|
|
||||||
# 外部脚本库配置
|
# 外部脚本库配置
|
||||||
use_external_script = data.get('use_external_script')
|
use_external_script = data.get('use_external_script')
|
||||||
external_script_config = None
|
external_script_config = None
|
||||||
@@ -691,6 +713,25 @@ class BuildTaskView(View):
|
|||||||
task.use_external_script = use_external_script
|
task.use_external_script = use_external_script
|
||||||
task.external_script_config = external_script_config
|
task.external_script_config = external_script_config
|
||||||
|
|
||||||
|
# 更新自动构建配置
|
||||||
|
if 'auto_build_enabled' in data:
|
||||||
|
task.auto_build_enabled = auto_build_enabled
|
||||||
|
|
||||||
|
if auto_build_enabled:
|
||||||
|
# 如果启用自动构建但没有webhook_token,生成一个
|
||||||
|
if not task.webhook_token and not webhook_token:
|
||||||
|
import secrets
|
||||||
|
task.webhook_token = secrets.token_urlsafe(32)
|
||||||
|
elif webhook_token is not None:
|
||||||
|
task.webhook_token = webhook_token
|
||||||
|
else:
|
||||||
|
# 如果取消自动构建,清除所有相关配置
|
||||||
|
task.auto_build_branches = []
|
||||||
|
task.webhook_token = ''
|
||||||
|
|
||||||
|
if 'auto_build_branches' in data and auto_build_enabled:
|
||||||
|
task.auto_build_branches = auto_build_branches
|
||||||
|
|
||||||
task.save()
|
task.save()
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
|
|||||||
229
backend/apps/views/webhook.py
Normal file
229
backend/apps/views/webhook.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from ..models import BuildTask, BuildHistory, User
|
||||||
|
from ..utils.builder import Builder
|
||||||
|
import threading
|
||||||
|
|
||||||
|
logger = logging.getLogger('apps')
|
||||||
|
|
||||||
|
def execute_auto_build(task, branch, commit_id, commit_message, commit_author):
|
||||||
|
"""执行自动构建任务"""
|
||||||
|
try:
|
||||||
|
# 生成构建号
|
||||||
|
from django.db.models import F
|
||||||
|
from ..views.build import generate_id
|
||||||
|
|
||||||
|
# 更新任务构建号并获取新的构建号
|
||||||
|
task.last_build_number = F('last_build_number') + 1
|
||||||
|
task.total_builds = F('total_builds') + 1
|
||||||
|
task.building_status = 'building'
|
||||||
|
task.save()
|
||||||
|
|
||||||
|
# 重新获取任务以获取更新后的构建号
|
||||||
|
task.refresh_from_db()
|
||||||
|
build_number = task.last_build_number
|
||||||
|
|
||||||
|
# 创建构建历史记录
|
||||||
|
history = BuildHistory.objects.create(
|
||||||
|
history_id=generate_id(),
|
||||||
|
task=task,
|
||||||
|
build_number=build_number,
|
||||||
|
branch=branch,
|
||||||
|
commit_id=commit_id,
|
||||||
|
status='pending',
|
||||||
|
requirement=f"自动构建: {commit_message[:200]} (by {commit_author})", # 使用提交信息作为构建需求
|
||||||
|
parameter_values=get_default_parameter_values(task.parameters), # 使用默认参数值
|
||||||
|
operator=None # 自动构建没有操作人
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = Builder(task, build_number, commit_id, history)
|
||||||
|
builder.execute()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"自动构建执行失败: {str(e)}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
# 无论构建成功、失败或异常,都将构建状态重置为空闲
|
||||||
|
from django.db import transaction
|
||||||
|
with transaction.atomic():
|
||||||
|
BuildTask.objects.filter(task_id=task.task_id).update(building_status='idle')
|
||||||
|
logger.info(f"任务 [{task.task_id}] 自动构建状态已重置为空闲")
|
||||||
|
|
||||||
|
def get_default_parameter_values(parameters):
|
||||||
|
"""获取参数的默认值"""
|
||||||
|
if not parameters:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
default_values = {}
|
||||||
|
for param in parameters:
|
||||||
|
param_name = param.get('name')
|
||||||
|
default_list = param.get('default_values', [])
|
||||||
|
if param_name and default_list:
|
||||||
|
default_values[param_name] = default_list
|
||||||
|
|
||||||
|
return default_values
|
||||||
|
|
||||||
|
def is_branch_matched(branch_name, branch_list):
|
||||||
|
"""检查分支是否在配置的分支列表中"""
|
||||||
|
return branch_name in branch_list
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class GitLabWebhookView(View):
|
||||||
|
"""GitLab Webhook处理视图"""
|
||||||
|
|
||||||
|
def post(self, request, task_id):
|
||||||
|
"""处理GitLab Push Events"""
|
||||||
|
try:
|
||||||
|
# 验证token
|
||||||
|
token = request.GET.get('token')
|
||||||
|
if not token:
|
||||||
|
logger.warning(f"Webhook请求缺少token: task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'Missing token'
|
||||||
|
}, status=401)
|
||||||
|
|
||||||
|
# 查找对应的构建任务
|
||||||
|
try:
|
||||||
|
task = BuildTask.objects.get(task_id=task_id, webhook_token=token)
|
||||||
|
except BuildTask.DoesNotExist:
|
||||||
|
logger.warning(f"Webhook token验证失败: task_id={task_id}, token={token}")
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'Invalid task or token'
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
# 检查任务是否启用自动构建
|
||||||
|
if not task.auto_build_enabled:
|
||||||
|
logger.info(f"任务[{task_id}]未启用自动构建,忽略webhook")
|
||||||
|
return JsonResponse({
|
||||||
|
'message': 'Auto build is not enabled for this task'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 检查任务状态
|
||||||
|
if task.status == 'disabled':
|
||||||
|
logger.info(f"任务[{task_id}]已禁用,忽略webhook")
|
||||||
|
return JsonResponse({
|
||||||
|
'message': 'Task is disabled'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 是否有正在进行的构建
|
||||||
|
if task.building_status == 'building':
|
||||||
|
logger.info(f"任务[{task_id}]正在构建中,忽略webhook")
|
||||||
|
return JsonResponse({
|
||||||
|
'message': 'Build is already in progress'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 解析webhook数据
|
||||||
|
try:
|
||||||
|
webhook_data = json.loads(request.body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error(f"Webhook数据解析失败: task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'Invalid JSON data'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# 是否是push事件
|
||||||
|
event_name = request.headers.get('X-Gitlab-Event', '')
|
||||||
|
if event_name != 'Push Hook':
|
||||||
|
logger.info(f"忽略非Push事件: {event_name}, task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'message': f'Ignored event: {event_name}'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 提取分支信息
|
||||||
|
ref = webhook_data.get('ref', '')
|
||||||
|
if not ref.startswith('refs/heads/'):
|
||||||
|
logger.info(f"忽略非分支推送: {ref}, task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'message': f'Ignored non-branch push: {ref}'
|
||||||
|
})
|
||||||
|
|
||||||
|
branch = ref.replace('refs/heads/', '')
|
||||||
|
|
||||||
|
# 检查分支是否在自动构建配置中
|
||||||
|
if not is_branch_matched(branch, task.auto_build_branches):
|
||||||
|
logger.info(f"分支[{branch}]不在自动构建配置中,忽略webhook: task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'message': f'Branch {branch} is not configured for auto build'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 提取提交信息
|
||||||
|
commits = webhook_data.get('commits', [])
|
||||||
|
if not commits:
|
||||||
|
logger.warning(f"Webhook数据中没有提交信息: task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'No commits found in webhook data'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# 使用最新的提交
|
||||||
|
latest_commit = commits[-1]
|
||||||
|
commit_id = latest_commit.get('id', '')
|
||||||
|
commit_message = latest_commit.get('message', '').strip()
|
||||||
|
commit_author = latest_commit.get('author', {}).get('name', 'Unknown')
|
||||||
|
|
||||||
|
if not commit_id:
|
||||||
|
logger.error(f"提交ID为空: task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'Commit ID is empty'
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
env_type = task.environment.type if task.environment else None
|
||||||
|
if env_type not in ['development', 'testing']:
|
||||||
|
logger.warning(f"环境类型[{env_type}]不支持自动构建: task_id={task_id}")
|
||||||
|
return JsonResponse({
|
||||||
|
'message': f'Environment type {env_type} does not support auto build'
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"触发自动构建: task_id={task_id}, branch={branch}, commit={commit_id[:8]}, author={commit_author}")
|
||||||
|
|
||||||
|
# 在新线程中执行自动构建
|
||||||
|
build_thread = threading.Thread(
|
||||||
|
target=execute_auto_build,
|
||||||
|
args=(task, branch, commit_id, commit_message, commit_author)
|
||||||
|
)
|
||||||
|
build_thread.start()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'message': 'Auto build triggered successfully',
|
||||||
|
'task_id': task_id,
|
||||||
|
'branch': branch,
|
||||||
|
'commit_id': commit_id[:8],
|
||||||
|
'commit_message': commit_message[:100]
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Webhook处理失败: {str(e)}", exc_info=True)
|
||||||
|
return JsonResponse({
|
||||||
|
'error': f'Internal server error: {str(e)}'
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
def get(self, request, task_id):
|
||||||
|
"""用于测试webhook配置"""
|
||||||
|
try:
|
||||||
|
token = request.GET.get('token')
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'Missing token'
|
||||||
|
}, status=401)
|
||||||
|
|
||||||
|
try:
|
||||||
|
task = BuildTask.objects.get(task_id=task_id, webhook_token=token)
|
||||||
|
except BuildTask.DoesNotExist:
|
||||||
|
return JsonResponse({
|
||||||
|
'error': 'Invalid task or token'
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'message': 'Webhook configuration is valid',
|
||||||
|
'task_name': task.name,
|
||||||
|
'auto_build_enabled': task.auto_build_enabled,
|
||||||
|
'auto_build_branches': task.auto_build_branches
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Webhook测试失败: {str(e)}", exc_info=True)
|
||||||
|
return JsonResponse({
|
||||||
|
'error': f'Internal server error: {str(e)}'
|
||||||
|
}, status=500)
|
||||||
@@ -13,6 +13,7 @@ from apps.views.user import UserView, UserProfileView
|
|||||||
from apps.views.role import RoleView, UserPermissionView
|
from apps.views.role import RoleView, UserPermissionView
|
||||||
from apps.views.logs import login_logs_list, login_log_detail
|
from apps.views.logs import login_logs_list, login_log_detail
|
||||||
from apps.views.dashboard import DashboardStatsView, BuildTrendView, BuildDetailView, RecentBuildsView, ProjectDistributionView
|
from apps.views.dashboard import DashboardStatsView, BuildTrendView, BuildDetailView, RecentBuildsView, ProjectDistributionView
|
||||||
|
from apps.views.webhook import GitLabWebhookView
|
||||||
|
|
||||||
from apps.views.security import SecurityConfigView
|
from apps.views.security import SecurityConfigView
|
||||||
|
|
||||||
@@ -40,6 +41,9 @@ urlpatterns = [
|
|||||||
# SSE构建日志流
|
# SSE构建日志流
|
||||||
path('api/build/logs/stream/<str:task_id>/<str:build_number>/', BuildLogSSEView.as_view(), name='build-log-sse'),
|
path('api/build/logs/stream/<str:task_id>/<str:build_number>/', BuildLogSSEView.as_view(), name='build-log-sse'),
|
||||||
|
|
||||||
|
# GitLab Webhook
|
||||||
|
path('api/webhook/gitlab/<str:task_id>/', GitLabWebhookView.as_view(), name='gitlab-webhook'),
|
||||||
|
|
||||||
# 通知机器人相关路由
|
# 通知机器人相关路由
|
||||||
path('api/notification/robots/', NotificationRobotView.as_view(), name='notification-robots'),
|
path('api/notification/robots/', NotificationRobotView.as_view(), name='notification-robots'),
|
||||||
path('api/notification/robots/test/', NotificationTestView.as_view(), name='notification-robot-test'),
|
path('api/notification/robots/test/', NotificationTestView.as_view(), name='notification-robot-test'),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ host = 127.0.0.1
|
|||||||
#host = mysql
|
#host = mysql
|
||||||
#host = liteops-mysql
|
#host = liteops-mysql
|
||||||
port = 3306
|
port = 3306
|
||||||
database = liteops
|
database = liteops_dev
|
||||||
user = root
|
user = root
|
||||||
password = 1234567xx
|
password = 1234567xx
|
||||||
default-character-set = utf8mb4
|
default-character-set = utf8mb4
|
||||||
123
liteops_init.sql
123
liteops_init.sql
@@ -43,7 +43,7 @@ CREATE TABLE `build_history` (
|
|||||||
KEY `build_history_operator_id_f43bdff4_fk_user_user_id` (`operator_id`),
|
KEY `build_history_operator_id_f43bdff4_fk_user_user_id` (`operator_id`),
|
||||||
CONSTRAINT `build_history_operator_id_f43bdff4_fk_user_user_id` FOREIGN KEY (`operator_id`) REFERENCES `user` (`user_id`),
|
CONSTRAINT `build_history_operator_id_f43bdff4_fk_user_user_id` FOREIGN KEY (`operator_id`) REFERENCES `user` (`user_id`),
|
||||||
CONSTRAINT `build_history_task_id_dfb7725d_fk_build_task_task_id` FOREIGN KEY (`task_id`) REFERENCES `build_task` (`task_id`)
|
CONSTRAINT `build_history_task_id_dfb7725d_fk_build_task_task_id` FOREIGN KEY (`task_id`) REFERENCES `build_task` (`task_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for build_task
|
-- Table structure for build_task
|
||||||
@@ -71,10 +71,13 @@ CREATE TABLE `build_task` (
|
|||||||
`version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
`version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||||
`build_time` json NOT NULL DEFAULT (_utf8mb3'{}'),
|
`build_time` json NOT NULL DEFAULT (_utf8mb3'{}'),
|
||||||
`requirement` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
|
`requirement` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
|
||||||
`building_status` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
|
`building_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||||
`external_script_config` json NOT NULL DEFAULT (_utf8mb3'{}'),
|
`external_script_config` json NOT NULL DEFAULT (_utf8mb3'{}'),
|
||||||
`use_external_script` tinyint(1) NOT NULL,
|
`use_external_script` tinyint(1) NOT NULL,
|
||||||
`parameters` json NOT NULL DEFAULT (_utf8mb3'[]'),
|
`parameters` json NOT NULL DEFAULT (_utf8mb3'[]'),
|
||||||
|
`auto_build_branches` json NOT NULL DEFAULT (_utf8mb3'[]'),
|
||||||
|
`auto_build_enabled` tinyint(1) NOT NULL,
|
||||||
|
`webhook_token` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `task_id` (`task_id`),
|
UNIQUE KEY `task_id` (`task_id`),
|
||||||
KEY `build_task_creator_id_e702c745_fk_user_user_id` (`creator_id`),
|
KEY `build_task_creator_id_e702c745_fk_user_user_id` (`creator_id`),
|
||||||
@@ -85,95 +88,7 @@ CREATE TABLE `build_task` (
|
|||||||
CONSTRAINT `build_task_environment_id_8f5e7798_fk_environment_environment_id` FOREIGN KEY (`environment_id`) REFERENCES `environment` (`environment_id`),
|
CONSTRAINT `build_task_environment_id_8f5e7798_fk_environment_environment_id` FOREIGN KEY (`environment_id`) REFERENCES `environment` (`environment_id`),
|
||||||
CONSTRAINT `build_task_git_token_id_813ab2b1_fk_gitlab_to` FOREIGN KEY (`git_token_id`) REFERENCES `gitlab_token_credential` (`credential_id`),
|
CONSTRAINT `build_task_git_token_id_813ab2b1_fk_gitlab_to` FOREIGN KEY (`git_token_id`) REFERENCES `gitlab_token_credential` (`credential_id`),
|
||||||
CONSTRAINT `build_task_project_id_f92c80ac_fk_project_project_id` FOREIGN KEY (`project_id`) REFERENCES `project` (`project_id`)
|
CONSTRAINT `build_task_project_id_f92c80ac_fk_project_project_id` FOREIGN KEY (`project_id`) REFERENCES `project` (`project_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for django_admin_log
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `django_admin_log`;
|
|
||||||
CREATE TABLE `django_admin_log` (
|
|
||||||
`id` int NOT NULL AUTO_INCREMENT,
|
|
||||||
`action_time` datetime(6) NOT NULL,
|
|
||||||
`object_id` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
|
|
||||||
`object_repr` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`action_flag` smallint unsigned NOT NULL,
|
|
||||||
`change_message` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`content_type_id` int DEFAULT NULL,
|
|
||||||
`user_id` int NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `django_admin_log_content_type_id_c4bce8eb_fk_django_co` (`content_type_id`),
|
|
||||||
KEY `django_admin_log_user_id_c564eba6_fk_auth_user_id` (`user_id`),
|
|
||||||
CONSTRAINT `django_admin_log_content_type_id_c4bce8eb_fk_django_co` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`),
|
|
||||||
CONSTRAINT `django_admin_log_user_id_c564eba6_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
|
||||||
CONSTRAINT `django_admin_log_chk_1` CHECK ((`action_flag` >= 0))
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for django_apscheduler_djangojob
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `django_apscheduler_djangojob`;
|
|
||||||
CREATE TABLE `django_apscheduler_djangojob` (
|
|
||||||
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`next_run_time` datetime(6) DEFAULT NULL,
|
|
||||||
`job_state` longblob NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `django_apscheduler_djangojob_next_run_time_2f022619` (`next_run_time`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for django_apscheduler_djangojobexecution
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `django_apscheduler_djangojobexecution`;
|
|
||||||
CREATE TABLE `django_apscheduler_djangojobexecution` (
|
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
|
||||||
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`run_time` datetime(6) NOT NULL,
|
|
||||||
`duration` decimal(15,2) DEFAULT NULL,
|
|
||||||
`finished` decimal(15,2) DEFAULT NULL,
|
|
||||||
`exception` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
|
||||||
`traceback` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
|
|
||||||
`job_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `unique_job_executions` (`job_id`,`run_time`),
|
|
||||||
KEY `django_apscheduler_djangojobexecution_run_time_16edd96b` (`run_time`),
|
|
||||||
CONSTRAINT `django_apscheduler_djangojobexecution_job_id_daf5090a_fk` FOREIGN KEY (`job_id`) REFERENCES `django_apscheduler_djangojob` (`id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for django_content_type
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `django_content_type`;
|
|
||||||
CREATE TABLE `django_content_type` (
|
|
||||||
`id` int NOT NULL AUTO_INCREMENT,
|
|
||||||
`app_label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`model` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `django_content_type_app_label_model_76bd3d3b_uniq` (`app_label`,`model`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for django_migrations
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `django_migrations`;
|
|
||||||
CREATE TABLE `django_migrations` (
|
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
|
||||||
`app` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`applied` datetime(6) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
||||||
|
|
||||||
-- ----------------------------
|
|
||||||
-- Table structure for django_session
|
|
||||||
-- ----------------------------
|
|
||||||
DROP TABLE IF EXISTS `django_session`;
|
|
||||||
CREATE TABLE `django_session` (
|
|
||||||
`session_key` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`session_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
|
||||||
`expire_date` datetime(6) NOT NULL,
|
|
||||||
PRIMARY KEY (`session_key`),
|
|
||||||
KEY `django_session_expire_date_a5c62663` (`expire_date`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for environment
|
-- Table structure for environment
|
||||||
@@ -192,7 +107,7 @@ CREATE TABLE `environment` (
|
|||||||
UNIQUE KEY `environment_id` (`environment_id`),
|
UNIQUE KEY `environment_id` (`environment_id`),
|
||||||
KEY `environment_creator_id_2f30820a_fk_user_user_id` (`creator_id`),
|
KEY `environment_creator_id_2f30820a_fk_user_user_id` (`creator_id`),
|
||||||
CONSTRAINT `environment_creator_id_2f30820a_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `environment_creator_id_2f30820a_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for gitlab_token_credential
|
-- Table structure for gitlab_token_credential
|
||||||
@@ -211,7 +126,7 @@ CREATE TABLE `gitlab_token_credential` (
|
|||||||
UNIQUE KEY `credential_id` (`credential_id`),
|
UNIQUE KEY `credential_id` (`credential_id`),
|
||||||
KEY `gitlab_token_credential_creator_id_d53c3666_fk_user_user_id` (`creator_id`),
|
KEY `gitlab_token_credential_creator_id_d53c3666_fk_user_user_id` (`creator_id`),
|
||||||
CONSTRAINT `gitlab_token_credential_creator_id_d53c3666_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `gitlab_token_credential_creator_id_d53c3666_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for kubeconfig_credential
|
-- Table structure for kubeconfig_credential
|
||||||
@@ -232,7 +147,7 @@ CREATE TABLE `kubeconfig_credential` (
|
|||||||
UNIQUE KEY `credential_id` (`credential_id`),
|
UNIQUE KEY `credential_id` (`credential_id`),
|
||||||
KEY `kubeconfig_credential_creator_id_a3490ac1_fk_user_user_id` (`creator_id`),
|
KEY `kubeconfig_credential_creator_id_a3490ac1_fk_user_user_id` (`creator_id`),
|
||||||
CONSTRAINT `kubeconfig_credential_creator_id_a3490ac1_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `kubeconfig_credential_creator_id_a3490ac1_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for login_attempt
|
-- Table structure for login_attempt
|
||||||
@@ -249,7 +164,7 @@ CREATE TABLE `login_attempt` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `login_attempt_user_id_ip_address_a69098a0_uniq` (`user_id`,`ip_address`),
|
UNIQUE KEY `login_attempt_user_id_ip_address_a69098a0_uniq` (`user_id`,`ip_address`),
|
||||||
CONSTRAINT `login_attempt_user_id_0f42fcb7_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `login_attempt_user_id_0f42fcb7_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for login_log
|
-- Table structure for login_log
|
||||||
@@ -268,7 +183,7 @@ CREATE TABLE `login_log` (
|
|||||||
UNIQUE KEY `log_id` (`log_id`),
|
UNIQUE KEY `log_id` (`log_id`),
|
||||||
KEY `login_log_user_id_69642132_fk_user_user_id` (`user_id`),
|
KEY `login_log_user_id_69642132_fk_user_user_id` (`user_id`),
|
||||||
CONSTRAINT `login_log_user_id_69642132_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `login_log_user_id_69642132_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for notification_robot
|
-- Table structure for notification_robot
|
||||||
@@ -292,7 +207,7 @@ CREATE TABLE `notification_robot` (
|
|||||||
UNIQUE KEY `robot_id` (`robot_id`),
|
UNIQUE KEY `robot_id` (`robot_id`),
|
||||||
KEY `notification_robot_creator_id_de406276_fk_user_user_id` (`creator_id`),
|
KEY `notification_robot_creator_id_de406276_fk_user_user_id` (`creator_id`),
|
||||||
CONSTRAINT `notification_robot_creator_id_de406276_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `notification_robot_creator_id_de406276_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for project
|
-- Table structure for project
|
||||||
@@ -312,7 +227,7 @@ CREATE TABLE `project` (
|
|||||||
UNIQUE KEY `project_id` (`project_id`),
|
UNIQUE KEY `project_id` (`project_id`),
|
||||||
KEY `project_creator_id_e70918ae_fk_user_user_id` (`creator_id`),
|
KEY `project_creator_id_e70918ae_fk_user_user_id` (`creator_id`),
|
||||||
CONSTRAINT `project_creator_id_e70918ae_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `project_creator_id_e70918ae_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for role
|
-- Table structure for role
|
||||||
@@ -332,7 +247,7 @@ CREATE TABLE `role` (
|
|||||||
UNIQUE KEY `name` (`name`),
|
UNIQUE KEY `name` (`name`),
|
||||||
KEY `role_creator_id_37780e7e_fk_user_user_id` (`creator_id`),
|
KEY `role_creator_id_37780e7e_fk_user_user_id` (`creator_id`),
|
||||||
CONSTRAINT `role_creator_id_37780e7e_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `role_creator_id_37780e7e_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for security_config
|
-- Table structure for security_config
|
||||||
@@ -348,7 +263,7 @@ CREATE TABLE `security_config` (
|
|||||||
`enable_2fa` tinyint(1) NOT NULL,
|
`enable_2fa` tinyint(1) NOT NULL,
|
||||||
`update_time` datetime(6) DEFAULT NULL,
|
`update_time` datetime(6) DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for ssh_key_credential
|
-- Table structure for ssh_key_credential
|
||||||
@@ -368,7 +283,7 @@ CREATE TABLE `ssh_key_credential` (
|
|||||||
UNIQUE KEY `credential_id` (`credential_id`),
|
UNIQUE KEY `credential_id` (`credential_id`),
|
||||||
KEY `ssh_key_credential_creator_id_c7396682_fk_user_user_id` (`creator_id`),
|
KEY `ssh_key_credential_creator_id_c7396682_fk_user_user_id` (`creator_id`),
|
||||||
CONSTRAINT `ssh_key_credential_creator_id_c7396682_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `ssh_key_credential_creator_id_c7396682_fk_user_user_id` FOREIGN KEY (`creator_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for user
|
-- Table structure for user
|
||||||
@@ -389,7 +304,7 @@ CREATE TABLE `user` (
|
|||||||
UNIQUE KEY `user_id` (`user_id`),
|
UNIQUE KEY `user_id` (`user_id`),
|
||||||
UNIQUE KEY `username` (`username`),
|
UNIQUE KEY `username` (`username`),
|
||||||
UNIQUE KEY `email` (`email`)
|
UNIQUE KEY `email` (`email`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for user_role
|
-- Table structure for user_role
|
||||||
@@ -406,7 +321,7 @@ CREATE TABLE `user_role` (
|
|||||||
KEY `user_role_role_id_6a11361a_fk_role_role_id` (`role_id`),
|
KEY `user_role_role_id_6a11361a_fk_role_role_id` (`role_id`),
|
||||||
CONSTRAINT `user_role_role_id_6a11361a_fk_role_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`),
|
CONSTRAINT `user_role_role_id_6a11361a_fk_role_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`),
|
||||||
CONSTRAINT `user_role_user_id_12d84374_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `user_role_user_id_12d84374_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for user_token
|
-- Table structure for user_token
|
||||||
@@ -423,7 +338,7 @@ CREATE TABLE `user_token` (
|
|||||||
UNIQUE KEY `token_id` (`token_id`),
|
UNIQUE KEY `token_id` (`token_id`),
|
||||||
KEY `user_token_user_id_69e1f632_fk_user_user_id` (`user_id`),
|
KEY `user_token_user_id_69e1f632_fk_user_user_id` (`user_id`),
|
||||||
CONSTRAINT `user_token_user_id_69e1f632_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
CONSTRAINT `user_token_user_id_69e1f632_fk_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 初始化数据
|
-- 初始化数据
|
||||||
|
|||||||
@@ -211,6 +211,59 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a-divider>自动构建配置</a-divider>
|
||||||
|
<div class="auto-build-config">
|
||||||
|
<a-form-item>
|
||||||
|
<a-checkbox v-model:checked="formState.auto_build_enabled" @change="handleAutoBuildChange">
|
||||||
|
<ThunderboltOutlined style="color: #52c41a; margin-right: 4px;" />
|
||||||
|
启用自动构建
|
||||||
|
</a-checkbox>
|
||||||
|
<div class="config-description">当代码推送到配置的分支时,自动触发构建任务</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div v-if="formState.auto_build_enabled" class="auto-build-details">
|
||||||
|
<a-form-item label="自动构建分支" required>
|
||||||
|
<a-select
|
||||||
|
v-model:value="formState.auto_build_branches"
|
||||||
|
mode="tags"
|
||||||
|
placeholder="输入分支名称,如:main, develop, release-v1.0"
|
||||||
|
style="width: 100%"
|
||||||
|
:token-separators="[',', ' ']"
|
||||||
|
>
|
||||||
|
<a-select-option value="main">main</a-select-option>
|
||||||
|
<a-select-option value="master">master</a-select-option>
|
||||||
|
<a-select-option value="develop">develop</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<div class="form-item-help">请输入精确的分支名称,多个分支用逗号或空格分隔</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="Webhook URL">
|
||||||
|
<a-input
|
||||||
|
:value="webhookUrl"
|
||||||
|
readonly
|
||||||
|
:addonBefore="'POST'"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<a-tooltip title="复制Webhook URL">
|
||||||
|
<a-button type="text" size="small" @click="copyWebhookUrl">
|
||||||
|
<CopyOutlined />
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
<div class="form-item-help">将此URL配置到GitLab项目的Webhook中,触发事件选择"Push events"</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-alert
|
||||||
|
message="自动构建说明"
|
||||||
|
description="启用自动构建后,当有代码推送到配置的分支时,系统将自动触发构建。构建参数将使用默认值,构建需求描述将自动获取最新提交信息。"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
style="margin-top: 16px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a-divider>构建阶段</a-divider>
|
<a-divider>构建阶段</a-divider>
|
||||||
<div class="stages-list">
|
<div class="stages-list">
|
||||||
<div v-for="(stage, index) in formState.stages" :key="index" class="stage-item">
|
<div v-for="(stage, index) in formState.stages" :key="index" class="stage-item">
|
||||||
@@ -389,7 +442,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
@@ -485,6 +538,10 @@ const formState = reactive({
|
|||||||
],
|
],
|
||||||
parameters: [],
|
parameters: [],
|
||||||
notification_channels: [],
|
notification_channels: [],
|
||||||
|
// 自动构建配置
|
||||||
|
auto_build_enabled: false,
|
||||||
|
auto_build_branches: [],
|
||||||
|
webhook_token: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 表单校验规则
|
// 表单校验规则
|
||||||
@@ -756,6 +813,11 @@ const loadTaskDetail = async (taskId) => {
|
|||||||
|
|
||||||
formState.notification_channels = response.data.data.notification_channels || [];
|
formState.notification_channels = response.data.data.notification_channels || [];
|
||||||
|
|
||||||
|
// 自动构建配置
|
||||||
|
formState.auto_build_enabled = response.data.data.auto_build_enabled || false;
|
||||||
|
formState.auto_build_branches = response.data.data.auto_build_branches || [];
|
||||||
|
formState.webhook_token = response.data.data.webhook_token || '';
|
||||||
|
|
||||||
if (response.data.data.project) {
|
if (response.data.data.project) {
|
||||||
formState.project_id = response.data.data.project.project_id;
|
formState.project_id = response.data.data.project.project_id;
|
||||||
}
|
}
|
||||||
@@ -840,6 +902,11 @@ const loadTaskDetailForCopy = async (sourceTaskId) => {
|
|||||||
|
|
||||||
formState.notification_channels = response.data.data.notification_channels || [];
|
formState.notification_channels = response.data.data.notification_channels || [];
|
||||||
|
|
||||||
|
// 自动构建配置(复制时不复制webhook_token,会重新生成)
|
||||||
|
formState.auto_build_enabled = response.data.data.auto_build_enabled || false;
|
||||||
|
formState.auto_build_branches = response.data.data.auto_build_branches || [];
|
||||||
|
formState.webhook_token = ''; // 复制时重置webhook token
|
||||||
|
|
||||||
if (response.data.data.project) {
|
if (response.data.data.project) {
|
||||||
formState.project_id = response.data.data.project.project_id;
|
formState.project_id = response.data.data.project.project_id;
|
||||||
}
|
}
|
||||||
@@ -956,6 +1023,41 @@ const truncateUrl = (url) => {
|
|||||||
return url.length > 50 ? url.substring(0, 47) + '...' : url;
|
return url.length > 50 ? url.substring(0, 47) + '...' : url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Webhook URL
|
||||||
|
const webhookUrl = computed(() => {
|
||||||
|
if (!formState.task_id && !isEdit.value) {
|
||||||
|
return '保存任务后将生成Webhook URL';
|
||||||
|
}
|
||||||
|
const taskId = formState.task_id || 'TASK_ID';
|
||||||
|
const token = formState.webhook_token || 'WEBHOOK_TOKEN';
|
||||||
|
const baseUrl = window.location.origin.replace(':5173', ':8900'); // 开发环境端口映射
|
||||||
|
return `${baseUrl}/api/webhook/gitlab/${taskId}/?token=${token}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理自动构建配置变更
|
||||||
|
const handleAutoBuildChange = (checked) => {
|
||||||
|
if (checked && !formState.webhook_token) {
|
||||||
|
// 生成webhook token
|
||||||
|
formState.webhook_token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||||
|
}
|
||||||
|
if (!checked) {
|
||||||
|
// 取消自动构建时清除所有相关配置
|
||||||
|
formState.auto_build_branches = [];
|
||||||
|
formState.webhook_token = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制Webhook URL
|
||||||
|
const copyWebhookUrl = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(webhookUrl.value);
|
||||||
|
message.success('Webhook URL已复制到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
message.error('复制失败,请手动复制');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const taskId = route.query.task_id;
|
const taskId = route.query.task_id;
|
||||||
const sourceTaskId = route.query.source_task_id;
|
const sourceTaskId = route.query.source_task_id;
|
||||||
@@ -1161,4 +1263,21 @@ li {
|
|||||||
.external-script-summary :deep(.ant-descriptions-item-content) {
|
.external-script-summary :deep(.ant-descriptions-item-content) {
|
||||||
color: #262626;
|
color: #262626;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-build-config {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-build-details {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user