host = rtrim($config['host'] ?? '', '/'); $this->username = $config['username'] ?? null; $this->apiToken = $config['api_token'] ?? null; $this->timeout = $config['timeout'] ?? 30; } public function isConfigured(): bool { return ! empty($this->host) && ! empty($this->username) && ! empty($this->apiToken); } public function getJobInfo(string $jobName): ?array { return $this->request($this->getJobPath($jobName).'/api/json'); } public function getBuildInfo(string $jobName, int $buildNumber): ?array { return $this->request($this->getJobPath($jobName)."/{$buildNumber}/api/json"); } public function getLastBuild(string $jobName): ?array { return $this->request($this->getJobPath($jobName).'/lastBuild/api/json'); } public function getParameterDefinitions(string $jobName): array { $jobInfo = $this->getJobInfo($jobName); if (! $jobInfo || empty($jobInfo['property'])) { return []; } $buildFormParameters = $this->getBuildFormParameters($jobName); foreach ($jobInfo['property'] as $property) { if (empty($property['parameterDefinitions']) || ! is_array($property['parameterDefinitions'])) { continue; } return array_map(function (array $definition) use ($buildFormParameters) { $name = $definition['name'] ?? ''; $formParameter = $buildFormParameters[$name] ?? []; $default = $definition['defaultParameterValue']['value'] ?? null; if (($formParameter['multiple'] ?? false) && isset($formParameter['default'])) { $default = $formParameter['default']; } return [ 'name' => $name, 'type' => $definition['type'] ?? $definition['_class'] ?? 'StringParameterDefinition', 'description' => $definition['description'] ?? '', 'default' => $default, 'choices' => $definition['choices'] ?? $formParameter['choices'] ?? [], 'multiple' => (bool) ($formParameter['multiple'] ?? false), ]; }, array_values(array_filter($property['parameterDefinitions'], fn ($definition) => ! empty($definition['name'])))); } return []; } public function triggerBuild(string $jobName, array $parameters = []): array { if (! $this->isConfigured()) { Log::warning('Jenkins client is not configured'); return [ 'success' => false, 'message' => 'Jenkins not configured', ]; } $path = $this->getJobPath($jobName).'/build?delay=0sec'; $url = $this->host.$path; try { $jobInfo = $this->getJobInfo($jobName); $nextBuildNumber = isset($jobInfo['nextBuildNumber']) ? (int) $jobInfo['nextBuildNumber'] : null; $request = $this->http(); $crumb = $this->getCrumb(); if ($crumb) { $request = $request->withHeaders([$crumb['field'] => $crumb['crumb']]); } $response = empty($parameters) ? $request->post($url) : $request->asForm()->post($url, [ 'json' => json_encode([ 'parameter' => $this->buildFormParameters($parameters), 'statusCode' => '303', 'redirectTo' => '.', ], JSON_UNESCAPED_UNICODE), 'Submit' => 'Build', ]); if ($response->successful() || $response->status() === 201) { return [ 'success' => true, 'queue_url' => $response->header('Location'), 'build_number' => $nextBuildNumber, 'status' => $response->status(), ]; } Log::warning('Jenkins build trigger failed', [ 'url' => $url, 'status' => $response->status(), 'body' => $response->body(), ]); return [ 'success' => false, 'message' => 'Jenkins 返回状态码 '.$response->status(), 'status' => $response->status(), ]; } catch (\Throwable $e) { Log::error('Jenkins build trigger error', [ 'url' => $url, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'message' => $e->getMessage(), ]; } } public function getBuilds(string $jobName, int $limit = 10): array { $jobInfo = $this->getJobInfo($jobName); if (! $jobInfo || empty($jobInfo['builds'])) { return []; } $builds = array_slice($jobInfo['builds'], 0, $limit); $result = []; foreach ($builds as $build) { $buildInfo = $this->getBuildInfo($jobName, $build['number']); if ($buildInfo) { $result[] = $buildInfo; } } return $result; } public function getBuildStatus(string $jobName, ?string $queueUrl = null, ?int $buildNumber = null): array { if (! $this->isConfigured()) { Log::warning('Jenkins client is not configured'); return [ 'success' => false, 'status' => 'UNKNOWN', 'message' => 'Jenkins not configured', ]; } $queueItem = null; if ($queueUrl && ! $buildNumber) { $queueItem = $this->getQueueItem($queueUrl); if (! $queueItem) { return [ 'success' => false, 'status' => 'UNKNOWN', 'queue_url' => $queueUrl, 'message' => '无法获取 Jenkins 队列状态', ]; } if ($queueItem['cancelled'] ?? false) { return [ 'success' => true, 'status' => 'ABORTED', 'result' => 'ABORTED', 'completed' => true, 'queue_url' => $queueUrl, ]; } if (empty($queueItem['executable']['number'])) { return [ 'success' => true, 'status' => 'PENDING', 'building' => true, 'completed' => false, 'queue_url' => $queueUrl, 'message' => $queueItem['why'] ?? null, ]; } $buildNumber = (int) $queueItem['executable']['number']; } if (! $buildNumber) { return [ 'success' => false, 'status' => 'UNKNOWN', 'queue_url' => $queueUrl, 'message' => '缺少 Jenkins 构建号', ]; } $buildInfo = $this->getBuildInfo($jobName, $buildNumber); if (! $buildInfo) { return [ 'success' => true, 'status' => 'PENDING', 'building' => true, 'completed' => false, 'build_number' => $buildNumber, 'queue_url' => $queueUrl, 'message' => $this->findQueuedItem($jobName) ? '等待 Jenkins 开始构建' : '等待 Jenkins 创建构建', ]; } $building = (bool) ($buildInfo['building'] ?? false); $result = $buildInfo['result'] ?? null; return [ 'success' => true, 'status' => $building ? 'BUILDING' : ($result ?? 'UNKNOWN'), 'result' => $result, 'building' => $building, 'completed' => ! $building && ! empty($result), 'build_number' => $buildNumber, 'build_url' => $buildInfo['url'] ?? ($queueItem['executable']['url'] ?? null), 'queue_url' => $queueUrl, ]; } public function cancelBuild(string $jobName, ?string $queueUrl = null, ?int $buildNumber = null): array { if (! $this->isConfigured()) { Log::warning('Jenkins client is not configured'); return [ 'success' => false, 'message' => 'Jenkins not configured', ]; } if ($queueUrl && ! $buildNumber) { $queueItem = $this->getQueueItem($queueUrl); if (! empty($queueItem['executable']['number'])) { $buildNumber = (int) $queueItem['executable']['number']; } else { $queueId = $this->extractQueueId($queueUrl); if (! $queueId) { return [ 'success' => false, 'message' => '无法识别 Jenkins 队列 ID', ]; } return [ ...$this->post('/queue/cancelItem?id='.rawurlencode($queueId), 'Jenkins queue cancel'), 'cancelled_queue' => true, ]; } } $queueItem = $this->findQueuedItem($jobName); if (! empty($queueItem['id'])) { return [ ...$this->post('/queue/cancelItem?id='.rawurlencode((string) $queueItem['id']), 'Jenkins queue cancel'), 'cancelled_queue' => true, ]; } if (! $buildNumber) { return [ 'success' => false, 'message' => '缺少 Jenkins 构建号', ]; } return [ ...$this->post($this->getJobPath($jobName)."/{$buildNumber}/stop", 'Jenkins build stop'), 'stopping_build' => true, ]; } private function request(string $path): ?array { if (! $this->isConfigured()) { Log::warning('Jenkins client is not configured'); return null; } $url = $this->host.$path; try { $response = $this->http()->get($url); if ($response->successful()) { return $response->json(); } Log::warning('Jenkins API request failed', [ 'url' => $url, 'status' => $response->status(), ]); return null; } catch (\Throwable $e) { Log::error('Jenkins API request error', [ 'url' => $url, 'error' => $e->getMessage(), ]); return null; } } private function post(string $path, string $operation): array { $url = $this->host.$path; try { $request = $this->http(); $crumb = $this->getCrumb(); if ($crumb) { $request = $request->withHeaders([$crumb['field'] => $crumb['crumb']]); } $response = $request->post($url); if ($response->successful() || in_array($response->status(), [201, 302], true)) { return [ 'success' => true, 'status' => $response->status(), ]; } Log::warning($operation.' failed', [ 'url' => $url, 'status' => $response->status(), 'body' => $response->body(), ]); return [ 'success' => false, 'message' => 'Jenkins 返回状态码 '.$response->status(), 'status' => $response->status(), ]; } catch (\Throwable $e) { Log::error($operation.' error', [ 'url' => $url, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'message' => $e->getMessage(), ]; } } private function buildFormParameters(array $parameters): array { return collect($parameters) ->map(fn ($value, $name) => [ 'name' => $name, 'value' => $value, ]) ->values() ->all(); } private function requestBody(string $path, bool $allowMethodNotAllowed = false): ?string { if (! $this->isConfigured()) { Log::warning('Jenkins client is not configured'); return null; } $url = $this->host.$path; try { $response = $this->http()->get($url); if ($response->successful() || ($allowMethodNotAllowed && $response->status() === 405)) { return $response->body(); } Log::warning('Jenkins page request failed', [ 'url' => $url, 'status' => $response->status(), ]); return null; } catch (\Throwable $e) { Log::error('Jenkins page request error', [ 'url' => $url, 'error' => $e->getMessage(), ]); return null; } } private function getBuildFormParameters(string $jobName): array { $html = $this->requestBody($this->getJobPath($jobName).'/build?delay=0sec', allowMethodNotAllowed: true); if (! $html) { return []; } $parameters = []; foreach (array_slice(preg_split('/