From c45d68e2010f5cc1b4163d7bc1defc2660830a02 Mon Sep 17 00:00:00 2001 From: tradewind Date: Fri, 14 Aug 2026 09:14:57 +0800 Subject: [PATCH] =?UTF-8?q?#feature:=20Jenkins=20=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E4=B8=8E=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=20release=20=E5=88=86=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 加入 crm-web/crm-opm 并可跳转 Jenkins 发布页;自动创建 release 时 commit 使用 release-版本号,并跳过本地 git hook,避免 agent-be 被拦下。 Co-authored-by: Cursor --- app/Clients/JenkinsClient.php | 11 ++ .../Admin/JenkinsBuildController.php | 23 +++- app/Services/GitMonitorService.php | 77 +++++++----- ...d_crm_web_and_crm_opm_jenkins_projects.php | 57 +++++++++ .../js/components/admin/JenkinsBuilds.vue | 115 +++++++++++++++--- tests/Unit/GitMonitorServiceTest.php | 32 +++++ tests/Unit/JenkinsClientTest.php | 18 +++ 7 files changed, 286 insertions(+), 47 deletions(-) create mode 100644 database/migrations/2026_08_13_154300_add_crm_web_and_crm_opm_jenkins_projects.php diff --git a/app/Clients/JenkinsClient.php b/app/Clients/JenkinsClient.php index 93b49fe..a72a354 100644 --- a/app/Clients/JenkinsClient.php +++ b/app/Clients/JenkinsClient.php @@ -45,6 +45,17 @@ class JenkinsClient 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 { $jobInfo = $this->getJobInfo($jobName); diff --git a/app/Http/Controllers/Admin/JenkinsBuildController.php b/app/Http/Controllers/Admin/JenkinsBuildController.php index 2482506..4f6a3a3 100644 --- a/app/Http/Controllers/Admin/JenkinsBuildController.php +++ b/app/Http/Controllers/Admin/JenkinsBuildController.php @@ -31,6 +31,7 @@ class JenkinsBuildController extends Controller 'slug' => $project->slug, 'name' => $project->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), ]; }) @@ -39,6 +40,7 @@ class JenkinsBuildController extends Controller return response()->json([ 'success' => true, 'data' => [ + 'jenkins_host' => rtrim((string) config('jenkins.host'), '/'), 'projects' => $projects, ], ]); @@ -79,10 +81,14 @@ class JenkinsBuildController extends Controller 'project_slug' => $project->slug, 'project_name' => $project->name, 'job_name' => $project->jenkins_job_name, + 'jenkins_job_url' => $this->jenkinsClient->getJobUrl($project->jenkins_job_name), 'success' => (bool) ($result['success'] ?? false), 'message' => $result['message'] ?? null, 'queue_url' => $result['queue_url'] ?? 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 */ $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[] = [ 'id' => $build['id'], 'project_slug' => $project->slug, 'job_name' => $project->jenkins_job_name, - ...$this->jenkinsClient->getBuildStatus( - $project->jenkins_job_name, - $build['queue_url'] ?? null, - isset($build['build_number']) ? (int) $build['build_number'] : null + 'jenkins_job_url' => $this->jenkinsClient->getJobUrl($project->jenkins_job_name), + ...$status, + 'build_url' => $status['build_url'] ?? ( + $buildNumber + ? $this->jenkinsClient->getJobUrl($project->jenkins_job_name, (int) $buildNumber) + : null ), ]; } diff --git a/app/Services/GitMonitorService.php b/app/Services/GitMonitorService.php index be78b35..ad340d9 100644 --- a/app/Services/GitMonitorService.php +++ b/app/Services/GitMonitorService.php @@ -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> */ 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, diff --git a/database/migrations/2026_08_13_154300_add_crm_web_and_crm_opm_jenkins_projects.php b/database/migrations/2026_08_13_154300_add_crm_web_and_crm_opm_jenkins_projects.php new file mode 100644 index 0000000..38b5520 --- /dev/null +++ b/database/migrations/2026_08_13_154300_add_crm_web_and_crm_opm_jenkins_projects.php @@ -0,0 +1,57 @@ + '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(); + } +}; diff --git a/resources/js/components/admin/JenkinsBuilds.vue b/resources/js/components/admin/JenkinsBuilds.vue index 44e4c4e..7a51ff7 100644 --- a/resources/js/components/admin/JenkinsBuilds.vue +++ b/resources/js/components/admin/JenkinsBuilds.vue @@ -80,7 +80,22 @@ {{ project.slug }} {{ project.name }} - {{ project.jenkins_job_name }} + + {{ project.jenkins_job_name }} + + + + + + {{ project.jenkins_job_name }} @@ -174,12 +189,34 @@
{{ record.project_slug || '-' }} {{ record.project_name }} + + Jenkins + + + + +
project {{ record.project_parameter || '-' }} 构建号 - {{ record.build_number ? `#${record.build_number}` : '-' }} + #{{ record.build_number }} + {{ record.build_number ? `#${record.build_number}` : '-' }}
{{ record.message }} @@ -298,7 +335,7 @@ export default { ParameterControl }, preferenceKey: 'toolbox.jenkinsBuilds.preferences.v1', - cacheKey: 'toolbox.jenkinsBuilds.projectsCache.v1', + cacheKey: 'toolbox.jenkinsBuilds.projectsCache.v2', operationRecordsKey: 'toolbox.jenkinsBuilds.operationRecords.v1', hiddenParameterNames: ['sql', 'masterCheck'], data() { @@ -308,6 +345,7 @@ export default { triggering: false, statusChecking: false, statusPollingTimer: null, + jenkinsHost: '', projects: [], operationRecords: [], primaryParameterOrder: ['env', 'branchName', 'deploy', 'deployVersion'], @@ -344,17 +382,16 @@ export default { if (freshCachedProjects) { cachedBuildProjects = freshCachedProjects; this.applyProjects(cachedBuildProjects); - return; - } - - if (!cachedBuildProjects) { - cachedBuildProjects = this.loadProjectsCache({ allowExpired: true }); - } - - if (cachedBuildProjects) { - this.applyProjects(cachedBuildProjects); } 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) }); @@ -384,6 +421,7 @@ export default { } cachedBuildProjects = data.data.projects || []; + this.jenkinsHost = data.data.jenkins_host || this.jenkinsHost; this.saveProjectsCache(cachedBuildProjects); this.applyProjects(cachedBuildProjects, { preserveCurrentValues: true }); @@ -431,6 +469,10 @@ export default { return null; } + if (cache.jenkins_host) { + this.jenkinsHost = cache.jenkins_host; + } + return cache.projects; } catch (error) { return null; @@ -439,6 +481,7 @@ export default { saveProjectsCache(projects) { window.localStorage.setItem(this.$options.cacheKey, JSON.stringify({ date: this.todayKey(), + jenkins_host: this.jenkinsHost, projects })); }, @@ -515,6 +558,46 @@ export default { preferenceProjectKey(project) { 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) { return String(parameter.type || '').toLowerCase().includes('boolean'); }, @@ -690,11 +773,14 @@ export default { project_slug: result.project_slug || requested.project_slug || '', project_name: result.project_name || requested.project_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), status: result.success ? (canTrackBuild ? 'PENDING' : 'UNKNOWN') : 'FAILURE', queue_url: result.queue_url || 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 || {}, message: result.success ? (canTrackBuild ? '已提交 Jenkins,等待发布结果' : '已提交 Jenkins,但未返回队列地址,无法自动跟踪或取消') @@ -788,6 +874,7 @@ export default { status: nextStatus, build_number: status.build_number || record.build_number, build_url: status.build_url || record.build_url, + jenkins_job_url: status.jenkins_job_url || record.jenkins_job_url, message: status.message || null, cancelling: false }; diff --git a/tests/Unit/GitMonitorServiceTest.php b/tests/Unit/GitMonitorServiceTest.php index 3a02d0f..371efb3 100644 --- a/tests/Unit/GitMonitorServiceTest.php +++ b/tests/Unit/GitMonitorServiceTest.php @@ -50,6 +50,38 @@ class GitMonitorServiceTest extends TestCase $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->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 diff --git a/tests/Unit/JenkinsClientTest.php b/tests/Unit/JenkinsClientTest.php index 8d809ec..5c59000 100644 --- a/tests/Unit/JenkinsClientTest.php +++ b/tests/Unit/JenkinsClientTest.php @@ -111,4 +111,22 @@ class JenkinsClientTest extends TestCase && $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') + ); + } }