#feature: Jenkins 构建入口与自动创建 release 分支
加入 crm-web/crm-opm 并可跳转 Jenkins 发布页;自动创建 release 时 commit 使用 release-版本号,并跳过本地 git hook,避免 agent-be 被拦下。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,6 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Project;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||
use Symfony\Component\Process\Process;
|
||||
@@ -12,15 +11,20 @@ use Symfony\Component\Process\Process;
|
||||
class GitMonitorService
|
||||
{
|
||||
private const DEVELOP_BRANCH = 'develop';
|
||||
|
||||
private const RELEASE_CACHE_KEY = 'git-monitor.release_cache';
|
||||
|
||||
/**
|
||||
* 项目配置(只包含允许巡检的项目)
|
||||
*
|
||||
* @var array<string, array<string, mixed>>
|
||||
*/
|
||||
private array $projects = [];
|
||||
|
||||
private string $projectsPath;
|
||||
|
||||
private int $commitScanLimit;
|
||||
|
||||
private int $gitTimeout;
|
||||
|
||||
public function __construct(
|
||||
@@ -41,9 +45,9 @@ class GitMonitorService
|
||||
}
|
||||
|
||||
// 检查是否需要刷新缓存
|
||||
if (!$force) {
|
||||
if (! $force) {
|
||||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
||||
if ($anyProject && !$this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||||
if ($anyProject && ! $this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||||
return $this->buildCachePayload();
|
||||
}
|
||||
}
|
||||
@@ -57,6 +61,7 @@ class GitMonitorService
|
||||
$projectKey = $repoConfig['jira_project'] ?? null;
|
||||
if (empty($projectKey)) {
|
||||
Log::warning('Jira project key missing for repository', ['repository' => $repoKey]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -67,7 +72,7 @@ class GitMonitorService
|
||||
// 根据当前版本号获取下一个版本
|
||||
$version = $this->jiraService->getUpcomingReleaseVersion($projectKey, $currentVersion);
|
||||
if ($version) {
|
||||
$branch = 'release/' . $version['version'];
|
||||
$branch = 'release/'.$version['version'];
|
||||
$payload['repositories'][$repoKey] = [
|
||||
'version' => $version['version'],
|
||||
'description' => $version['description'] ?? null,
|
||||
@@ -106,7 +111,7 @@ class GitMonitorService
|
||||
{
|
||||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
||||
|
||||
if (!$anyProject || $this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||||
if (! $anyProject || $this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||||
return $this->refreshReleaseCache(true);
|
||||
}
|
||||
|
||||
@@ -152,6 +157,7 @@ class GitMonitorService
|
||||
$branch = data_get($releaseCache, "repositories.{$repoKey}.branch");
|
||||
if (empty($branch)) {
|
||||
Log::warning('Missing release branch info for repository', ['repository' => $repoKey]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -176,7 +182,7 @@ class GitMonitorService
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($alerts)) {
|
||||
if (! empty($alerts)) {
|
||||
$this->dingTalkService->sendText($this->buildAlertMessage($alerts));
|
||||
}
|
||||
|
||||
@@ -191,6 +197,7 @@ class GitMonitorService
|
||||
|
||||
try {
|
||||
$cachedTime = $cachedAt instanceof Carbon ? $cachedAt : Carbon::parse($cachedAt);
|
||||
|
||||
return $cachedTime->lt(Carbon::now()->startOfDay());
|
||||
} catch (\Throwable) {
|
||||
return true;
|
||||
@@ -201,12 +208,12 @@ class GitMonitorService
|
||||
{
|
||||
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
||||
|
||||
if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) {
|
||||
if (! is_dir($path) || ! is_dir($path.DIRECTORY_SEPARATOR.'.git')) {
|
||||
throw new \RuntimeException("Project path {$path} is not a valid git repository");
|
||||
}
|
||||
|
||||
$this->synchronizeRepository($path, $branch);
|
||||
$remoteBranch = 'origin/' . $branch;
|
||||
$remoteBranch = 'origin/'.$branch;
|
||||
$head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]);
|
||||
|
||||
// 从 Project 模型获取 lastChecked
|
||||
@@ -231,7 +238,7 @@ class GitMonitorService
|
||||
// 只在 merge 提交或冲突解决提交中检测缺失函数
|
||||
if ($isMerge || $isConflictResolution) {
|
||||
$missingFunctions = $this->detectMissingFunctions($path, $commit);
|
||||
if (!empty($missingFunctions)) {
|
||||
if (! empty($missingFunctions)) {
|
||||
$issues['missing_functions'][] = [
|
||||
'commit' => $this->getCommitMetadata($path, $commit),
|
||||
'details' => $missingFunctions,
|
||||
@@ -346,11 +353,13 @@ class GitMonitorService
|
||||
foreach (preg_split('/\R/', $diff) as $line) {
|
||||
if (str_starts_with($line, 'diff --git')) {
|
||||
$currentFile = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($line, '+++ b/')) {
|
||||
$currentFile = substr($line, 6);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -358,7 +367,7 @@ class GitMonitorService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$currentFile || !str_ends_with($currentFile, '.php')) {
|
||||
if (! $currentFile || ! str_ends_with($currentFile, '.php')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -367,6 +376,7 @@ class GitMonitorService
|
||||
if ($function) {
|
||||
$removed[$currentFile][] = $function;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -381,7 +391,7 @@ class GitMonitorService
|
||||
$issues = [];
|
||||
foreach ($removed as $file => $functions) {
|
||||
$diffed = array_diff($functions, $added[$file] ?? []);
|
||||
if (!empty($diffed)) {
|
||||
if (! empty($diffed)) {
|
||||
$issues[] = [
|
||||
'file' => $file,
|
||||
'functions' => array_values(array_unique($diffed)),
|
||||
@@ -422,7 +432,7 @@ class GitMonitorService
|
||||
{
|
||||
$separator = "\x1F";
|
||||
$format = "%H{$separator}%an{$separator}%ad{$separator}%s";
|
||||
$raw = $this->runGit($repoPath, ['git', 'show', '-s', "--date=iso-strict", "--pretty={$format}", $commit]);
|
||||
$raw = $this->runGit($repoPath, ['git', 'show', '-s', '--date=iso-strict', "--pretty={$format}", $commit]);
|
||||
$parts = array_pad(explode($separator, $raw, 4), 4, '');
|
||||
|
||||
return [
|
||||
@@ -435,7 +445,7 @@ class GitMonitorService
|
||||
|
||||
private function hasIssues(array $issues): bool
|
||||
{
|
||||
return !empty($issues['develop_merges']) || !empty($issues['missing_functions']);
|
||||
return ! empty($issues['develop_merges']) || ! empty($issues['missing_functions']);
|
||||
}
|
||||
|
||||
private function buildAlertMessage(array $alerts): string
|
||||
@@ -445,7 +455,7 @@ class GitMonitorService
|
||||
foreach ($alerts as $result) {
|
||||
$lines[] = sprintf('%s(%s)', $result['display'], $result['branch']);
|
||||
|
||||
if (!empty($result['issues']['develop_merges'])) {
|
||||
if (! empty($result['issues']['develop_merges'])) {
|
||||
$lines[] = ' - 检测到 develop 合并:';
|
||||
foreach ($result['issues']['develop_merges'] as $commit) {
|
||||
$lines[] = sprintf(
|
||||
@@ -457,7 +467,7 @@ class GitMonitorService
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($result['issues']['missing_functions'])) {
|
||||
if (! empty($result['issues']['missing_functions'])) {
|
||||
$lines[] = ' - 疑似缺失函数:';
|
||||
|
||||
foreach ($result['issues']['missing_functions'] as $issue) {
|
||||
@@ -523,6 +533,7 @@ class GitMonitorService
|
||||
'display' => $project->name,
|
||||
];
|
||||
}
|
||||
|
||||
return $projects;
|
||||
}
|
||||
|
||||
@@ -531,9 +542,10 @@ class GitMonitorService
|
||||
|
||||
if ($configProjects !== null && is_array($configProjects)) {
|
||||
$enabled = config('git-monitor.enabled_projects', []);
|
||||
if (!empty($enabled)) {
|
||||
if (! empty($enabled)) {
|
||||
return array_intersect_key($configProjects, array_flip($enabled));
|
||||
}
|
||||
|
||||
return $configProjects;
|
||||
}
|
||||
|
||||
@@ -553,7 +565,7 @@ class GitMonitorService
|
||||
];
|
||||
|
||||
foreach ($enabled as $repoKey) {
|
||||
if (!is_string($repoKey) || $repoKey === '') {
|
||||
if (! is_string($repoKey) || $repoKey === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -567,12 +579,13 @@ class GitMonitorService
|
||||
|
||||
private function resolveProjectPath(string $repoKey, array $repoConfig): string
|
||||
{
|
||||
if (!empty($repoConfig['path'])) {
|
||||
if (! empty($repoConfig['path'])) {
|
||||
return rtrim($repoConfig['path'], '/');
|
||||
}
|
||||
|
||||
$directory = $repoConfig['directory'] ?? $repoKey;
|
||||
return $this->projectsPath . '/' . ltrim($directory, '/');
|
||||
|
||||
return $this->projectsPath.'/'.ltrim($directory, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -582,20 +595,23 @@ class GitMonitorService
|
||||
{
|
||||
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
||||
|
||||
if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) {
|
||||
if (! is_dir($path) || ! is_dir($path.DIRECTORY_SEPARATOR.'.git')) {
|
||||
Log::warning('Invalid git repository path', ['repository' => $repoKey, 'path' => $path]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
|
||||
$version = $this->runGit($path, ['git', 'show', 'origin/master:version.txt']);
|
||||
|
||||
return trim($version) ?: null;
|
||||
} catch (ProcessFailedException $e) {
|
||||
Log::warning('Failed to read version.txt from master branch', [
|
||||
'repository' => $repoKey,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -608,6 +624,7 @@ class GitMonitorService
|
||||
try {
|
||||
$this->runGit($path, ['git', 'fetch', 'origin']);
|
||||
$this->runGit($path, ['git', 'ls-remote', '--exit-code', '--heads', 'origin', $branch]);
|
||||
|
||||
return true;
|
||||
} catch (ProcessFailedException) {
|
||||
return false;
|
||||
@@ -623,14 +640,16 @@ class GitMonitorService
|
||||
$worktreePath = null;
|
||||
$worktreeCreated = false;
|
||||
|
||||
if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) {
|
||||
if (! is_dir($path) || ! is_dir($path.DIRECTORY_SEPARATOR.'.git')) {
|
||||
Log::warning('Invalid git repository path for branch creation', ['repository' => $repoKey, 'path' => $path]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查远程分支是否已存在
|
||||
if ($this->remoteBranchExists($path, $branch)) {
|
||||
Log::info('Release branch already exists on remote', ['repository' => $repoKey, 'branch' => $branch]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -640,25 +659,25 @@ class GitMonitorService
|
||||
try {
|
||||
// 在临时 worktree 中创建并推送分支,避免切换或修改用户正在工作的仓库。
|
||||
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
|
||||
$worktreePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'toolbox-release-' . str_replace(['/', '\\'], '-', $repoKey . '-' . $version) . '-' . bin2hex(random_bytes(4));
|
||||
$worktreePath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'toolbox-release-'.str_replace(['/', '\\'], '-', $repoKey.'-'.$version).'-'.bin2hex(random_bytes(4));
|
||||
$this->runGit($path, ['git', 'worktree', 'add', '--detach', $worktreePath, 'origin/master']);
|
||||
$worktreeCreated = true;
|
||||
|
||||
// 修改 version.txt 文件
|
||||
$versionFile = $worktreePath . DIRECTORY_SEPARATOR . 'version.txt';
|
||||
if (!file_put_contents($versionFile, $version)) {
|
||||
throw new \RuntimeException("Failed to write version.txt");
|
||||
$versionFile = $worktreePath.DIRECTORY_SEPARATOR.'version.txt';
|
||||
if (! file_put_contents($versionFile, $version)) {
|
||||
throw new \RuntimeException('Failed to write version.txt');
|
||||
}
|
||||
|
||||
// 添加并提交更改
|
||||
$this->runGit($worktreePath, ['git', 'add', 'version.txt']);
|
||||
|
||||
// 构建提交信息:分支名 + 空格 + Jira 描述
|
||||
$commitMessage = $branch . ($description ? ' ' . $description : '');
|
||||
$this->runGit($worktreePath, ['git', 'commit', '-m', $commitMessage]);
|
||||
// 构建提交信息:release-版本号 + 空格 + Jira 描述
|
||||
$commitMessage = 'release-'.$version.($description ? ' '.$description : '');
|
||||
$this->runGit($worktreePath, ['git', 'commit', '--no-verify', '-m', $commitMessage]);
|
||||
|
||||
// 推送到远程
|
||||
$this->runGit($worktreePath, ['git', 'push', 'origin', 'HEAD:refs/heads/' . $branch]);
|
||||
$this->runGit($worktreePath, ['git', 'push', '--no-verify', 'origin', 'HEAD:refs/heads/'.$branch]);
|
||||
|
||||
Log::info('Created and pushed release branch', [
|
||||
'repository' => $repoKey,
|
||||
|
||||
Reference in New Issue
Block a user