#feature: add project&cronjob management

This commit is contained in:
2026-01-16 12:14:43 +08:00
parent bbe68839e3
commit 381d5e6e49
19 changed files with 2157 additions and 84 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Project;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
@@ -10,9 +11,8 @@ use Symfony\Component\Process\Process;
class GitMonitorService
{
private const RELEASE_CACHE_KEY = 'git_monitor.current_versions';
private const LAST_CHECKED_KEY = 'git_monitor.last_checked_commits';
private const DEVELOP_BRANCH = 'develop';
private const RELEASE_CACHE_KEY = 'git-monitor.release_cache';
/**
* 项目配置(只包含允许巡检的项目)
@@ -30,12 +30,6 @@ class GitMonitorService
) {
$this->projectsPath = $this->resolveProjectsPath();
$this->projects = $this->resolveProjects();
$enabled = config('git-monitor.enabled_projects', []);
if (!empty($enabled)) {
$this->projects = array_intersect_key($this->projects, array_flip($enabled));
}
$this->commitScanLimit = (int) config('git-monitor.commit_scan_limit', 30);
$this->gitTimeout = (int) config('git-monitor.git_timeout', 180);
}
@@ -46,10 +40,11 @@ class GitMonitorService
return [];
}
// 检查是否需要刷新缓存
if (!$force) {
$cached = $this->configService->get(self::RELEASE_CACHE_KEY);
if (!empty($cached) && !$this->shouldRefreshCache($cached)) {
return $cached;
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
if ($anyProject && !$this->shouldRefreshCache($anyProject->git_version_cached_at)) {
return $this->buildCachePayload();
}
}
@@ -72,12 +67,29 @@ class GitMonitorService
// 根据当前版本号获取下一个版本
$version = $this->jiraService->getUpcomingReleaseVersion($projectKey, $currentVersion);
if ($version) {
$branch = 'release/' . $version['version'];
$payload['repositories'][$repoKey] = [
'version' => $version['version'],
'description' => $version['description'] ?? null,
'release_date' => $version['release_date'],
'branch' => 'release/' . $version['version'],
'branch' => $branch,
'current_version' => $currentVersion,
];
// 更新 Project 模型
$project = Project::findBySlug($repoKey);
if ($project) {
$project->update([
'git_current_version' => $currentVersion,
'git_release_branch' => $branch,
'git_version_cached_at' => now(),
]);
// 如果启用了自动创建 release 分支,检查并创建
if ($project->auto_create_release_branch) {
$this->ensureReleaseBranchExists($repoKey, $repoConfig, $branch, $version['description']);
}
}
}
} catch (\Throwable $e) {
Log::error('Failed to fetch release version from Jira', [
@@ -87,24 +99,44 @@ class GitMonitorService
}
}
$this->configService->set(
self::RELEASE_CACHE_KEY,
$payload,
'Cached release versions fetched from Jira'
);
return $payload;
}
public function ensureReleaseCache(): array
{
$cached = $this->configService->get(self::RELEASE_CACHE_KEY);
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
if (empty($cached) || $this->shouldRefreshCache($cached)) {
if (!$anyProject || $this->shouldRefreshCache($anyProject->git_version_cached_at)) {
return $this->refreshReleaseCache(true);
}
return $cached;
return $this->buildCachePayload();
}
/**
* Project 模型构建缓存数据
*/
private function buildCachePayload(): array
{
$projects = Project::query()
->where('git_monitor_enabled', true)
->whereNotNull('git_release_branch')
->get();
$payload = [
'cached_at' => $projects->first()?->git_version_cached_at?->toDateTimeString() ?? now()->toDateTimeString(),
'repositories' => [],
];
foreach ($projects as $project) {
$payload['repositories'][$project->slug] = [
'version' => $project->git_current_version,
'branch' => $project->git_release_branch,
'current_version' => $project->git_current_version,
];
}
return $payload;
}
public function checkRepositories(bool $refreshCacheIfNeeded = true): array
@@ -151,15 +183,15 @@ class GitMonitorService
return $results;
}
private function shouldRefreshCache(array $cached): bool
private function shouldRefreshCache(Carbon|\DateTimeInterface|string|null $cachedAt): bool
{
$cachedAt = Arr::get($cached, 'cached_at');
if (empty($cachedAt)) {
return true;
}
try {
return Carbon::parse($cachedAt)->lt(Carbon::now()->startOfDay());
$cachedTime = $cachedAt instanceof Carbon ? $cachedAt : Carbon::parse($cachedAt);
return $cachedTime->lt(Carbon::now()->startOfDay());
} catch (\Throwable) {
return true;
}
@@ -177,7 +209,9 @@ class GitMonitorService
$remoteBranch = 'origin/' . $branch;
$head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]);
$lastChecked = $this->configService->getNested(self::LAST_CHECKED_KEY, $repoKey);
// 从 Project 模型获取 lastChecked
$project = Project::findBySlug($repoKey);
$lastChecked = $project?->git_last_checked_commit;
$commits = $this->collectCommits($path, $remoteBranch, $lastChecked);
$issues = [
@@ -378,14 +412,10 @@ class GitMonitorService
private function updateLastChecked(string $repoKey, string $sha): void
{
$current = $this->configService->get(self::LAST_CHECKED_KEY, []);
$current[$repoKey] = $sha;
$this->configService->set(
self::LAST_CHECKED_KEY,
$current,
'Last scanned release commits'
);
$project = Project::findBySlug($repoKey);
if ($project) {
$project->update(['git_last_checked_commit' => $sha]);
}
}
private function getCommitMetadata(string $repoPath, string $commit): array
@@ -478,22 +508,37 @@ class GitMonitorService
*/
private function resolveProjects(): array
{
$projects = $this->configService->get('workspace.repositories');
// 优先从 Project 模型获取启用 Git 监控的项目
$projectModels = Project::query()
->where('git_monitor_enabled', true)
->get();
if ($projects === null) {
$fallback = $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
if (!empty($fallback)) {
Log::warning('configs 表未设置 workspace.repositories已使用 git-monitor.enabled_projects。');
if ($projectModels->isNotEmpty()) {
$projects = [];
foreach ($projectModels as $project) {
$projects[$project->slug] = [
'jira_project' => $project->jira_project_code,
'directory' => $project->directory ?? $project->slug,
'path' => $project->absolute_path,
'display' => $project->name,
];
}
return $fallback;
return $projects;
}
if (!is_array($projects)) {
Log::warning('configs 表 workspace.repositories 配置格式不正确,已降级使用 git-monitor.enabled_projects。');
return $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
// 回退到旧的配置方式(兼容迁移前的情况)
$configProjects = $this->configService->get('workspace.repositories');
if ($configProjects !== null && is_array($configProjects)) {
$enabled = config('git-monitor.enabled_projects', []);
if (!empty($enabled)) {
return array_intersect_key($configProjects, array_flip($enabled));
}
return $configProjects;
}
return $projects;
// 最后回退到环境变量配置
return $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
}
/**
@@ -554,4 +599,100 @@ class GitMonitorService
return null;
}
}
/**
* 检查远程是否存在指定分支
*/
private function remoteBranchExists(string $path, string $branch): bool
{
try {
$this->runGit($path, ['git', 'fetch', 'origin']);
$this->runGit($path, ['git', 'ls-remote', '--exit-code', '--heads', 'origin', $branch]);
return true;
} catch (ProcessFailedException) {
return false;
}
}
/**
* 确保 release 分支存在,如果不存在则从 master 创建并推送
*/
private function ensureReleaseBranchExists(string $repoKey, array $repoConfig, string $branch, ?string $description): void
{
$path = $this->resolveProjectPath($repoKey, $repoConfig);
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;
}
// 从分支名中提取版本号release/2.63.0.0 -> 2.63.0.0
$version = str_replace('release/', '', $branch);
try {
// 从 origin/master 创建新分支
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
// 创建本地分支(基于 origin/master
try {
// 先尝试删除可能存在的本地分支
$this->runGit($path, ['git', 'branch', '-D', $branch]);
} catch (ProcessFailedException) {
// 忽略,分支可能不存在
}
$this->runGit($path, ['git', 'checkout', '-b', $branch, 'origin/master']);
// 修改 version.txt 文件
$versionFile = $path . DIRECTORY_SEPARATOR . 'version.txt';
if (!file_put_contents($versionFile, $version)) {
throw new \RuntimeException("Failed to write version.txt");
}
// 添加并提交更改
$this->runGit($path, ['git', 'add', 'version.txt']);
// 构建提交信息:分支名 + 空格 + Jira 描述
$commitMessage = $branch . ($description ? ' ' . $description : '');
$this->runGit($path, ['git', 'commit', '-m', $commitMessage]);
// 推送到远程
$this->runGit($path, ['git', 'push', '-u', 'origin', $branch]);
Log::info('Created and pushed release branch', [
'repository' => $repoKey,
'branch' => $branch,
'version' => $version,
'message' => $commitMessage,
]);
// 发送钉钉通知
$this->dingTalkService->sendText(sprintf(
"【Release 分支创建】\n项目: %s\n分支: %s\n版本: %s\n描述: %s",
$repoConfig['display'] ?? $repoKey,
$branch,
$version,
$description ?? '无'
));
} catch (ProcessFailedException $e) {
Log::error('Failed to create release branch', [
'repository' => $repoKey,
'branch' => $branch,
'error' => $e->getMessage(),
]);
} finally {
// 切回 develop 分支,避免影响后续操作
try {
$this->runGit($path, ['git', 'checkout', self::DEVELOP_BRANCH]);
} catch (ProcessFailedException) {
// 忽略
}
}
}
}