From 7dc992fb191984f0ffa138726a84ae267be9aacb Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 5 Jun 2026 16:46:31 +0200 Subject: [PATCH] sync BE/builders --- .../backend/public/api/build_log.php | 17 ++++- .../backend/public/assets/js/builder.js | 50 +++++++++++++-- .../build_console/backend/src/BuildRunner.php | 64 +++++++++++++++++-- .../backend/views/build_detail.php | 6 +- .../build_console/backend/views/layout.php | 3 +- scripts/docker-build-runner.sh | 52 ++++++++++++--- 6 files changed, 168 insertions(+), 24 deletions(-) diff --git a/examples/build_console/backend/public/api/build_log.php b/examples/build_console/backend/public/api/build_log.php index a6ff7c3..d4587ac 100644 --- a/examples/build_console/backend/public/api/build_log.php +++ b/examples/build_console/backend/public/api/build_log.php @@ -5,6 +5,8 @@ Auth::check(); $id = (int) ($_GET['id'] ?? 0); $offset = (int) ($_GET['offset'] ?? 0); +$full = isset($_GET['full']) && $_GET['full'] === '1'; +$download = isset($_GET['download']) && $_GET['download'] === '1'; if ($id <= 0) { json_out(['ok' => false, 'error' => 'id required'], 400); } @@ -12,7 +14,20 @@ $build = BuildRepository::getById($id); if (!$build) { json_out(['ok' => false, 'error' => 'not found'], 404); } -$tail = BuildRunner::readLogTail($build['log_path'] ?? null, $offset); +$logPath = $build['log_path'] ?? null; +if ($download) { + if (!$logPath || !is_file($logPath)) { + json_out(['ok' => false, 'error' => 'log not found'], 404); + } + $code = preg_replace('/[^a-zA-Z0-9._-]+/', '_', (string) ($build['build_code'] ?? 'build')); + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $code . '.log"'); + readfile($logPath); + exit; +} +$tail = $full + ? BuildRunner::readLogFull($logPath) + : BuildRunner::readLogTail($logPath, $offset); if ($build['status'] === 'running' && $tail['eof'] && is_file(BuildRunner::artifactDir($id) . '/BUILD_INFO.json')) { BuildRunner::finalizeFromArtifacts($id); $build = BuildRepository::getById($id); diff --git a/examples/build_console/backend/public/assets/js/builder.js b/examples/build_console/backend/public/assets/js/builder.js index f13c345..2271e42 100644 --- a/examples/build_console/backend/public/assets/js/builder.js +++ b/examples/build_console/backend/public/assets/js/builder.js @@ -142,31 +142,67 @@ } var buildId = document.body.getAttribute('data-build-id'); + var buildStatus = document.body.getAttribute('data-build-status') || ''; var logEl = document.getElementById('build-log-tail'); if (buildId && logEl) { var offset = 0; var full = ''; + var showFull = false; + var DISPLAY_TAIL = 50000; + + function renderLog() { + logEl.textContent = showFull ? full : full.slice(-DISPLAY_TAIL); + logEl.scrollTop = logEl.scrollHeight; + } + function poll() { - fetch(base + '/api/build_log.php?id=' + encodeURIComponent(buildId) + '&offset=' + offset) + var url = base + '/api/build_log.php?id=' + encodeURIComponent(buildId) + '&offset=' + offset; + fetch(url) .then(function (r) { return r.json(); }) .then(function (j) { if (!j.ok) return; if (j.log && j.log.text) { full += j.log.text; offset = j.log.offset || offset; - logEl.textContent = full.slice(-12000); - logEl.scrollTop = logEl.scrollHeight; + renderLog(); } - if (j.build && j.build.status === 'running') { - setTimeout(poll, 1000); + var running = j.build && j.build.status === 'running'; + var eof = j.log && j.log.eof; + if (running || !eof) { + setTimeout(poll, running ? 1000 : 250); } }).catch(function () { setTimeout(poll, 2000); }); } - poll(); + + function loadFullLog() { + return fetch(base + '/api/build_log.php?id=' + encodeURIComponent(buildId) + '&full=1') + .then(function (r) { return r.json(); }) + .then(function (j) { + if (j.ok && j.log && j.log.text) { + full = j.log.text; + offset = j.log.offset || 0; + renderLog(); + } + }); + } + + if (buildStatus && buildStatus !== 'running' && buildStatus !== 'queued') { + loadFullLog().catch(function () { poll(); }); + } else { + poll(); + } + var expand = document.getElementById('build-log-expand'); if (expand) { expand.addEventListener('click', function () { - logEl.textContent = full; + showFull = true; + if (full === '') { + loadFullLog().then(function () { renderLog(); }); + } else { + renderLog(); + } + expand.textContent = 'Showing full log'; + expand.disabled = true; }); } } diff --git a/examples/build_console/backend/src/BuildRunner.php b/examples/build_console/backend/src/BuildRunner.php index 45bfa56..b5567fd 100644 --- a/examples/build_console/backend/src/BuildRunner.php +++ b/examples/build_console/backend/src/BuildRunner.php @@ -138,6 +138,11 @@ YAML; $runner = (string) cfg('build.runner_script', '/workspace/scripts/docker-build-runner.sh'); $repo = (string) cfg('build.repo_root', '/workspace'); + $gitRemote = trim((string) shell_exec( + 'git -c safe.directory=' . escapeshellarg($repo) + . ' -C ' . escapeshellarg($repo) + . ' remote get-url origin 2>/dev/null' + )); $env = [ 'BUILD_ID=' . $id, 'BUILD_LOG_FILE=' . escapeshellarg($logPath), @@ -149,6 +154,7 @@ YAML; 'ANDROIDCAST_CI_VERSION=' . escapeshellarg((string) cfg('build.ci_version', '00.01.00.1000')), 'GIT_REF=' . escapeshellarg((string) ($params['git_ref'] ?? '')), 'GIT_SHA=' . escapeshellarg((string) ($params['git_sha'] ?? '')), + 'GIT_REMOTE=' . escapeshellarg($gitRemote), 'GRADLE_TASK=' . escapeshellarg((string) ($params['gradle_task'] ?? 'assembleDebug')), 'RUN_UNIT_TESTS=' . (!empty($params['run_tests']) ? '1' : '0'), 'RUN_NATIVE=' . (!empty($params['run_native']) ? '1' : '0'), @@ -260,28 +266,78 @@ YAML; } elseif (str_contains($tail, '[runner] finished')) { self::finalizeFromArtifacts($id); } elseif ($tail !== '') { - self::failBuild($id, 'Build runner exited unexpectedly', $logPath); + $hint = self::summarizeLogFailure($logPath); + $msg = $hint !== '' ? $hint : 'Build runner exited unexpectedly'; + self::failBuild($id, $msg, $logPath); } else { self::failBuild($id, 'Build runner never started', $logPath); } } } + /** Last lines / markers from build.log for reconcile error_message (max 512 chars). */ + public static function summarizeLogFailure(?string $logPath): string { + if (!$logPath || !is_file($logPath)) { + return ''; + } + $size = filesize($logPath) ?: 0; + $chunk = (string) file_get_contents($logPath, false, null, max(0, $size - 32768)); + if ($chunk === '') { + return ''; + } + $lines = preg_split('/\r\n|\r|\n/', $chunk) ?: []; + $markers = []; + foreach ($lines as $line) { + $trim = trim($line); + if ($trim === '') { + continue; + } + if (preg_match('/\[runner\] ERROR:|\[builder\] ERROR:|fatal:|BUILD FAILED|Cannot connect to the Docker daemon/i', $trim)) { + $markers[] = $trim; + } + } + $tailLines = array_values(array_filter($lines, static fn(string $l): bool => trim($l) !== '')); + $last = array_slice($tailLines, -4); + $parts = $markers !== [] ? array_slice($markers, -2) : $last; + $summary = implode(' | ', $parts); + if ($summary === '') { + return ''; + } + if (strlen($summary) > 480) { + $summary = '…' . substr($summary, -477); + } + return 'Build runner exited unexpectedly: ' . $summary; + } + public static function readLogTail(?string $logPath, int $offset = 0, int $maxBytes = 65536): array { if (!$logPath || !is_file($logPath)) { - return ['offset' => 0, 'eof' => true, 'text' => '']; + return ['offset' => 0, 'eof' => true, 'text' => '', 'size' => 0]; } $size = filesize($logPath) ?: 0; $offset = max(0, min($offset, $size)); $fh = fopen($logPath, 'rb'); if (!$fh) { - return ['offset' => $offset, 'eof' => true, 'text' => '']; + return ['offset' => $offset, 'eof' => true, 'text' => '', 'size' => $size]; } fseek($fh, $offset); $chunk = fread($fh, $maxBytes) ?: ''; $newOffset = $offset + strlen($chunk); fclose($fh); - return ['offset' => $newOffset, 'eof' => $newOffset >= $size, 'text' => $chunk]; + return ['offset' => $newOffset, 'eof' => $newOffset >= $size, 'text' => $chunk, 'size' => $size]; + } + + public static function readLogFull(?string $logPath, int $maxBytes = 2097152): array { + if (!$logPath || !is_file($logPath)) { + return ['offset' => 0, 'eof' => true, 'text' => '', 'size' => 0, 'truncated' => false]; + } + $size = filesize($logPath) ?: 0; + $truncated = $size > $maxBytes; + $start = $truncated ? $size - $maxBytes : 0; + $text = (string) file_get_contents($logPath, false, null, $start); + if ($truncated) { + $text = "[log truncated: showing last {$maxBytes} of {$size} bytes]\n" . $text; + } + return ['offset' => $size, 'eof' => true, 'text' => $text, 'size' => $size, 'truncated' => $truncated]; } public static function finalizeFromArtifacts(int $buildId): void { diff --git a/examples/build_console/backend/views/build_detail.php b/examples/build_console/backend/views/build_detail.php index 775b5cb..a591878 100644 --- a/examples/build_console/backend/views/build_detail.php +++ b/examples/build_console/backend/views/build_detail.php @@ -84,6 +84,10 @@ if ($status === 'failed' && $dockerFail && $healthDocker):

Build log

+

Live tail shows the last ~50 KB while running. Failed builds load the full log automatically. Per-step files (when present): docker-build.log, docker-run.log under this build’s artifact dir.


-  
+  
+ + Download log +
diff --git a/examples/build_console/backend/views/layout.php b/examples/build_console/backend/views/layout.php index f1a22c8..5711866 100644 --- a/examples/build_console/backend/views/layout.php +++ b/examples/build_console/backend/views/layout.php @@ -22,7 +22,8 @@ $links = cfg('links', []); > + + >