#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:
@@ -45,6 +45,17 @@ class JenkinsClient
|
|||||||
return $this->request($this->getJobPath($jobName).'/lastBuild/api/json');
|
return $this->request($this->getJobPath($jobName).'/lastBuild/api/json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getJobUrl(string $jobName, ?int $buildNumber = null): ?string
|
||||||
|
{
|
||||||
|
if (empty($this->host) || trim($jobName) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $this->host.$this->getJobPath($jobName).'/';
|
||||||
|
|
||||||
|
return $buildNumber ? $url.$buildNumber.'/' : $url;
|
||||||
|
}
|
||||||
|
|
||||||
public function getParameterDefinitions(string $jobName): array
|
public function getParameterDefinitions(string $jobName): array
|
||||||
{
|
{
|
||||||
$jobInfo = $this->getJobInfo($jobName);
|
$jobInfo = $this->getJobInfo($jobName);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class JenkinsBuildController extends Controller
|
|||||||
'slug' => $project->slug,
|
'slug' => $project->slug,
|
||||||
'name' => $project->name,
|
'name' => $project->name,
|
||||||
'jenkins_job_name' => $project->jenkins_job_name,
|
'jenkins_job_name' => $project->jenkins_job_name,
|
||||||
|
'jenkins_job_url' => $this->jenkinsClient->getJobUrl($project->jenkins_job_name),
|
||||||
'parameters' => $this->jenkinsClient->getParameterDefinitions($project->jenkins_job_name),
|
'parameters' => $this->jenkinsClient->getParameterDefinitions($project->jenkins_job_name),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
@@ -39,6 +40,7 @@ class JenkinsBuildController extends Controller
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
|
'jenkins_host' => rtrim((string) config('jenkins.host'), '/'),
|
||||||
'projects' => $projects,
|
'projects' => $projects,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@@ -79,10 +81,14 @@ class JenkinsBuildController extends Controller
|
|||||||
'project_slug' => $project->slug,
|
'project_slug' => $project->slug,
|
||||||
'project_name' => $project->name,
|
'project_name' => $project->name,
|
||||||
'job_name' => $project->jenkins_job_name,
|
'job_name' => $project->jenkins_job_name,
|
||||||
|
'jenkins_job_url' => $this->jenkinsClient->getJobUrl($project->jenkins_job_name),
|
||||||
'success' => (bool) ($result['success'] ?? false),
|
'success' => (bool) ($result['success'] ?? false),
|
||||||
'message' => $result['message'] ?? null,
|
'message' => $result['message'] ?? null,
|
||||||
'queue_url' => $result['queue_url'] ?? null,
|
'queue_url' => $result['queue_url'] ?? null,
|
||||||
'build_number' => $result['build_number'] ?? null,
|
'build_number' => $result['build_number'] ?? null,
|
||||||
|
'build_url' => isset($result['build_number'])
|
||||||
|
? $this->jenkinsClient->getJobUrl($project->jenkins_job_name, (int) $result['build_number'])
|
||||||
|
: null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +126,23 @@ class JenkinsBuildController extends Controller
|
|||||||
/** @var Project $project */
|
/** @var Project $project */
|
||||||
$project = $projects[$build['project_slug']];
|
$project = $projects[$build['project_slug']];
|
||||||
|
|
||||||
|
$status = $this->jenkinsClient->getBuildStatus(
|
||||||
|
$project->jenkins_job_name,
|
||||||
|
$build['queue_url'] ?? null,
|
||||||
|
isset($build['build_number']) ? (int) $build['build_number'] : null
|
||||||
|
);
|
||||||
|
$buildNumber = $status['build_number'] ?? ($build['build_number'] ?? null);
|
||||||
|
|
||||||
$results[] = [
|
$results[] = [
|
||||||
'id' => $build['id'],
|
'id' => $build['id'],
|
||||||
'project_slug' => $project->slug,
|
'project_slug' => $project->slug,
|
||||||
'job_name' => $project->jenkins_job_name,
|
'job_name' => $project->jenkins_job_name,
|
||||||
...$this->jenkinsClient->getBuildStatus(
|
'jenkins_job_url' => $this->jenkinsClient->getJobUrl($project->jenkins_job_name),
|
||||||
$project->jenkins_job_name,
|
...$status,
|
||||||
$build['queue_url'] ?? null,
|
'build_url' => $status['build_url'] ?? (
|
||||||
isset($build['build_number']) ? (int) $build['build_number'] : null
|
$buildNumber
|
||||||
|
? $this->jenkinsClient->getJobUrl($project->jenkins_job_name, (int) $buildNumber)
|
||||||
|
: null
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Services;
|
|||||||
|
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||||
use Symfony\Component\Process\Process;
|
use Symfony\Component\Process\Process;
|
||||||
@@ -12,15 +11,20 @@ use Symfony\Component\Process\Process;
|
|||||||
class GitMonitorService
|
class GitMonitorService
|
||||||
{
|
{
|
||||||
private const DEVELOP_BRANCH = 'develop';
|
private const DEVELOP_BRANCH = 'develop';
|
||||||
|
|
||||||
private const RELEASE_CACHE_KEY = 'git-monitor.release_cache';
|
private const RELEASE_CACHE_KEY = 'git-monitor.release_cache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目配置(只包含允许巡检的项目)
|
* 项目配置(只包含允许巡检的项目)
|
||||||
|
*
|
||||||
* @var array<string, array<string, mixed>>
|
* @var array<string, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
private array $projects = [];
|
private array $projects = [];
|
||||||
|
|
||||||
private string $projectsPath;
|
private string $projectsPath;
|
||||||
|
|
||||||
private int $commitScanLimit;
|
private int $commitScanLimit;
|
||||||
|
|
||||||
private int $gitTimeout;
|
private int $gitTimeout;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -41,9 +45,9 @@ class GitMonitorService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要刷新缓存
|
// 检查是否需要刷新缓存
|
||||||
if (!$force) {
|
if (! $force) {
|
||||||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
$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();
|
return $this->buildCachePayload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,7 @@ class GitMonitorService
|
|||||||
$projectKey = $repoConfig['jira_project'] ?? null;
|
$projectKey = $repoConfig['jira_project'] ?? null;
|
||||||
if (empty($projectKey)) {
|
if (empty($projectKey)) {
|
||||||
Log::warning('Jira project key missing for repository', ['repository' => $repoKey]);
|
Log::warning('Jira project key missing for repository', ['repository' => $repoKey]);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ class GitMonitorService
|
|||||||
// 根据当前版本号获取下一个版本
|
// 根据当前版本号获取下一个版本
|
||||||
$version = $this->jiraService->getUpcomingReleaseVersion($projectKey, $currentVersion);
|
$version = $this->jiraService->getUpcomingReleaseVersion($projectKey, $currentVersion);
|
||||||
if ($version) {
|
if ($version) {
|
||||||
$branch = 'release/' . $version['version'];
|
$branch = 'release/'.$version['version'];
|
||||||
$payload['repositories'][$repoKey] = [
|
$payload['repositories'][$repoKey] = [
|
||||||
'version' => $version['version'],
|
'version' => $version['version'],
|
||||||
'description' => $version['description'] ?? null,
|
'description' => $version['description'] ?? null,
|
||||||
@@ -106,7 +111,7 @@ class GitMonitorService
|
|||||||
{
|
{
|
||||||
$anyProject = Project::query()->whereNotNull('git_version_cached_at')->first();
|
$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);
|
return $this->refreshReleaseCache(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +157,7 @@ class GitMonitorService
|
|||||||
$branch = data_get($releaseCache, "repositories.{$repoKey}.branch");
|
$branch = data_get($releaseCache, "repositories.{$repoKey}.branch");
|
||||||
if (empty($branch)) {
|
if (empty($branch)) {
|
||||||
Log::warning('Missing release branch info for repository', ['repository' => $repoKey]);
|
Log::warning('Missing release branch info for repository', ['repository' => $repoKey]);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +182,7 @@ class GitMonitorService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($alerts)) {
|
if (! empty($alerts)) {
|
||||||
$this->dingTalkService->sendText($this->buildAlertMessage($alerts));
|
$this->dingTalkService->sendText($this->buildAlertMessage($alerts));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +197,7 @@ class GitMonitorService
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$cachedTime = $cachedAt instanceof Carbon ? $cachedAt : Carbon::parse($cachedAt);
|
$cachedTime = $cachedAt instanceof Carbon ? $cachedAt : Carbon::parse($cachedAt);
|
||||||
|
|
||||||
return $cachedTime->lt(Carbon::now()->startOfDay());
|
return $cachedTime->lt(Carbon::now()->startOfDay());
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
return true;
|
return true;
|
||||||
@@ -201,12 +208,12 @@ class GitMonitorService
|
|||||||
{
|
{
|
||||||
$path = $this->resolveProjectPath($repoKey, $repoConfig);
|
$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");
|
throw new \RuntimeException("Project path {$path} is not a valid git repository");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->synchronizeRepository($path, $branch);
|
$this->synchronizeRepository($path, $branch);
|
||||||
$remoteBranch = 'origin/' . $branch;
|
$remoteBranch = 'origin/'.$branch;
|
||||||
$head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]);
|
$head = $this->runGit($path, ['git', 'rev-parse', $remoteBranch]);
|
||||||
|
|
||||||
// 从 Project 模型获取 lastChecked
|
// 从 Project 模型获取 lastChecked
|
||||||
@@ -231,7 +238,7 @@ class GitMonitorService
|
|||||||
// 只在 merge 提交或冲突解决提交中检测缺失函数
|
// 只在 merge 提交或冲突解决提交中检测缺失函数
|
||||||
if ($isMerge || $isConflictResolution) {
|
if ($isMerge || $isConflictResolution) {
|
||||||
$missingFunctions = $this->detectMissingFunctions($path, $commit);
|
$missingFunctions = $this->detectMissingFunctions($path, $commit);
|
||||||
if (!empty($missingFunctions)) {
|
if (! empty($missingFunctions)) {
|
||||||
$issues['missing_functions'][] = [
|
$issues['missing_functions'][] = [
|
||||||
'commit' => $this->getCommitMetadata($path, $commit),
|
'commit' => $this->getCommitMetadata($path, $commit),
|
||||||
'details' => $missingFunctions,
|
'details' => $missingFunctions,
|
||||||
@@ -346,11 +353,13 @@ class GitMonitorService
|
|||||||
foreach (preg_split('/\R/', $diff) as $line) {
|
foreach (preg_split('/\R/', $diff) as $line) {
|
||||||
if (str_starts_with($line, 'diff --git')) {
|
if (str_starts_with($line, 'diff --git')) {
|
||||||
$currentFile = null;
|
$currentFile = null;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_starts_with($line, '+++ b/')) {
|
if (str_starts_with($line, '+++ b/')) {
|
||||||
$currentFile = substr($line, 6);
|
$currentFile = substr($line, 6);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +367,7 @@ class GitMonitorService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$currentFile || !str_ends_with($currentFile, '.php')) {
|
if (! $currentFile || ! str_ends_with($currentFile, '.php')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +376,7 @@ class GitMonitorService
|
|||||||
if ($function) {
|
if ($function) {
|
||||||
$removed[$currentFile][] = $function;
|
$removed[$currentFile][] = $function;
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +391,7 @@ class GitMonitorService
|
|||||||
$issues = [];
|
$issues = [];
|
||||||
foreach ($removed as $file => $functions) {
|
foreach ($removed as $file => $functions) {
|
||||||
$diffed = array_diff($functions, $added[$file] ?? []);
|
$diffed = array_diff($functions, $added[$file] ?? []);
|
||||||
if (!empty($diffed)) {
|
if (! empty($diffed)) {
|
||||||
$issues[] = [
|
$issues[] = [
|
||||||
'file' => $file,
|
'file' => $file,
|
||||||
'functions' => array_values(array_unique($diffed)),
|
'functions' => array_values(array_unique($diffed)),
|
||||||
@@ -422,7 +432,7 @@ class GitMonitorService
|
|||||||
{
|
{
|
||||||
$separator = "\x1F";
|
$separator = "\x1F";
|
||||||
$format = "%H{$separator}%an{$separator}%ad{$separator}%s";
|
$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, '');
|
$parts = array_pad(explode($separator, $raw, 4), 4, '');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -435,7 +445,7 @@ class GitMonitorService
|
|||||||
|
|
||||||
private function hasIssues(array $issues): bool
|
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
|
private function buildAlertMessage(array $alerts): string
|
||||||
@@ -445,7 +455,7 @@ class GitMonitorService
|
|||||||
foreach ($alerts as $result) {
|
foreach ($alerts as $result) {
|
||||||
$lines[] = sprintf('%s(%s)', $result['display'], $result['branch']);
|
$lines[] = sprintf('%s(%s)', $result['display'], $result['branch']);
|
||||||
|
|
||||||
if (!empty($result['issues']['develop_merges'])) {
|
if (! empty($result['issues']['develop_merges'])) {
|
||||||
$lines[] = ' - 检测到 develop 合并:';
|
$lines[] = ' - 检测到 develop 合并:';
|
||||||
foreach ($result['issues']['develop_merges'] as $commit) {
|
foreach ($result['issues']['develop_merges'] as $commit) {
|
||||||
$lines[] = sprintf(
|
$lines[] = sprintf(
|
||||||
@@ -457,7 +467,7 @@ class GitMonitorService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($result['issues']['missing_functions'])) {
|
if (! empty($result['issues']['missing_functions'])) {
|
||||||
$lines[] = ' - 疑似缺失函数:';
|
$lines[] = ' - 疑似缺失函数:';
|
||||||
|
|
||||||
foreach ($result['issues']['missing_functions'] as $issue) {
|
foreach ($result['issues']['missing_functions'] as $issue) {
|
||||||
@@ -523,6 +533,7 @@ class GitMonitorService
|
|||||||
'display' => $project->name,
|
'display' => $project->name,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $projects;
|
return $projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,9 +542,10 @@ class GitMonitorService
|
|||||||
|
|
||||||
if ($configProjects !== null && is_array($configProjects)) {
|
if ($configProjects !== null && is_array($configProjects)) {
|
||||||
$enabled = config('git-monitor.enabled_projects', []);
|
$enabled = config('git-monitor.enabled_projects', []);
|
||||||
if (!empty($enabled)) {
|
if (! empty($enabled)) {
|
||||||
return array_intersect_key($configProjects, array_flip($enabled));
|
return array_intersect_key($configProjects, array_flip($enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $configProjects;
|
return $configProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +565,7 @@ class GitMonitorService
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($enabled as $repoKey) {
|
foreach ($enabled as $repoKey) {
|
||||||
if (!is_string($repoKey) || $repoKey === '') {
|
if (! is_string($repoKey) || $repoKey === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,12 +579,13 @@ class GitMonitorService
|
|||||||
|
|
||||||
private function resolveProjectPath(string $repoKey, array $repoConfig): string
|
private function resolveProjectPath(string $repoKey, array $repoConfig): string
|
||||||
{
|
{
|
||||||
if (!empty($repoConfig['path'])) {
|
if (! empty($repoConfig['path'])) {
|
||||||
return rtrim($repoConfig['path'], '/');
|
return rtrim($repoConfig['path'], '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
$directory = $repoConfig['directory'] ?? $repoKey;
|
$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);
|
$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]);
|
Log::warning('Invalid git repository path', ['repository' => $repoKey, 'path' => $path]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
|
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
|
||||||
$version = $this->runGit($path, ['git', 'show', 'origin/master:version.txt']);
|
$version = $this->runGit($path, ['git', 'show', 'origin/master:version.txt']);
|
||||||
|
|
||||||
return trim($version) ?: null;
|
return trim($version) ?: null;
|
||||||
} catch (ProcessFailedException $e) {
|
} catch (ProcessFailedException $e) {
|
||||||
Log::warning('Failed to read version.txt from master branch', [
|
Log::warning('Failed to read version.txt from master branch', [
|
||||||
'repository' => $repoKey,
|
'repository' => $repoKey,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -608,6 +624,7 @@ class GitMonitorService
|
|||||||
try {
|
try {
|
||||||
$this->runGit($path, ['git', 'fetch', 'origin']);
|
$this->runGit($path, ['git', 'fetch', 'origin']);
|
||||||
$this->runGit($path, ['git', 'ls-remote', '--exit-code', '--heads', 'origin', $branch]);
|
$this->runGit($path, ['git', 'ls-remote', '--exit-code', '--heads', 'origin', $branch]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (ProcessFailedException) {
|
} catch (ProcessFailedException) {
|
||||||
return false;
|
return false;
|
||||||
@@ -623,14 +640,16 @@ class GitMonitorService
|
|||||||
$worktreePath = null;
|
$worktreePath = null;
|
||||||
$worktreeCreated = false;
|
$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]);
|
Log::warning('Invalid git repository path for branch creation', ['repository' => $repoKey, 'path' => $path]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查远程分支是否已存在
|
// 检查远程分支是否已存在
|
||||||
if ($this->remoteBranchExists($path, $branch)) {
|
if ($this->remoteBranchExists($path, $branch)) {
|
||||||
Log::info('Release branch already exists on remote', ['repository' => $repoKey, 'branch' => $branch]);
|
Log::info('Release branch already exists on remote', ['repository' => $repoKey, 'branch' => $branch]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,25 +659,25 @@ class GitMonitorService
|
|||||||
try {
|
try {
|
||||||
// 在临时 worktree 中创建并推送分支,避免切换或修改用户正在工作的仓库。
|
// 在临时 worktree 中创建并推送分支,避免切换或修改用户正在工作的仓库。
|
||||||
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
|
$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']);
|
$this->runGit($path, ['git', 'worktree', 'add', '--detach', $worktreePath, 'origin/master']);
|
||||||
$worktreeCreated = true;
|
$worktreeCreated = true;
|
||||||
|
|
||||||
// 修改 version.txt 文件
|
// 修改 version.txt 文件
|
||||||
$versionFile = $worktreePath . DIRECTORY_SEPARATOR . 'version.txt';
|
$versionFile = $worktreePath.DIRECTORY_SEPARATOR.'version.txt';
|
||||||
if (!file_put_contents($versionFile, $version)) {
|
if (! file_put_contents($versionFile, $version)) {
|
||||||
throw new \RuntimeException("Failed to write version.txt");
|
throw new \RuntimeException('Failed to write version.txt');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加并提交更改
|
// 添加并提交更改
|
||||||
$this->runGit($worktreePath, ['git', 'add', 'version.txt']);
|
$this->runGit($worktreePath, ['git', 'add', 'version.txt']);
|
||||||
|
|
||||||
// 构建提交信息:分支名 + 空格 + Jira 描述
|
// 构建提交信息:release-版本号 + 空格 + Jira 描述
|
||||||
$commitMessage = $branch . ($description ? ' ' . $description : '');
|
$commitMessage = 'release-'.$version.($description ? ' '.$description : '');
|
||||||
$this->runGit($worktreePath, ['git', 'commit', '-m', $commitMessage]);
|
$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', [
|
Log::info('Created and pushed release branch', [
|
||||||
'repository' => $repoKey,
|
'repository' => $repoKey,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('projects')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$projects = [
|
||||||
|
[
|
||||||
|
'slug' => 'crm-web',
|
||||||
|
'name' => 'Crm web',
|
||||||
|
'directory' => 'crm-web',
|
||||||
|
'jenkins_job_name' => 'crm-dev-crm_web',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'slug' => 'crm-opm',
|
||||||
|
'name' => 'Crm opm',
|
||||||
|
'directory' => 'crm-opm',
|
||||||
|
'jenkins_job_name' => 'crm-dev-crm_opm',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($projects as $data) {
|
||||||
|
$project = Project::query()->firstOrCreate(
|
||||||
|
['slug' => $data['slug']],
|
||||||
|
[
|
||||||
|
'name' => $data['name'],
|
||||||
|
'directory' => $data['directory'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$project->update([
|
||||||
|
'jenkins_job_name' => $data['jenkins_job_name'],
|
||||||
|
'jenkins_notify_enabled' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('projects')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Project::query()
|
||||||
|
->whereIn('slug', ['crm-web', 'crm-opm'])
|
||||||
|
->whereIn('jenkins_job_name', ['crm-dev-crm_web', 'crm-dev-crm_opm'])
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -80,7 +80,22 @@
|
|||||||
<span class="font-mono text-sm font-semibold text-gray-800 truncate">{{ project.slug }}</span>
|
<span class="font-mono text-sm font-semibold text-gray-800 truncate">{{ project.slug }}</span>
|
||||||
<span class="text-[11px] text-gray-500 truncate" :title="project.name">{{ project.name }}</span>
|
<span class="text-[11px] text-gray-500 truncate" :title="project.name">{{ project.name }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="block font-mono text-[11px] text-gray-400 truncate" :title="project.jenkins_job_name">{{ project.jenkins_job_name }}</span>
|
<a
|
||||||
|
v-if="jenkinsPageUrl(project)"
|
||||||
|
:href="jenkinsPageUrl(project)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="mt-0.5 inline-flex max-w-full items-center gap-1 font-mono text-[11px] text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
|
:title="'打开 Jenkins 发布页:' + project.jenkins_job_name"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ project.jenkins_job_name }}</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-3 w-3 shrink-0">
|
||||||
|
<path fill-rule="evenodd" d="M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z" clip-rule="evenodd" />
|
||||||
|
<path fill-rule="evenodd" d="M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<span v-else class="block font-mono text-[11px] text-gray-400 truncate" :title="project.jenkins_job_name">{{ project.jenkins_job_name }}</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
@@ -174,12 +189,34 @@
|
|||||||
<div class="mt-1 flex items-center gap-1.5 text-xs text-gray-700 min-w-0">
|
<div class="mt-1 flex items-center gap-1.5 text-xs text-gray-700 min-w-0">
|
||||||
<span class="font-mono font-semibold truncate" :title="record.project_slug">{{ record.project_slug || '-' }}</span>
|
<span class="font-mono font-semibold truncate" :title="record.project_slug">{{ record.project_slug || '-' }}</span>
|
||||||
<span class="text-[11px] text-gray-400 truncate" :title="record.project_name">{{ record.project_name }}</span>
|
<span class="text-[11px] text-gray-400 truncate" :title="record.project_name">{{ record.project_name }}</span>
|
||||||
|
<a
|
||||||
|
v-if="jenkinsPageUrl(record)"
|
||||||
|
:href="jenkinsPageUrl(record)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="ml-auto inline-flex shrink-0 items-center gap-0.5 text-[11px] text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
|
title="打开 Jenkins 发布页"
|
||||||
|
>
|
||||||
|
Jenkins
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-3 w-3">
|
||||||
|
<path fill-rule="evenodd" d="M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z" clip-rule="evenodd" />
|
||||||
|
<path fill-rule="evenodd" d="M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-0.5 grid grid-cols-[3.25rem_minmax(0,1fr)] gap-1 text-[11px] text-gray-500">
|
<div class="mt-0.5 grid grid-cols-[3.25rem_minmax(0,1fr)] gap-1 text-[11px] text-gray-500">
|
||||||
<span class="text-gray-400">project</span>
|
<span class="text-gray-400">project</span>
|
||||||
<span class="font-mono truncate" :title="record.project_parameter">{{ record.project_parameter || '-' }}</span>
|
<span class="font-mono truncate" :title="record.project_parameter">{{ record.project_parameter || '-' }}</span>
|
||||||
<span class="text-gray-400">构建号</span>
|
<span class="text-gray-400">构建号</span>
|
||||||
<span class="font-mono truncate">{{ record.build_number ? `#${record.build_number}` : '-' }}</span>
|
<a
|
||||||
|
v-if="jenkinsBuildUrl(record)"
|
||||||
|
:href="jenkinsBuildUrl(record)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="font-mono truncate text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
|
:title="'打开 Jenkins 构建 #' + record.build_number"
|
||||||
|
>#{{ record.build_number }}</a>
|
||||||
|
<span v-else class="font-mono truncate">{{ record.build_number ? `#${record.build_number}` : '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="record.message" class="mt-1 text-[11px] text-gray-400 truncate" :title="record.message">
|
<div v-if="record.message" class="mt-1 text-[11px] text-gray-400 truncate" :title="record.message">
|
||||||
{{ record.message }}
|
{{ record.message }}
|
||||||
@@ -298,7 +335,7 @@ export default {
|
|||||||
ParameterControl
|
ParameterControl
|
||||||
},
|
},
|
||||||
preferenceKey: 'toolbox.jenkinsBuilds.preferences.v1',
|
preferenceKey: 'toolbox.jenkinsBuilds.preferences.v1',
|
||||||
cacheKey: 'toolbox.jenkinsBuilds.projectsCache.v1',
|
cacheKey: 'toolbox.jenkinsBuilds.projectsCache.v2',
|
||||||
operationRecordsKey: 'toolbox.jenkinsBuilds.operationRecords.v1',
|
operationRecordsKey: 'toolbox.jenkinsBuilds.operationRecords.v1',
|
||||||
hiddenParameterNames: ['sql', 'masterCheck'],
|
hiddenParameterNames: ['sql', 'masterCheck'],
|
||||||
data() {
|
data() {
|
||||||
@@ -308,6 +345,7 @@ export default {
|
|||||||
triggering: false,
|
triggering: false,
|
||||||
statusChecking: false,
|
statusChecking: false,
|
||||||
statusPollingTimer: null,
|
statusPollingTimer: null,
|
||||||
|
jenkinsHost: '',
|
||||||
projects: [],
|
projects: [],
|
||||||
operationRecords: [],
|
operationRecords: [],
|
||||||
primaryParameterOrder: ['env', 'branchName', 'deploy', 'deployVersion'],
|
primaryParameterOrder: ['env', 'branchName', 'deploy', 'deployVersion'],
|
||||||
@@ -344,17 +382,16 @@ export default {
|
|||||||
if (freshCachedProjects) {
|
if (freshCachedProjects) {
|
||||||
cachedBuildProjects = freshCachedProjects;
|
cachedBuildProjects = freshCachedProjects;
|
||||||
this.applyProjects(cachedBuildProjects);
|
this.applyProjects(cachedBuildProjects);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cachedBuildProjects) {
|
|
||||||
cachedBuildProjects = this.loadProjectsCache({ allowExpired: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedBuildProjects) {
|
|
||||||
this.applyProjects(cachedBuildProjects);
|
|
||||||
} else {
|
} else {
|
||||||
this.loading = true;
|
if (!cachedBuildProjects) {
|
||||||
|
cachedBuildProjects = this.loadProjectsCache({ allowExpired: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedBuildProjects) {
|
||||||
|
this.applyProjects(cachedBuildProjects);
|
||||||
|
} else {
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshProjects({ silent: Boolean(cachedBuildProjects) });
|
this.refreshProjects({ silent: Boolean(cachedBuildProjects) });
|
||||||
@@ -384,6 +421,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cachedBuildProjects = data.data.projects || [];
|
cachedBuildProjects = data.data.projects || [];
|
||||||
|
this.jenkinsHost = data.data.jenkins_host || this.jenkinsHost;
|
||||||
this.saveProjectsCache(cachedBuildProjects);
|
this.saveProjectsCache(cachedBuildProjects);
|
||||||
this.applyProjects(cachedBuildProjects, { preserveCurrentValues: true });
|
this.applyProjects(cachedBuildProjects, { preserveCurrentValues: true });
|
||||||
|
|
||||||
@@ -431,6 +469,10 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cache.jenkins_host) {
|
||||||
|
this.jenkinsHost = cache.jenkins_host;
|
||||||
|
}
|
||||||
|
|
||||||
return cache.projects;
|
return cache.projects;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
@@ -439,6 +481,7 @@ export default {
|
|||||||
saveProjectsCache(projects) {
|
saveProjectsCache(projects) {
|
||||||
window.localStorage.setItem(this.$options.cacheKey, JSON.stringify({
|
window.localStorage.setItem(this.$options.cacheKey, JSON.stringify({
|
||||||
date: this.todayKey(),
|
date: this.todayKey(),
|
||||||
|
jenkins_host: this.jenkinsHost,
|
||||||
projects
|
projects
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -515,6 +558,46 @@ export default {
|
|||||||
preferenceProjectKey(project) {
|
preferenceProjectKey(project) {
|
||||||
return project.jenkins_job_name || project.slug;
|
return project.jenkins_job_name || project.slug;
|
||||||
},
|
},
|
||||||
|
jenkinsPageUrl(item) {
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.jenkins_job_url) {
|
||||||
|
return item.jenkins_job_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.buildJenkinsUrl(item.jenkins_job_name || item.job_name);
|
||||||
|
},
|
||||||
|
jenkinsBuildUrl(record) {
|
||||||
|
if (!record?.build_number) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.build_url) {
|
||||||
|
return record.build_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.buildJenkinsUrl(record.job_name || record.jenkins_job_name, record.build_number);
|
||||||
|
},
|
||||||
|
buildJenkinsUrl(jobName, buildNumber = null) {
|
||||||
|
if (!jobName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = String(jobName).split('/').map((segment) => segment.trim()).filter(Boolean);
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = (this.jenkinsHost || '').replace(/\/$/, '');
|
||||||
|
if (!host) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = '/job/' + segments.map(encodeURIComponent).join('/job/') + '/';
|
||||||
|
return buildNumber ? `${host}${path}${buildNumber}/` : `${host}${path}`;
|
||||||
|
},
|
||||||
isBooleanParameter(parameter) {
|
isBooleanParameter(parameter) {
|
||||||
return String(parameter.type || '').toLowerCase().includes('boolean');
|
return String(parameter.type || '').toLowerCase().includes('boolean');
|
||||||
},
|
},
|
||||||
@@ -690,11 +773,14 @@ export default {
|
|||||||
project_slug: result.project_slug || requested.project_slug || '',
|
project_slug: result.project_slug || requested.project_slug || '',
|
||||||
project_name: result.project_name || requested.project_name || '',
|
project_name: result.project_name || requested.project_name || '',
|
||||||
job_name: result.job_name || requested.job_name || '',
|
job_name: result.job_name || requested.job_name || '',
|
||||||
|
jenkins_job_url: result.jenkins_job_url || this.buildJenkinsUrl(result.job_name || requested.job_name),
|
||||||
project_parameter: this.formatParameterValue(requested.parameters?.project),
|
project_parameter: this.formatParameterValue(requested.parameters?.project),
|
||||||
status: result.success ? (canTrackBuild ? 'PENDING' : 'UNKNOWN') : 'FAILURE',
|
status: result.success ? (canTrackBuild ? 'PENDING' : 'UNKNOWN') : 'FAILURE',
|
||||||
queue_url: result.queue_url || null,
|
queue_url: result.queue_url || null,
|
||||||
build_number: result.build_number || null,
|
build_number: result.build_number || null,
|
||||||
build_url: null,
|
build_url: result.build_url || (result.build_number
|
||||||
|
? this.buildJenkinsUrl(result.job_name || requested.job_name, result.build_number)
|
||||||
|
: null),
|
||||||
parameters: requested.parameters || {},
|
parameters: requested.parameters || {},
|
||||||
message: result.success
|
message: result.success
|
||||||
? (canTrackBuild ? '已提交 Jenkins,等待发布结果' : '已提交 Jenkins,但未返回队列地址,无法自动跟踪或取消')
|
? (canTrackBuild ? '已提交 Jenkins,等待发布结果' : '已提交 Jenkins,但未返回队列地址,无法自动跟踪或取消')
|
||||||
@@ -788,6 +874,7 @@ export default {
|
|||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
build_number: status.build_number || record.build_number,
|
build_number: status.build_number || record.build_number,
|
||||||
build_url: status.build_url || record.build_url,
|
build_url: status.build_url || record.build_url,
|
||||||
|
jenkins_job_url: status.jenkins_job_url || record.jenkins_job_url,
|
||||||
message: status.message || null,
|
message: status.message || null,
|
||||||
cancelling: false
|
cancelling: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,6 +50,38 @@ class GitMonitorServiceTest extends TestCase
|
|||||||
$this->assertStringContainsString('?? work.txt', $this->git($repoPath, ['git', 'status', '--short']));
|
$this->assertStringContainsString('?? work.txt', $this->git($repoPath, ['git', 'status', '--short']));
|
||||||
$this->assertNotEmpty($this->git($repoPath, ['git', 'ls-remote', '--heads', 'origin', 'release/1.1.0']));
|
$this->assertNotEmpty($this->git($repoPath, ['git', 'ls-remote', '--heads', 'origin', 'release/1.1.0']));
|
||||||
$this->assertSame('1.1.0', $this->git($repoPath, ['git', 'show', 'origin/release/1.1.0:version.txt']));
|
$this->assertSame('1.1.0', $this->git($repoPath, ['git', 'show', 'origin/release/1.1.0:version.txt']));
|
||||||
|
$this->assertSame('release-1.1.0 next release', $this->git($repoPath, ['git', 'log', '-1', '--pretty=%s', 'origin/release/1.1.0']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_auto_creating_release_branch_skips_local_git_hooks(): void
|
||||||
|
{
|
||||||
|
$workspacePath = $this->makeTempDirectory('toolbox-git-workspace-');
|
||||||
|
$remotePath = $this->makeTempDirectory('toolbox-git-remote-');
|
||||||
|
$repoPath = $workspacePath.DIRECTORY_SEPARATOR.'demo-repo';
|
||||||
|
|
||||||
|
$this->git($remotePath, ['git', 'init', '--bare']);
|
||||||
|
$this->git($workspacePath, ['git', 'clone', $remotePath, 'demo-repo']);
|
||||||
|
$this->git($repoPath, ['git', 'config', 'user.email', 'test@example.com']);
|
||||||
|
$this->git($repoPath, ['git', 'config', 'user.name', 'Test User']);
|
||||||
|
file_put_contents($repoPath.DIRECTORY_SEPARATOR.'version.txt', '1.0.0');
|
||||||
|
$this->git($repoPath, ['git', 'add', 'version.txt']);
|
||||||
|
$this->git($repoPath, ['git', 'commit', '-m', 'initial']);
|
||||||
|
$this->git($repoPath, ['git', 'branch', '-M', 'master']);
|
||||||
|
$this->git($repoPath, ['git', 'push', '-u', 'origin', 'master']);
|
||||||
|
|
||||||
|
$hooksPath = $repoPath.DIRECTORY_SEPARATOR.'.git'.DIRECTORY_SEPARATOR.'hooks';
|
||||||
|
file_put_contents($hooksPath.DIRECTORY_SEPARATOR.'commit-msg', "#!/bin/sh\necho hook-blocked-commit >&2\nexit 1\n");
|
||||||
|
file_put_contents($hooksPath.DIRECTORY_SEPARATOR.'pre-push', "#!/bin/sh\necho hook-blocked-push >&2\nexit 1\n");
|
||||||
|
chmod($hooksPath.DIRECTORY_SEPARATOR.'commit-msg', 0755);
|
||||||
|
chmod($hooksPath.DIRECTORY_SEPARATOR.'pre-push', 0755);
|
||||||
|
$this->git($repoPath, ['git', 'config', 'core.hooksPath', $hooksPath]);
|
||||||
|
|
||||||
|
$service = $this->makeGitMonitorService($workspacePath);
|
||||||
|
$method = new \ReflectionMethod($service, 'ensureReleaseBranchExists');
|
||||||
|
$method->invoke($service, 'demo-repo', ['directory' => 'demo-repo'], 'release/1.1.0', 'next release');
|
||||||
|
|
||||||
|
$this->assertNotEmpty($this->git($repoPath, ['git', 'ls-remote', '--heads', 'origin', 'release/1.1.0']));
|
||||||
|
$this->assertSame('1.1.0', $this->git($repoPath, ['git', 'show', 'origin/release/1.1.0:version.txt']));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function makeGitMonitorService(string $workspacePath): GitMonitorService
|
private function makeGitMonitorService(string $workspacePath): GitMonitorService
|
||||||
|
|||||||
@@ -111,4 +111,22 @@ class JenkinsClientTest extends TestCase
|
|||||||
&& $request->url() === 'https://jenkins.example.com/queue/cancelItem?id=456';
|
&& $request->url() === 'https://jenkins.example.com/queue/cancelItem?id=456';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_get_job_url_builds_job_and_build_pages(): void
|
||||||
|
{
|
||||||
|
$client = app(JenkinsClient::class);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
'https://jenkins.example.com/job/crm-dev-crm_web/',
|
||||||
|
$client->getJobUrl('crm-dev-crm_web')
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://jenkins.example.com/job/crm-dev-crm_web/42/',
|
||||||
|
$client->getJobUrl('crm-dev-crm_web', 42)
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
'https://jenkins.example.com/job/folder/job/nested-job/',
|
||||||
|
$client->getJobUrl('folder/nested-job')
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user