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).(empty($parameters) ? '/build' : '/buildWithParameters'); $url = $this->host.$path; try { $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, $parameters); if ($response->successful() || $response->status() === 201) { return [ 'success' => true, 'queue_url' => $response->header('Location'), '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; } 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 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('/
]*>/s', $html) ?: [], 1) as $parameterHtml) { if (! preg_match('/]*)>(.*?)<\/select>/s', $parameterHtml, $selectMatch)) { continue; } $name = html_entity_decode($nameMatch[1], ENT_QUOTES | ENT_HTML5); $selectAttributes = $selectMatch[1]; $optionsHtml = $selectMatch[2]; $multiple = str_contains($selectAttributes, 'multiple'); preg_match_all('/]*) value="([^"]*)"[^>]*>(.*?)<\/option>/s', $optionsHtml, $optionMatches, PREG_SET_ORDER); $choices = []; $defaults = []; foreach ($optionMatches as $optionMatch) { $attributes = $optionMatch[1]; $value = html_entity_decode($optionMatch[2], ENT_QUOTES | ENT_HTML5); $label = trim(strip_tags(html_entity_decode($optionMatch[3], ENT_QUOTES | ENT_HTML5))); $selected = str_contains($attributes, 'selected') || str_contains($label, '√'); $label = trim(str_replace('√', '', $label)); $choices[] = [ 'value' => $value, 'label' => $label ?: $value, 'selected' => $selected, ]; if ($selected) { $defaults[] = $value; } } $parameters[$name] = [ 'choices' => $choices, 'default' => $multiple ? $defaults : ($defaults[0] ?? null), 'multiple' => $multiple, ]; } return $parameters; } private function http(): PendingRequest { return Http::timeout($this->timeout) ->withBasicAuth($this->username, $this->apiToken); } private function getCrumb(): ?array { $crumb = $this->request('/crumbIssuer/api/json'); if (! $crumb || empty($crumb['crumb']) || empty($crumb['crumbRequestField'])) { return null; } return [ 'field' => $crumb['crumbRequestField'], 'crumb' => $crumb['crumb'], ]; } private function getJobPath(string $jobName): string { $segments = array_filter(explode('/', trim($jobName, '/')), fn ($segment) => $segment !== ''); return '/job/'.implode('/job/', array_map('rawurlencode', $segments)); } }