#feature: add Jenkins deploy monitor & log clean task

This commit is contained in:
2026-01-19 11:46:38 +08:00
parent 381d5e6e49
commit da3b05b7c0
22 changed files with 968 additions and 80 deletions

View File

@@ -0,0 +1,294 @@
<?php
namespace App\Services;
use App\Clients\JenkinsClient;
use App\Models\JenkinsDeployment;
use App\Models\Project;
use Illuminate\Support\Facades\Log;
class JenkinsMonitorService
{
public function __construct(
private readonly JenkinsClient $jenkinsClient,
private readonly DingTalkService $dingTalkService,
private readonly ConfigService $configService
) {}
public function checkAllProjects(): array
{
if (!$this->jenkinsClient->isConfigured()) {
Log::warning('Jenkins client is not configured, skipping monitor');
return ['skipped' => true, 'reason' => 'Jenkins not configured'];
}
$projects = Project::getJenkinsNotifyEnabled();
$results = [];
foreach ($projects as $project) {
$results[$project->slug] = $this->checkProject($project);
}
return $results;
}
public function checkProject(Project $project): array
{
if (empty($project->jenkins_job_name)) {
return ['skipped' => true, 'reason' => 'No Jenkins job configured'];
}
$jobName = $project->jenkins_job_name;
$lastNotifiedBuild = $project->jenkins_last_notified_build ?? 0;
$allowedTriggers = $this->getAllowedTriggers();
$builds = $this->jenkinsClient->getBuilds($jobName, 5);
$newBuilds = [];
foreach ($builds as $build) {
// 只处理已完成的构建
if ($build['building'] ?? true) {
continue;
}
$buildNumber = $build['number'];
// 跳过已通知的构建
if ($buildNumber <= $lastNotifiedBuild) {
continue;
}
// 检查是否已存在记录
$exists = JenkinsDeployment::where('job_name', $jobName)
->where('build_number', $buildNumber)
->exists();
if ($exists) {
continue;
}
// 解析构建信息
$triggeredBy = $this->extractTriggeredBy($build);
$branch = $this->extractBranch($build);
$commitSha = $this->extractCommitSha($build);
$buildParams = $this->extractBuildParams($build);
// 过滤触发者(只通知指定用户触发的构建)
// 如果没有配置允许的触发者列表,则跳过所有通知
if (empty($allowedTriggers)) {
Log::info('Skipping build - no allowed triggers configured', [
'job' => $jobName,
'build' => $buildNumber,
'triggered_by' => $triggeredBy,
]);
continue;
}
// 检查触发者是否在允许列表中
if (!$this->isAllowedTrigger($triggeredBy, $allowedTriggers)) {
Log::info('Skipping build due to trigger filter', [
'job' => $jobName,
'build' => $buildNumber,
'triggered_by' => $triggeredBy,
]);
continue;
}
// 保存发布记录
$deployment = JenkinsDeployment::create([
'project_id' => $project->id,
'build_number' => $buildNumber,
'job_name' => $jobName,
'status' => $build['result'] ?? 'UNKNOWN',
'branch' => $branch,
'commit_sha' => $commitSha,
'triggered_by' => $triggeredBy,
'duration' => $build['duration'] ?? null,
'build_url' => $build['url'] ?? null,
'raw_data' => $build,
'build_params' => $buildParams,
'notified' => false,
]);
// 发送通知
$this->sendNotification($project, $deployment);
$deployment->update(['notified' => true]);
$newBuilds[] = $buildNumber;
}
// 更新最后通知的构建号
if (!empty($newBuilds)) {
$project->update([
'jenkins_last_notified_build' => max($newBuilds),
]);
}
return [
'job' => $jobName,
'new_builds' => $newBuilds,
];
}
private function sendNotification(Project $project, JenkinsDeployment $deployment): void
{
$lines = [];
$lines[] = sprintf(
"%s 【Jenkins 发布通知】",
$deployment->getStatusEmoji()
);
$lines[] = sprintf("项目: %s", $project->name);
$lines[] = sprintf("状态: %s", $deployment->getStatusLabel());
$lines[] = sprintf("构建号: #%d", $deployment->build_number);
$lines[] = sprintf("触发者: %s", $deployment->triggered_by ?? '-');
$lines[] = sprintf("耗时: %s", $deployment->getFormattedDuration());
// 添加构建参数
if (!empty($deployment->build_params)) {
$lines[] = "\n构建参数:";
foreach ($deployment->build_params as $key => $value) {
// 格式化参数值
if (is_bool($value)) {
$value = $value ? 'true' : 'false';
} elseif (is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
} elseif ($value === null || $value === '') {
continue; // 跳过空值
}
$lines[] = sprintf(" %s: %s", $key, $value);
}
}
$lines[] = sprintf("\n详情: %s", $deployment->build_url ?? '-');
$message = implode("\n", $lines);
$this->dingTalkService->sendText($message);
}
private function extractTriggeredBy(array $build): ?string
{
$actions = $build['actions'] ?? [];
foreach ($actions as $action) {
// UserIdCause - 用户手动触发
if (isset($action['causes'])) {
foreach ($action['causes'] as $cause) {
if (isset($cause['userId'])) {
return $cause['userId'];
}
if (isset($cause['userName'])) {
return $cause['userName'];
}
}
}
}
return null;
}
private function extractBranch(array $build): ?string
{
$actions = $build['actions'] ?? [];
// 优先从参数中获取 branchName
foreach ($actions as $action) {
if (isset($action['parameters'])) {
foreach ($action['parameters'] as $param) {
if (in_array($param['name'] ?? '', ['branchName', 'BRANCH', 'branch', 'GIT_BRANCH', 'BRANCH_NAME'])) {
$value = $param['value'] ?? null;
if (!empty($value)) {
return $value;
}
}
}
}
}
// 如果参数中没有,再从 Git 分支信息中获取
foreach ($actions as $action) {
if (isset($action['lastBuiltRevision']['branch'])) {
foreach ($action['lastBuiltRevision']['branch'] as $branch) {
$name = $branch['name'] ?? '';
// 移除 origin/ 和 refs/remotes/origin/ 前缀
return preg_replace('/^(refs\/remotes\/origin\/|origin\/)/', '', $name);
}
}
}
return null;
}
private function extractCommitSha(array $build): ?string
{
$actions = $build['actions'] ?? [];
foreach ($actions as $action) {
if (isset($action['lastBuiltRevision']['SHA1'])) {
return substr($action['lastBuiltRevision']['SHA1'], 0, 8);
}
}
return null;
}
private function getAllowedTriggers(): array
{
$config = $this->configService->get('jenkins_allowed_triggers', []);
// 如果配置为空,返回空数组
if (empty($config)) {
return [];
}
// 如果配置是数组,直接返回(过滤空值)
if (is_array($config)) {
return array_filter(array_map('trim', $config));
}
// 如果配置是字符串(兼容旧格式),按逗号分隔
if (is_string($config)) {
return array_filter(array_map('trim', explode(',', $config)));
}
return [];
}
private function isAllowedTrigger(?string $triggeredBy, array $allowedTriggers): bool
{
if (empty($triggeredBy)) {
return false;
}
foreach ($allowedTriggers as $allowed) {
if (strcasecmp($triggeredBy, $allowed) === 0) {
return true;
}
}
return false;
}
private function extractBuildParams(array $build): array
{
$actions = $build['actions'] ?? [];
foreach ($actions as $action) {
// 查找 ParametersAction
if (isset($action['_class']) && $action['_class'] === 'hudson.model.ParametersAction') {
if (isset($action['parameters']) && is_array($action['parameters'])) {
$params = [];
foreach ($action['parameters'] as $param) {
$name = $param['name'] ?? null;
$value = $param['value'] ?? null;
if ($name !== null) {
$params[$name] = $value;
}
}
return $params;
}
}
}
return [];
}
}