699 lines
24 KiB
PHP
699 lines
24 KiB
PHP
<?php
|
||
|
||
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;
|
||
|
||
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(
|
||
private readonly ConfigService $configService,
|
||
private readonly JiraService $jiraService,
|
||
private readonly DingTalkService $dingTalkService
|
||
) {
|
||
$this->projectsPath = $this->resolveProjectsPath();
|
||
$this->projects = $this->resolveProjects();
|
||
$this->commitScanLimit = (int) config('git-monitor.commit_scan_limit', 30);
|
||
$this->gitTimeout = (int) config('git-monitor.git_timeout', 180);
|
||
}
|
||
|
||
public function refreshReleaseCache(bool $force = false): array
|
||
{
|
||
if (empty($this->projects)) {
|
||
return [];
|
||
}
|
||
|
||
// 检查是否需要刷新缓存
|
||
if (!$force) {
|
||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
||
if ($anyProject && !$this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||
return $this->buildCachePayload();
|
||
}
|
||
}
|
||
|
||
$payload = [
|
||
'cached_at' => Carbon::now()->toDateTimeString(),
|
||
'repositories' => [],
|
||
];
|
||
|
||
foreach ($this->projects as $repoKey => $repoConfig) {
|
||
$projectKey = $repoConfig['jira_project'] ?? null;
|
||
if (empty($projectKey)) {
|
||
Log::warning('Jira project key missing for repository', ['repository' => $repoKey]);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
// 先从 master 分支获取当前版本号
|
||
$currentVersion = $this->getMasterVersion($repoKey, $repoConfig);
|
||
|
||
// 根据当前版本号获取下一个版本
|
||
$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' => $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', [
|
||
'project' => $projectKey,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
return $payload;
|
||
}
|
||
|
||
public function ensureReleaseCache(): array
|
||
{
|
||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
||
|
||
if (!$anyProject || $this->shouldRefreshCache($anyProject->git_version_cached_at)) {
|
||
return $this->refreshReleaseCache(true);
|
||
}
|
||
|
||
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
|
||
{
|
||
$releaseCache = $refreshCacheIfNeeded
|
||
? $this->ensureReleaseCache()
|
||
: ($this->configService->get(self::RELEASE_CACHE_KEY) ?? []);
|
||
|
||
$results = [];
|
||
$alerts = [];
|
||
|
||
foreach ($this->projects as $repoKey => $repoConfig) {
|
||
$branch = data_get($releaseCache, "repositories.{$repoKey}.branch");
|
||
if (empty($branch)) {
|
||
Log::warning('Missing release branch info for repository', ['repository' => $repoKey]);
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
$result = $this->inspectRepository($repoKey, $repoConfig, $branch);
|
||
$results[$repoKey] = $result;
|
||
|
||
if ($this->hasIssues($result['issues'])) {
|
||
$alerts[$repoKey] = $result;
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::error('Git monitoring failed for repository', [
|
||
'repository' => $repoKey,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
|
||
$results[$repoKey] = [
|
||
'repository' => $repoKey,
|
||
'branch' => $branch,
|
||
'error' => $e->getMessage(),
|
||
];
|
||
}
|
||
}
|
||
|
||
if (!empty($alerts)) {
|
||
$this->dingTalkService->sendText($this->buildAlertMessage($alerts));
|
||
}
|
||
|
||
return $results;
|
||
}
|
||
|
||
private function shouldRefreshCache(Carbon|\DateTimeInterface|string|null $cachedAt): bool
|
||
{
|
||
if (empty($cachedAt)) {
|
||
return true;
|
||
}
|
||
|
||
try {
|
||
$cachedTime = $cachedAt instanceof Carbon ? $cachedAt : Carbon::parse($cachedAt);
|
||
return $cachedTime->lt(Carbon::now()->startOfDay());
|
||
} catch (\Throwable) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
private function inspectRepository(string $repoKey, array $repoConfig, string $branch): array
|
||
{
|
||
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
||
|
||
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;
|
||
$head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]);
|
||
|
||
// 从 Project 模型获取 lastChecked
|
||
$project = Project::findBySlug($repoKey);
|
||
$lastChecked = $project?->git_last_checked_commit;
|
||
$commits = $this->collectCommits($path, $remoteBranch, $lastChecked);
|
||
|
||
$issues = [
|
||
'develop_merges' => [],
|
||
'missing_functions' => [],
|
||
];
|
||
|
||
foreach ($commits as $commit) {
|
||
$isMerge = $this->isMergeCommit($path, $commit);
|
||
$isConflictResolution = $this->isConflictResolution($path, $commit);
|
||
|
||
// 只检测直接从 develop 合并的情况
|
||
if ($isMerge && $this->isDevelopMerge($path, $commit)) {
|
||
$issues['develop_merges'][] = $this->getCommitMetadata($path, $commit);
|
||
}
|
||
|
||
// 只在 merge 提交或冲突解决提交中检测缺失函数
|
||
if ($isMerge || $isConflictResolution) {
|
||
$missingFunctions = $this->detectMissingFunctions($path, $commit);
|
||
if (!empty($missingFunctions)) {
|
||
$issues['missing_functions'][] = [
|
||
'commit' => $this->getCommitMetadata($path, $commit),
|
||
'details' => $missingFunctions,
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->updateLastChecked($repoKey, $head);
|
||
|
||
return [
|
||
'repository' => $repoKey,
|
||
'display' => $repoConfig['display'] ?? ucfirst($repoKey),
|
||
'branch' => $branch,
|
||
'path' => $path,
|
||
'head' => $head,
|
||
'commits_scanned' => count($commits),
|
||
'issues' => $issues,
|
||
];
|
||
}
|
||
|
||
private function synchronizeRepository(string $path, string $branch): void
|
||
{
|
||
$this->runGit($path, ['git', 'fetch', 'origin']);
|
||
$this->runGit($path, ['git', 'fetch', 'origin', $branch]);
|
||
$this->runGit($path, ['git', 'fetch', 'origin', self::DEVELOP_BRANCH]);
|
||
|
||
$this->runGit($path, ['git', 'show-ref', '--verify', "refs/remotes/origin/{$branch}"]);
|
||
}
|
||
|
||
private function checkoutBranch(string $path, string $branch): void
|
||
{
|
||
try {
|
||
$this->runGit($path, ['git', 'show-ref', '--verify', "refs/heads/{$branch}"]);
|
||
$this->runGit($path, ['git', 'checkout', $branch]);
|
||
} catch (ProcessFailedException) {
|
||
$this->runGit($path, ['git', 'checkout', '-B', $branch, "origin/{$branch}"]);
|
||
}
|
||
}
|
||
|
||
private function collectCommits(string $repoPath, string $branch, ?string $lastChecked): array
|
||
{
|
||
try {
|
||
$output = $lastChecked
|
||
? $this->runGit($repoPath, ['git', 'log', "{$lastChecked}..{$branch}", '--pretty=%H'])
|
||
: $this->runGit($repoPath, ['git', 'log', $branch, '-n', (string) $this->commitScanLimit, '--pretty=%H']);
|
||
} catch (ProcessFailedException) {
|
||
$output = $this->runGit($repoPath, ['git', 'log', '-n', (string) $this->commitScanLimit, '--pretty=%H']);
|
||
}
|
||
|
||
return array_values(array_filter(array_map('trim', explode("\n", $output))));
|
||
}
|
||
|
||
/**
|
||
* 判断是否为 merge 提交(有多个父提交)
|
||
*/
|
||
private function isMergeCommit(string $repoPath, string $commit): bool
|
||
{
|
||
$parents = trim($this->runGit($repoPath, ['git', 'show', '-s', '--pretty=%P', $commit]));
|
||
$parentShas = array_values(array_filter(explode(' ', $parents)));
|
||
|
||
return count($parentShas) >= 2;
|
||
}
|
||
|
||
/**
|
||
* 判断是否为冲突解决提交
|
||
* 通过检查 commit message 是否包含冲突相关关键词
|
||
*/
|
||
private function isConflictResolution(string $repoPath, string $commit): bool
|
||
{
|
||
$message = strtolower($this->runGit($repoPath, ['git', 'show', '-s', '--pretty=%s', $commit]));
|
||
|
||
$conflictKeywords = ['conflict', 'resolve', '冲突', '解决冲突'];
|
||
foreach ($conflictKeywords as $keyword) {
|
||
if (str_contains($message, $keyword)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 判断是否为直接从 develop 分支合并到 release 的提交
|
||
* 只检测 commit message 明确包含 "merge.*develop" 或 "develop.*into" 的情况
|
||
*/
|
||
private function isDevelopMerge(string $repoPath, string $commit): bool
|
||
{
|
||
$message = strtolower($this->runGit($repoPath, ['git', 'show', '-s', '--pretty=%s', $commit]));
|
||
|
||
// 检测 "Merge branch 'develop'" 或 "merge develop into" 等模式
|
||
// 排除 feature/xxx、hotfix/xxx 等分支的合并
|
||
if (preg_match("/merge\s+(branch\s+)?['\"]?develop['\"]?(\s+into)?/i", $message)) {
|
||
return true;
|
||
}
|
||
|
||
// 检测 "develop into release" 模式
|
||
if (preg_match("/develop\s+into\s+['\"]?release/i", $message)) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private function detectMissingFunctions(string $repoPath, string $commit): array
|
||
{
|
||
$diff = $this->runGit($repoPath, ['git', 'show', $commit, '--pretty=format:', '--unified=0']);
|
||
$currentFile = null;
|
||
$removed = [];
|
||
$added = [];
|
||
|
||
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;
|
||
}
|
||
|
||
if (str_starts_with($line, '--- a/')) {
|
||
continue;
|
||
}
|
||
|
||
if (!$currentFile || !str_ends_with($currentFile, '.php')) {
|
||
continue;
|
||
}
|
||
|
||
if (str_starts_with($line, '-')) {
|
||
$function = $this->extractFunctionName(substr($line, 1));
|
||
if ($function) {
|
||
$removed[$currentFile][] = $function;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (str_starts_with($line, '+')) {
|
||
$function = $this->extractFunctionName(substr($line, 1));
|
||
if ($function) {
|
||
$added[$currentFile][] = $function;
|
||
}
|
||
}
|
||
}
|
||
|
||
$issues = [];
|
||
foreach ($removed as $file => $functions) {
|
||
$diffed = array_diff($functions, $added[$file] ?? []);
|
||
if (!empty($diffed)) {
|
||
$issues[] = [
|
||
'file' => $file,
|
||
'functions' => array_values(array_unique($diffed)),
|
||
];
|
||
}
|
||
}
|
||
|
||
return $issues;
|
||
}
|
||
|
||
private function extractFunctionName(string $line): ?string
|
||
{
|
||
$trimmed = trim($line);
|
||
|
||
// 跳过注释行
|
||
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '#') || str_starts_with($trimmed, '*') || str_starts_with($trimmed, '/*')) {
|
||
return null;
|
||
}
|
||
|
||
// 只匹配真正的函数定义,必须以 function 关键字开头,或者以访问修饰符开头
|
||
// 匹配: function foo(, public function foo(, private static function foo( 等
|
||
if (preg_match('/^(?:(?:public|protected|private)\s+)?(?:static\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/', $trimmed, $matches)) {
|
||
return $matches[1];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function updateLastChecked(string $repoKey, string $sha): void
|
||
{
|
||
$project = Project::findBySlug($repoKey);
|
||
if ($project) {
|
||
$project->update(['git_last_checked_commit' => $sha]);
|
||
}
|
||
}
|
||
|
||
private function getCommitMetadata(string $repoPath, string $commit): array
|
||
{
|
||
$separator = "\x1F";
|
||
$format = "%H{$separator}%an{$separator}%ad{$separator}%s";
|
||
$raw = $this->runGit($repoPath, ['git', 'show', '-s', "--date=iso-strict", "--pretty={$format}", $commit]);
|
||
$parts = array_pad(explode($separator, $raw, 4), 4, '');
|
||
|
||
return [
|
||
'hash' => $parts[0],
|
||
'author' => $parts[1],
|
||
'date' => $parts[2],
|
||
'subject' => $parts[3],
|
||
];
|
||
}
|
||
|
||
private function hasIssues(array $issues): bool
|
||
{
|
||
return !empty($issues['develop_merges']) || !empty($issues['missing_functions']);
|
||
}
|
||
|
||
private function buildAlertMessage(array $alerts): string
|
||
{
|
||
$lines = ['【Git监控告警】'];
|
||
|
||
foreach ($alerts as $result) {
|
||
$lines[] = sprintf('%s(%s)', $result['display'], $result['branch']);
|
||
|
||
if (!empty($result['issues']['develop_merges'])) {
|
||
$lines[] = ' - 检测到 develop 合并:';
|
||
foreach ($result['issues']['develop_merges'] as $commit) {
|
||
$lines[] = sprintf(
|
||
' • %s %s (%s)',
|
||
substr($commit['hash'], 0, 8),
|
||
$commit['subject'],
|
||
$commit['author']
|
||
);
|
||
}
|
||
}
|
||
|
||
if (!empty($result['issues']['missing_functions'])) {
|
||
$lines[] = ' - 疑似缺失函数:';
|
||
|
||
foreach ($result['issues']['missing_functions'] as $issue) {
|
||
$lines[] = sprintf(
|
||
' • %s %s',
|
||
substr($issue['commit']['hash'], 0, 8),
|
||
$issue['commit']['subject']
|
||
);
|
||
|
||
foreach ($issue['details'] as $detail) {
|
||
$functions = implode(', ', array_slice($detail['functions'], 0, 5));
|
||
$lines[] = sprintf(' %s => %s', $detail['file'], $functions);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return implode("\n", $lines);
|
||
}
|
||
|
||
private function runGit(string $path, array $command): string
|
||
{
|
||
return $this->runProcess($command, $path);
|
||
}
|
||
|
||
private function runProcess(array $command, ?string $workingDirectory = null): string
|
||
{
|
||
$process = new Process($command, $workingDirectory);
|
||
$process->setTimeout($this->gitTimeout);
|
||
$process->mustRun();
|
||
|
||
return trim($process->getOutput());
|
||
}
|
||
|
||
private function resolveProjectsPath(): string
|
||
{
|
||
$path = $this->configService->get('workspace.projects_path');
|
||
|
||
if (empty($path)) {
|
||
throw new \RuntimeException('configs 表未设置 workspace.projects_path。');
|
||
}
|
||
|
||
return rtrim($path, '/');
|
||
}
|
||
|
||
/**
|
||
* @return array<string, array<string, mixed>>
|
||
*/
|
||
private function resolveProjects(): array
|
||
{
|
||
// 优先从 Project 模型获取启用 Git 监控的项目
|
||
$projectModels = Project::query()
|
||
->where('git_monitor_enabled', true)
|
||
->get();
|
||
|
||
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 $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 $this->buildFallbackProjects(config('git-monitor.enabled_projects', []));
|
||
}
|
||
|
||
/**
|
||
* @return array<string, array<string, mixed>>
|
||
*/
|
||
private function buildFallbackProjects(array $enabled): array
|
||
{
|
||
$projects = [];
|
||
$jiraKeyMap = [
|
||
'portal-be' => 'WP',
|
||
'agent-be' => 'AM',
|
||
];
|
||
|
||
foreach ($enabled as $repoKey) {
|
||
if (!is_string($repoKey) || $repoKey === '') {
|
||
continue;
|
||
}
|
||
|
||
$projects[$repoKey] = [
|
||
'jira_project' => $jiraKeyMap[$repoKey] ?? $repoKey,
|
||
];
|
||
}
|
||
|
||
return $projects;
|
||
}
|
||
|
||
private function resolveProjectPath(string $repoKey, array $repoConfig): string
|
||
{
|
||
if (!empty($repoConfig['path'])) {
|
||
return rtrim($repoConfig['path'], '/');
|
||
}
|
||
|
||
$directory = $repoConfig['directory'] ?? $repoKey;
|
||
return $this->projectsPath . '/' . ltrim($directory, '/');
|
||
}
|
||
|
||
/**
|
||
* 从 master 分支读取 version.txt 获取当前版本号
|
||
*/
|
||
public function getMasterVersion(string $repoKey, array $repoConfig): ?string
|
||
{
|
||
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查远程是否存在指定分支
|
||
*/
|
||
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) {
|
||
// 忽略
|
||
}
|
||
}
|
||
}
|
||
}
|