From a27958b4306cc93bdabd0a55f0f2af032068037c Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 5 Jun 2026 16:50:08 +0200 Subject: [PATCH] sync BE --- .../backend/public/api/build_actions.php | 8 ++ .../backend/public/assets/js/builder.js | 44 +++++-- .../build_console/backend/src/BuildRunner.php | 93 +++++++++++++- .../backend/views/build_detail.php | 32 ++++- .../backend/public/assets/css/app.css | 120 +++++++++++++++++- scripts/docker-build-runner.sh | 91 +++++++++++-- 6 files changed, 358 insertions(+), 30 deletions(-) diff --git a/examples/build_console/backend/public/api/build_actions.php b/examples/build_console/backend/public/api/build_actions.php index 903bcb9..3d8be86 100644 --- a/examples/build_console/backend/public/api/build_actions.php +++ b/examples/build_console/backend/public/api/build_actions.php @@ -36,6 +36,14 @@ try { $build = BuildRunner::rerunBuild($buildId, $override, $userId); json_out(['ok' => true, 'build' => $build]); } + if ($action === 'rerun_from_failed') { + $build = BuildRunner::rerunFromFailed($buildId, $userId, false); + json_out(['ok' => true, 'build' => $build, 'resume_from' => BuildRunner::detectFailedStep($buildId)]); + } + if ($action === 'rerun_with_ssh') { + $build = BuildRunner::rerunFromFailed($buildId, $userId, true); + json_out(['ok' => true, 'build' => $build, 'resume_from' => BuildRunner::detectFailedStep($buildId)]); + } json_out(['ok' => false, 'error' => 'unknown_action'], 400); } catch (Throwable $e) { error_log('build_actions: ' . $e->getMessage()); diff --git a/examples/build_console/backend/public/assets/js/builder.js b/examples/build_console/backend/public/assets/js/builder.js index 2271e42..621bc44 100644 --- a/examples/build_console/backend/public/assets/js/builder.js +++ b/examples/build_console/backend/public/assets/js/builder.js @@ -79,29 +79,43 @@ } var rerunBtn = document.getElementById('build-rerun-btn'); + var rerunFailedBtn = document.getElementById('build-rerun-failed-btn'); + var rerunSshBtn = document.getElementById('build-rerun-ssh-btn'); var stopBtn = document.getElementById('build-stop-btn'); var rerunAsToggle = document.getElementById('build-rerun-as-toggle'); var rerunAsForm = document.getElementById('build-rerun-as-form'); var actionStatus = document.getElementById('build-action-status'); + + function goToBuild(j) { + if (j.build && j.build.id) { + window.location.href = base + '/?view=build&id=' + j.build.id; + } + } + + function bindRerunAction(btn, action, confirmMsg) { + if (!btn) return; + btn.addEventListener('click', function () { + var id = Number(btn.getAttribute('data-build-id')); + if (!id) return; + if (confirmMsg && !confirm(confirmMsg)) return; + if (actionStatus) actionStatus.textContent = 'Starting…'; + postBuildAction({ action: action, build_id: id }).then(goToBuild).catch(function (err) { + if (actionStatus) actionStatus.textContent = err.message || 'Failed'; + }); + }); + } if (rerunAsToggle && rerunAsForm) { rerunAsToggle.addEventListener('click', function () { rerunAsForm.hidden = !rerunAsForm.hidden; }); } - if (rerunBtn) { - rerunBtn.addEventListener('click', function () { - var id = Number(rerunBtn.getAttribute('data-build-id')); - if (!id) return; - if (actionStatus) actionStatus.textContent = 'Re-running…'; - postBuildAction({ action: 'rerun', build_id: id }).then(function (j) { - if (j.build && j.build.id) { - window.location.href = base + '/?view=build&id=' + j.build.id; - } - }).catch(function (err) { - if (actionStatus) actionStatus.textContent = err.message || 'Failed'; - }); - }); - } + bindRerunAction(rerunBtn, 'rerun', null); + bindRerunAction(rerunFailedBtn, 'rerun_from_failed', null); + bindRerunAction( + rerunSshBtn, + 'rerun_with_ssh', + 'Start an SSH debug session on the builder? You will need shell access to the builder host.' + ); if (stopBtn) { stopBtn.addEventListener('click', function () { var id = Number(stopBtn.getAttribute('data-build-id')); @@ -170,6 +184,8 @@ var eof = j.log && j.log.eof; if (running || !eof) { setTimeout(poll, running ? 1000 : 250); + } else if (j.build && j.build.phase === 'ssh_debug' && !document.getElementById('build-ssh-panel')) { + window.location.reload(); } }).catch(function () { setTimeout(poll, 2000); }); } diff --git a/examples/build_console/backend/src/BuildRunner.php b/examples/build_console/backend/src/BuildRunner.php index b5567fd..a550e1e 100644 --- a/examples/build_console/backend/src/BuildRunner.php +++ b/examples/build_console/backend/src/BuildRunner.php @@ -130,9 +130,10 @@ YAML; return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode, 'status' => 'failed']; } + $phase = !empty($params['ssh_debug']) ? 'ssh_debug' : 'docker'; BuildRepository::update($id, [ 'status' => 'running', - 'phase' => 'docker', + 'phase' => $phase, 'started_at' => gmdate('Y-m-d H:i:s'), ]); @@ -163,6 +164,16 @@ YAML; 'OTA_CHANNEL=' . escapeshellarg((string) ($params['ota_channel'] ?? 'staging')), 'AUTO_OTA=' . (!empty($params['auto_ota']) ? '1' : '0'), ]; + if (!empty($params['resume_from'])) { + $env[] = 'BUILD_RESUME_FROM=' . escapeshellarg((string) $params['resume_from']); + } + if (!empty($params['reuse_build_id'])) { + $reuseSrc = self::artifactDir((int) $params['reuse_build_id']) . '/src'; + $env[] = 'BUILD_REUSE_SRC_DIR=' . escapeshellarg($reuseSrc); + } + if (!empty($params['ssh_debug'])) { + $env[] = 'BUILD_SSH_DEBUG=1'; + } $cmd = implode(' ', $env) . ' nohup ' . escapeshellarg($runner) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 & echo $!'; $pid = trim((string) shell_exec($cmd)); file_put_contents($dir . '/runner.pid', $pid); @@ -193,6 +204,7 @@ YAML; if ($pid !== '' && ctype_digit($pid)) { shell_exec('kill ' . escapeshellarg($pid) . ' 2>/dev/null'); } + self::stopDebugContainer($id); $logPath = $build['log_path'] ?? ($dir . '/build.log'); if ($logPath) { file_put_contents($logPath, "\n[builder] Stopped by operator\n", FILE_APPEND); @@ -222,10 +234,78 @@ YAML; if (empty($params['git_ref'])) { $params['git_ref'] = $build['git_ref'] ?? $build['branch'] ?? 'next'; } - $params['rerun_of'] = $id; + if (!isset($params['rerun_of'])) { + $params['rerun_of'] = $id; + } return self::enqueue($params, $userId); } + /** CircleCI-style: resume from the step that failed (git → docker-build → docker-run). */ + public static function detectFailedStep(int $buildId): string { + $dir = self::artifactDir($buildId); + $logTail = ''; + $logPath = $dir . '/build.log'; + if (is_file($logPath)) { + $size = filesize($logPath) ?: 0; + $logTail = (string) file_get_contents($logPath, false, null, max(0, $size - 65536)); + } + $hasRun = is_file($dir . '/docker-run.log') && (filesize($dir . '/docker-run.log') ?: 0) > 0; + $hasBuild = is_file($dir . '/docker-build.log') && (filesize($dir . '/docker-build.log') ?: 0) > 0; + + if ($hasRun && !is_file($dir . '/android_cast-latest.apk')) { + return 'docker-run'; + } + if ($hasBuild || str_contains($logTail, '[runner] step docker-build')) { + if (!str_contains($logTail, '[runner] step docker-run') && !str_contains($logTail, '[runner] finished')) { + return 'docker-build'; + } + $buildStep = $hasBuild ? (string) file_get_contents($dir . '/docker-build.log') : ''; + if (str_contains($logTail, 'ERROR: failed to build') || str_contains($buildStep, 'ERROR: failed to build')) { + return 'docker-build'; + } + } + if (str_contains($logTail, 'isolate:') || str_contains($logTail, 'syncing shared checkout') + || str_contains($logTail, 'dubious ownership') || is_file($dir . '/git-clone.log')) { + return 'git'; + } + return $hasRun ? 'docker-run' : ($hasBuild ? 'docker-build' : 'git'); + } + + /** @return array */ + public static function rerunFromFailed(int $id, ?int $userId = null, bool $sshDebug = false): array { + $build = BuildRepository::getById($id); + if (!$build) { + throw new InvalidArgumentException('build_not_found'); + } + $status = (string) ($build['status'] ?? ''); + if (!in_array($status, ['failed', 'cancelled'], true)) { + throw new InvalidArgumentException('rerun_from_failed_requires_failed_build'); + } + $step = self::detectFailedStep($id); + return self::rerunBuild($id, [ + 'resume_from' => $step, + 'reuse_build_id' => $id, + 'ssh_debug' => $sshDebug, + 'rerun_of' => $id, + ], $userId); + } + + public static function debugContainerName(int $buildId): string { + return 'androidcast-bld-' . $buildId; + } + + public static function debugContainerAlive(int $buildId): bool { + $bin = escapeshellarg(self::dockerBinary()); + $name = escapeshellarg(self::debugContainerName($buildId)); + return trim((string) shell_exec("{$bin} inspect -f '{{.State.Running}}' {$name} 2>/dev/null")) === 'true'; + } + + public static function stopDebugContainer(int $buildId): void { + $bin = escapeshellarg(self::dockerBinary()); + $name = escapeshellarg(self::debugContainerName($buildId)); + shell_exec("{$bin} rm -f {$name} 2>/dev/null"); + } + public static function runnerAlive(int $buildId): bool { $pidFile = self::artifactDir($buildId) . '/runner.pid'; $pid = is_file($pidFile) ? trim((string) file_get_contents($pidFile)) : ''; @@ -258,6 +338,15 @@ YAML; $size = filesize($logPath) ?: 0; $tail = (string) file_get_contents($logPath, false, null, max(0, $size - 8192)); } + $modeFile = $dir . '/runner.mode'; + if (is_file($modeFile) && trim((string) file_get_contents($modeFile)) === 'ssh_debug') { + if (self::debugContainerAlive($id)) { + BuildRepository::update($id, ['status' => 'running', 'phase' => 'ssh_debug']); + continue; + } + self::failBuild($id, 'SSH debug container ended', $logPath); + continue; + } if (self::runnerAlive($id)) { continue; } diff --git a/examples/build_console/backend/views/build_detail.php b/examples/build_console/backend/views/build_detail.php index a591878..4686989 100644 --- a/examples/build_console/backend/views/build_detail.php +++ b/examples/build_console/backend/views/build_detail.php @@ -2,14 +2,25 @@ $artifacts = json_decode($build['artifacts_json'] ?? '{}', true) ?: []; $params = json_decode($build['params_json'] ?? '{}', true) ?: []; $status = (string) ($build['status'] ?? ''); +$phase = (string) ($build['phase'] ?? ''); $isRunning = $status === 'running'; +$canRerunFromFailed = in_array($status, ['failed', 'cancelled'], true); +$failedStep = $canRerunFromFailed ? BuildRunner::detectFailedStep((int) ($build['id'] ?? 0)) : ''; +$isSshDebug = $phase === 'ssh_debug' || ($isRunning && !empty($params['ssh_debug'])); +$sshContainer = BuildRunner::debugContainerName((int) ($build['id'] ?? 0)); +$builderHost = (string) ($build['builder_id'] ?? gethostname() ?: 'builder'); $pipelineYaml = trim((string) ($build['pipeline_yaml'] ?? '')); if ($pipelineYaml === '') { $pipelineYaml = '(none — will be generated on next trigger from build.config.yml or params)'; } +$stepLabels = [ + 'git' => 'Git checkout / shallow clone', + 'docker-build' => 'Docker image build', + 'docker-run' => 'Gradle pipeline (docker run)', +]; ?>

Build

-

Status: · Phase: +

Status: · Phase: · @@ -21,17 +32,34 @@ if ($status === 'failed' && $dockerFail && $healthDocker): ?>

Ecosystem health reports Docker: ok now. The log below is from this failed run — use Re-run (do not only refresh).

+ +

Failed at step: Re-run from failed skips earlier steps when artifacts allow (like CircleCI).

+

Actions

+ ← Back + + + + - +
+ +
+ SSH debug session +

Shell into the CI container on the builder host (same idea as CircleCI “Rerun with SSH”). Stop ends the session.

+
ssh <user>@
+
+docker exec -it  bash
+
+