#feature: add project&cronjob management
This commit is contained in:
@@ -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) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user