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