#feature: add Jenkins batch build trigger
Add an admin Jenkins build page that reuses notification-enabled project config, loads Jenkins build parameters, supports persisted selections, and triggers selected jobs in one action.
This commit is contained in:
+219
-10
@@ -2,14 +2,18 @@
|
||||
|
||||
namespace App\Clients;
|
||||
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class JenkinsClient
|
||||
{
|
||||
private ?string $host;
|
||||
|
||||
private ?string $username;
|
||||
|
||||
private ?string $apiToken;
|
||||
|
||||
private int $timeout;
|
||||
|
||||
public function __construct()
|
||||
@@ -23,28 +27,121 @@ class JenkinsClient
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return !empty($this->host) && !empty($this->username) && !empty($this->apiToken);
|
||||
return ! empty($this->host) && ! empty($this->username) && ! empty($this->apiToken);
|
||||
}
|
||||
|
||||
public function getJobInfo(string $jobName): ?array
|
||||
{
|
||||
return $this->request("/job/{$jobName}/api/json");
|
||||
return $this->request($this->getJobPath($jobName).'/api/json');
|
||||
}
|
||||
|
||||
public function getBuildInfo(string $jobName, int $buildNumber): ?array
|
||||
{
|
||||
return $this->request("/job/{$jobName}/{$buildNumber}/api/json");
|
||||
return $this->request($this->getJobPath($jobName)."/{$buildNumber}/api/json");
|
||||
}
|
||||
|
||||
public function getLastBuild(string $jobName): ?array
|
||||
{
|
||||
return $this->request("/job/{$jobName}/lastBuild/api/json");
|
||||
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'])) {
|
||||
if (! $jobInfo || empty($jobInfo['builds'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -63,17 +160,16 @@ class JenkinsClient
|
||||
|
||||
private function request(string $path): ?array
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
if (! $this->isConfigured()) {
|
||||
Log::warning('Jenkins client is not configured');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = $this->host . $path;
|
||||
$url = $this->host.$path;
|
||||
|
||||
try {
|
||||
$response = Http::timeout($this->timeout)
|
||||
->withBasicAuth($this->username, $this->apiToken)
|
||||
->get($url);
|
||||
$response = $this->http()->get($url);
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json();
|
||||
@@ -94,4 +190,117 @@ class JenkinsClient
|
||||
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('/<div name="parameter"[^>]*>/s', $html) ?: [], 1) as $parameterHtml) {
|
||||
if (! preg_match('/<input name="name" type="hidden" value="([^"]+)"/s', $parameterHtml, $nameMatch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! preg_match('/<select([^>]*)>(.*?)<\/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('/<option([^>]*) 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Clients\JenkinsClient;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class JenkinsBuildController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly JenkinsClient $jenkinsClient
|
||||
) {}
|
||||
|
||||
public function projects(): JsonResponse
|
||||
{
|
||||
if (! $this->jenkinsClient->isConfigured()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Jenkins 未配置,请先配置 JENKINS_HOST、JENKINS_USERNAME、JENKINS_API_TOKEN',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$projects = Project::getJenkinsNotifyEnabled()
|
||||
->map(function (Project $project) {
|
||||
return [
|
||||
'id' => $project->id,
|
||||
'slug' => $project->slug,
|
||||
'name' => $project->name,
|
||||
'jenkins_job_name' => $project->jenkins_job_name,
|
||||
'parameters' => $this->jenkinsClient->getParameterDefinitions($project->jenkins_job_name),
|
||||
];
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'projects' => $projects,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function trigger(Request $request): JsonResponse
|
||||
{
|
||||
if (! $this->jenkinsClient->isConfigured()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Jenkins 未配置,请先配置 Jenkins 连接信息',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$projectSlugs = Project::getJenkinsNotifyEnabled()->pluck('slug')->all();
|
||||
|
||||
$data = $request->validate([
|
||||
'builds' => ['required', 'array', 'min:1'],
|
||||
'builds.*.project_slug' => ['required', 'string', Rule::in($projectSlugs)],
|
||||
'builds.*.parameters' => ['nullable', 'array'],
|
||||
]);
|
||||
|
||||
$projects = Project::getJenkinsNotifyEnabled()->keyBy('slug');
|
||||
$results = [];
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($data['builds'] as $build) {
|
||||
/** @var Project $project */
|
||||
$project = $projects[$build['project_slug']];
|
||||
$parameters = $this->normalizeParameters($build['parameters'] ?? []);
|
||||
$result = $this->jenkinsClient->triggerBuild($project->jenkins_job_name, $parameters);
|
||||
|
||||
if ($result['success'] ?? false) {
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'project_slug' => $project->slug,
|
||||
'project_name' => $project->name,
|
||||
'job_name' => $project->jenkins_job_name,
|
||||
'success' => (bool) ($result['success'] ?? false),
|
||||
'message' => $result['message'] ?? null,
|
||||
'queue_url' => $result['queue_url'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => $successCount === count($results),
|
||||
'message' => sprintf('触发完成:成功 %d 个,失败 %d 个', $successCount, count($results) - $successCount),
|
||||
'data' => [
|
||||
'results' => $results,
|
||||
],
|
||||
], $successCount > 0 ? 200 : 422);
|
||||
}
|
||||
|
||||
private function normalizeParameters(array $parameters): array
|
||||
{
|
||||
return collect($parameters)
|
||||
->filter(fn ($value) => $value !== null && $value !== '')
|
||||
->map(function ($value) {
|
||||
if (is_array($value)) {
|
||||
return implode(',', array_filter($value, fn ($item) => $item !== null && $item !== ''));
|
||||
}
|
||||
|
||||
return is_bool($value) ? ($value ? 'true' : 'false') : $value;
|
||||
})
|
||||
->filter(fn ($value) => $value !== '')
|
||||
->all();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user