From db91842dd9b160abcc5802cc9534ceae03794d4b Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 5 Jun 2026 15:19:48 +0200 Subject: [PATCH] sync BE --- .../backend/config/config.example.php | 1 + .../backend/public/api/build_actions.php | 43 +++++ .../backend/public/api/build_trigger.php | 3 +- .../backend/public/assets/js/builder.js | 83 +++++++++ .../build_console/backend/public/index.php | 6 + .../backend/src/BuildRepository.php | 1 + .../build_console/backend/src/BuildRunner.php | 166 +++++++++++++++++- .../backend/views/build_detail.php | 46 ++++- .../backend/public/assets/css/app.css | 10 ++ .../backend/public/assets/js/app.js | 101 +++++++++-- .../backend/public/assets/js/tickets.js | 77 +++++++- scripts/docker-build-runner.sh | 8 + 12 files changed, 520 insertions(+), 25 deletions(-) create mode 100644 examples/build_console/backend/public/api/build_actions.php diff --git a/examples/build_console/backend/config/config.example.php b/examples/build_console/backend/config/config.example.php index ec6e0e6..30f42c0 100644 --- a/examples/build_console/backend/config/config.example.php +++ b/examples/build_console/backend/config/config.example.php @@ -30,6 +30,7 @@ return [ 'ota_mount' => '/workspace/orchestration/runtime/ota-artifacts', 'ota_base_url' => 'http://localhost:8080', 'runner_script' => '/workspace/scripts/docker-build-runner.sh', + // Repo copy preferred; generated YAML used if missing on builder host 'pipeline_config' => '/workspace/build.config.yml', 'default_channels' => ['stable', 'staging', 'dev', 'nightly'], ], diff --git a/examples/build_console/backend/public/api/build_actions.php b/examples/build_console/backend/public/api/build_actions.php new file mode 100644 index 0000000..903bcb9 --- /dev/null +++ b/examples/build_console/backend/public/api/build_actions.php @@ -0,0 +1,43 @@ + false, 'error' => 'POST required'], 405); +} + +$raw = file_get_contents('php://input') ?: '{}'; +$body = json_decode($raw, true); +if (!is_array($body)) { + $body = $_POST; +} + +$action = (string) ($body['action'] ?? ''); +$buildId = (int) ($body['build_id'] ?? 0); +if ($buildId <= 0) { + json_out(['ok' => false, 'error' => 'build_id required'], 400); +} + +$user = Auth::user(); +$userId = isset($user['id']) ? (int) $user['id'] : null; + +try { + if ($action === 'stop') { + $ok = BuildRunner::stopBuild($buildId); + json_out(['ok' => $ok, 'build' => BuildRepository::getById($buildId)]); + } + if ($action === 'rerun') { + $build = BuildRunner::rerunBuild($buildId, null, $userId); + json_out(['ok' => true, 'build' => $build]); + } + if ($action === 'rerun_as') { + $override = is_array($body['params'] ?? null) ? $body['params'] : []; + $build = BuildRunner::rerunBuild($buildId, $override, $userId); + json_out(['ok' => true, 'build' => $build]); + } + json_out(['ok' => false, 'error' => 'unknown_action'], 400); +} catch (Throwable $e) { + error_log('build_actions: ' . $e->getMessage()); + json_out(['ok' => false, 'error' => $e->getMessage()], 500); +} diff --git a/examples/build_console/backend/public/api/build_trigger.php b/examples/build_console/backend/public/api/build_trigger.php index 3dd302a..be05f77 100644 --- a/examples/build_console/backend/public/api/build_trigger.php +++ b/examples/build_console/backend/public/api/build_trigger.php @@ -13,8 +13,7 @@ if (!is_array($params)) { $params = $_POST; } -$pipelinePath = (string) cfg('build.pipeline_config', '/workspace/build.config.yml'); -$params['pipeline_yaml'] = is_file($pipelinePath) ? (string) file_get_contents($pipelinePath) : null; +$params['pipeline_yaml'] = BuildRunner::resolvePipelineYaml($params); $params['run_tests'] = !empty($params['run_tests']); $params['run_native'] = array_key_exists('run_native', $params) ? !empty($params['run_native']) : true; $params['run_apk'] = array_key_exists('run_apk', $params) ? !empty($params['run_apk']) : true; diff --git a/examples/build_console/backend/public/assets/js/builder.js b/examples/build_console/backend/public/assets/js/builder.js index 3f13c62..f13c345 100644 --- a/examples/build_console/backend/public/assets/js/builder.js +++ b/examples/build_console/backend/public/assets/js/builder.js @@ -58,6 +58,89 @@ }); } + function postBuildAction(payload) { + return fetch(base + '/api/build_actions.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify(payload) + }).then(function (r) { + return r.text().then(function (text) { + var j = null; + try { j = JSON.parse(text); } catch (e) { + throw new Error(r.status + ': ' + (text.slice(0, 120) || r.statusText)); + } + if (!r.ok || !j.ok) { + throw new Error((j && j.error) || ('HTTP ' + r.status)); + } + return j; + }); + }); + } + + var rerunBtn = document.getElementById('build-rerun-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'); + 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'; + }); + }); + } + if (stopBtn) { + stopBtn.addEventListener('click', function () { + var id = Number(stopBtn.getAttribute('data-build-id')); + if (!id || !confirm('Stop this build?')) return; + if (actionStatus) actionStatus.textContent = 'Stopping…'; + postBuildAction({ action: 'stop', build_id: id }).then(function () { + window.location.reload(); + }).catch(function (err) { + if (actionStatus) actionStatus.textContent = err.message || 'Failed'; + }); + }); + } + if (rerunAsForm) { + rerunAsForm.addEventListener('submit', function (ev) { + ev.preventDefault(); + var id = Number(rerunBtn && rerunBtn.getAttribute('data-build-id')); + if (!id) return; + var fd = new FormData(rerunAsForm); + var params = { + git_ref: fd.get('git_ref') || 'next', + gradle_task: fd.get('gradle_task') || 'assembleDebug', + ota_channel: fd.get('ota_channel') || 'staging', + run_tests: !!rerunAsForm.querySelector('[name=run_tests]').checked, + run_native: !!rerunAsForm.querySelector('[name=run_native]').checked, + run_apk: !!rerunAsForm.querySelector('[name=run_apk]').checked, + auto_ota: !!rerunAsForm.querySelector('[name=auto_ota]').checked, + auto_deploy: !!rerunAsForm.querySelector('[name=auto_deploy]').checked + }; + if (actionStatus) actionStatus.textContent = 'Starting…'; + postBuildAction({ action: 'rerun_as', build_id: id, params: params }).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'; + }); + }); + } + var buildId = document.body.getAttribute('data-build-id'); var logEl = document.getElementById('build-log-tail'); if (buildId && logEl) { diff --git a/examples/build_console/backend/public/index.php b/examples/build_console/backend/public/index.php index bb36819..817dde9 100644 --- a/examples/build_console/backend/public/index.php +++ b/examples/build_console/backend/public/index.php @@ -25,6 +25,10 @@ if ($route === '/api/build_log.php' || str_ends_with($route, '/api/build_log.php require __DIR__ . '/api/build_log.php'; exit; } +if ($route === '/api/build_actions.php' || str_ends_with($route, '/api/build_actions.php')) { + require __DIR__ . '/api/build_actions.php'; + exit; +} if ($route === '/api/heartbeat.php' || str_ends_with($route, '/api/heartbeat.php')) { require __DIR__ . '/api/heartbeat.php'; exit; @@ -55,6 +59,8 @@ if ($route === '/login') { Auth::check(); +BuildRunner::reconcileRunningBuilds(); + $view = $_GET['view'] ?? 'home'; if ($view === 'build' && isset($_GET['id'])) { diff --git a/examples/build_console/backend/src/BuildRepository.php b/examples/build_console/backend/src/BuildRepository.php index 6b0e9c0..914c04b 100644 --- a/examples/build_console/backend/src/BuildRepository.php +++ b/examples/build_console/backend/src/BuildRepository.php @@ -111,6 +111,7 @@ SQL); public static function healthSummary(): array { self::ensureSchema(); + BuildRunner::reconcileRunningBuilds(); $pdo = Database::pdo(); $running = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'running'")->fetchColumn(); $failed = (int) $pdo->query("SELECT COUNT(*) FROM builds WHERE status = 'failed' AND created_at > (NOW() - INTERVAL 7 DAY)")->fetchColumn(); diff --git a/examples/build_console/backend/src/BuildRunner.php b/examples/build_console/backend/src/BuildRunner.php index b75591c..c6ad85e 100644 --- a/examples/build_console/backend/src/BuildRunner.php +++ b/examples/build_console/backend/src/BuildRunner.php @@ -2,10 +2,62 @@ declare(strict_types=1); final class BuildRunner { + public static function dockerAvailable(): bool { + return trim((string) shell_exec('docker info >/dev/null 2>&1 && echo ok')) === 'ok'; + } + public static function generateBuildCode(): string { return 'bld-' . gmdate('Ymd-His') . '-' . substr(bin2hex(random_bytes(3)), 0, 6); } + public static function resolvePipelineYaml(array $params): string { + $candidates = array_filter([ + (string) cfg('build.pipeline_config', ''), + rtrim((string) cfg('build.repo_root', ''), '/') . '/build.config.yml', + ]); + foreach ($candidates as $path) { + if ($path !== '' && is_file($path)) { + $text = (string) file_get_contents($path); + if ($text !== '') { + return $text; + } + } + } + return self::generatePipelineYaml($params); + } + + public static function generatePipelineYaml(array $params): string { + $gitRef = (string) ($params['git_ref'] ?? 'next'); + $gradle = (string) ($params['gradle_task'] ?? 'assembleDebug'); + $channel = (string) ($params['ota_channel'] ?? 'staging'); + $runTests = !empty($params['run_tests']) ? 'true' : 'false'; + $runNative = !empty($params['run_native']) ? 'true' : 'false'; + $runApk = !empty($params['run_apk']) ? 'true' : 'false'; + $autoOta = !empty($params['auto_ota']) ? 'true' : 'false'; + $ciVer = (string) cfg('build.ci_version', '00.01.00.1000'); + return << $buildCode, @@ -20,7 +73,7 @@ final class BuildRunner { 'phase' => 'queued', 'git_ref' => $params['git_ref'] ?? null, 'git_sha' => $params['git_sha'] ?? null, - 'branch' => $params['branch'] ?? null, + 'branch' => $params['branch'] ?? ($params['git_ref'] ?? null), 'pipeline_yaml' => $params['pipeline_yaml'] ?? null, 'params_json' => json_encode($params, JSON_UNESCAPED_SLASHES), 'dockerfile_version' => cfg('build.ci_version', '00.01.00.1000'), @@ -35,8 +88,14 @@ final class BuildRunner { throw new RuntimeException('Cannot create artifact dir: ' . $dir); } $logPath = $dir . '/build.log'; + BuildRepository::update($id, ['log_path' => $logPath]); + + if (!self::dockerAvailable()) { + self::failBuild($id, 'Docker daemon not available', $logPath); + return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode, 'status' => 'failed']; + } + BuildRepository::update($id, [ - 'log_path' => $logPath, 'status' => 'running', 'phase' => 'docker', 'started_at' => gmdate('Y-m-d H:i:s'), @@ -67,6 +126,109 @@ final class BuildRunner { return BuildRepository::getById($id) ?? ['id' => $id, 'build_code' => $buildCode]; } + public static function failBuild(int $id, string $message, ?string $logPath = null): void { + if ($logPath) { + file_put_contents($logPath, "\n[builder] ERROR: {$message}\n", FILE_APPEND); + } + BuildRepository::update($id, [ + 'status' => 'failed', + 'phase' => 'error', + 'error_message' => substr($message, 0, 512), + 'finished_at' => gmdate('Y-m-d H:i:s'), + ]); + } + + public static function stopBuild(int $id): bool { + $build = BuildRepository::getById($id); + if (!$build || ($build['status'] ?? '') !== 'running') { + return false; + } + $dir = self::artifactDir($id); + $pidFile = $dir . '/runner.pid'; + $pid = is_file($pidFile) ? trim((string) file_get_contents($pidFile)) : ''; + if ($pid !== '' && ctype_digit($pid)) { + shell_exec('kill ' . escapeshellarg($pid) . ' 2>/dev/null'); + } + $logPath = $build['log_path'] ?? ($dir . '/build.log'); + if ($logPath) { + file_put_contents($logPath, "\n[builder] Stopped by operator\n", FILE_APPEND); + } + BuildRepository::update($id, [ + 'status' => 'cancelled', + 'phase' => 'stopped', + 'error_message' => 'Stopped by operator', + 'finished_at' => gmdate('Y-m-d H:i:s'), + ]); + return true; + } + + /** @return array */ + public static function rerunBuild(int $id, ?array $overrideParams = null, ?int $userId = null): array { + $build = BuildRepository::getById($id); + if (!$build) { + throw new InvalidArgumentException('build_not_found'); + } + $params = json_decode((string) ($build['params_json'] ?? '{}'), true); + if (!is_array($params)) { + $params = []; + } + if ($overrideParams) { + $params = array_merge($params, $overrideParams); + } + if (empty($params['git_ref'])) { + $params['git_ref'] = $build['git_ref'] ?? $build['branch'] ?? 'next'; + } + $params['rerun_of'] = $id; + return self::enqueue($params, $userId); + } + + public static function runnerAlive(int $buildId): bool { + $pidFile = self::artifactDir($buildId) . '/runner.pid'; + $pid = is_file($pidFile) ? trim((string) file_get_contents($pidFile)) : ''; + if ($pid === '' || !ctype_digit($pid)) { + return false; + } + $out = trim((string) shell_exec('kill -0 ' . escapeshellarg($pid) . ' 2>/dev/null && echo alive')); + return $out === 'alive'; + } + + public static function reconcileRunningBuilds(): void { + BuildRepository::ensureSchema(); + $stmt = Database::pdo()->query("SELECT id, log_path FROM builds WHERE status = 'running'"); + if (!$stmt) { + return; + } + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $id = (int) ($row['id'] ?? 0); + if ($id <= 0) { + continue; + } + $dir = self::artifactDir($id); + $logPath = (string) ($row['log_path'] ?? ($dir . '/build.log')); + if (is_file($dir . '/android_cast-latest.apk')) { + self::finalizeFromArtifacts($id); + continue; + } + $tail = ''; + if (is_file($logPath)) { + $size = filesize($logPath) ?: 0; + $tail = (string) file_get_contents($logPath, false, null, max(0, $size - 8192)); + } + if (self::runnerAlive($id)) { + continue; + } + if (str_contains($tail, 'Cannot connect to the Docker daemon') || !self::dockerAvailable()) { + self::failBuild($id, 'Docker daemon not available', $logPath); + } elseif (str_contains($tail, '[runner] finished')) { + self::finalizeFromArtifacts($id); + } elseif ($tail !== '') { + self::failBuild($id, 'Build runner exited unexpectedly', $logPath); + } else { + self::failBuild($id, 'Build runner never started', $logPath); + } + } + } + public static function readLogTail(?string $logPath, int $offset = 0, int $maxBytes = 65536): array { if (!$logPath || !is_file($logPath)) { return ['offset' => 0, 'eof' => true, 'text' => '']; diff --git a/examples/build_console/backend/views/build_detail.php b/examples/build_console/backend/views/build_detail.php index d982134..fdc7f34 100644 --- a/examples/build_console/backend/views/build_detail.php +++ b/examples/build_console/backend/views/build_detail.php @@ -1,9 +1,50 @@

Build

-

Status: · Phase:

+

Status: · Phase: + + · + +

+ +
+

Actions

+
+ + + + + + +
+ +
@@ -30,7 +71,8 @@ $params = json_decode($build['params_json'] ?? '{}', true) ?: [];

Pipeline (YAML)

-
+

Snapshot stored with this build. New triggers use build.config.yml from the repo when present, otherwise a generated default.

+
diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index c0b5dce..d7dafdd 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -826,6 +826,16 @@ a:hover { text-decoration: underline; } color: #fff; background: var(--tag-bg, #5c6b82); } +button.report-tag--filter { + border: none; + cursor: pointer; + font: inherit; + color: inherit; + line-height: 1.3; +} +button.report-tag--filter:hover { + filter: brightness(1.12); +} .reports-filter-banner { margin: 8px 0 0; padding: 10px 12px; diff --git a/examples/crash_reporter/backend/public/assets/js/app.js b/examples/crash_reporter/backend/public/assets/js/app.js index 34d3bc4..c4a3864 100644 --- a/examples/crash_reporter/backend/public/assets/js/app.js +++ b/examples/crash_reporter/backend/public/assets/js/app.js @@ -224,17 +224,26 @@ return { label: kind || '?', bg: '#5c6b82' }; } - function renderTagPills(tags) { + function renderTagPills(tags, filterable) { if (!tags.length) return ''; return tags - .map( - (t) => - '' + - escapeHtml(t.label) + - '' - ) + .map((t) => { + const label = escapeHtml(t.label || t.id || ''); + const bg = escapeHtml(t.bg || '#5c6b82'); + const id = t.id ? String(t.id).trim() : ''; + if (filterable && id) { + return ( + '' + ); + } + return '' + label + ''; + }) .join(''); } @@ -255,12 +264,13 @@ const id = String(t.id).toLowerCase(); if (id === 'new' || id === 'java' || id === 'ndk' || id === 'android') return; tags.push({ + id, label: t.label || t.id, bg: t.bg || '#5c6b82', }); }); } - return renderTagPills(tags); + return renderTagPills(tags, !grouped); } function apiSortKey(key) { @@ -621,12 +631,12 @@ if (!href) return; const go = () => navigateTo(href); const onActivate = (e) => { - if (e.target.closest('.report-tree-toggle')) return; + if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return; go(); }; el.addEventListener('click', onActivate); el.addEventListener('keydown', (e) => { - if (e.target.closest('.report-tree-toggle')) return; + if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); @@ -635,6 +645,16 @@ }); } + function bindReportTagFilters(root, onTag) { + if (!root || typeof onTag !== 'function') return; + root.querySelectorAll('.report-tag--filter').forEach((btn) => { + btn.addEventListener('click', (e) => { + const tagId = btn.getAttribute('data-tag-id') || ''; + if (tagId) onTag(tagId, e); + }); + }); + } + function initReportsApp() { const app = document.getElementById('reports-app'); if (!app) return; @@ -948,6 +968,63 @@ }); tbody.innerHTML = html; bindTreeInteractions(tbody); + bindReportTagFilters(tbody, handleReportTagClick); + } + + function applyReportTagFilter(tagId) { + listContext.filterTags = [tagId]; + listContext.tagMode = listContext.tagMode || 'and'; + grouped = false; + app.setAttribute('data-grouped', '0'); + page = 1; + updateFilterBanner(listContext, filterBanner); + const p = new URLSearchParams(window.location.search); + p.set('view', 'reports'); + p.delete('tag'); + p.append('tag', tagId); + p.set('tag_mode', listContext.tagMode); + p.delete('group'); + p.delete('page'); + history.replaceState(null, '', basePath() + '/?' + p.toString()); + const modeWrap = document.getElementById('tag-mode-wrap'); + if (modeWrap) modeWrap.hidden = false; + const modeSel = document.getElementById('tag-mode-select'); + if (modeSel) modeSel.value = listContext.tagMode; + load(); + } + + function handleReportTagClick(tagId, ev) { + ev.stopPropagation(); + ev.preventDefault(); + if (grouped) { + applyReportTagFilter(tagId); + return; + } + const xhr = new XMLHttpRequest(); + xhr.open( + 'GET', + basePath() + + '/api/reports.php?page=1&per_page=2&tag=' + + encodeURIComponent(tagId) + + '&tag_mode=and&sort=received_at_ms&dir=desc&since_ms=0', + true + ); + xhr.onload = function () { + let data; + try { + data = JSON.parse(xhr.responseText); + } catch { + return; + } + if (!data.ok) return; + const total = Number(data.total || 0); + if (total === 1 && data.items && data.items[0] && data.items[0].id) { + navigateTo(basePath() + '/?view=report&id=' + data.items[0].id); + return; + } + applyReportTagFilter(tagId); + }; + xhr.send(); } function load() { diff --git a/examples/crash_reporter/backend/public/assets/js/tickets.js b/examples/crash_reporter/backend/public/assets/js/tickets.js index 9b1d565..ce5889e 100644 --- a/examples/crash_reporter/backend/public/assets/js/tickets.js +++ b/examples/crash_reporter/backend/public/assets/js/tickets.js @@ -32,12 +32,20 @@ } function renderTagPill(tag) { + const id = String(tag.id || '').trim(); + const label = escapeHtml(tag.label || tag.id); + const bg = escapeHtml(tag.bg || '#5c6b82'); + if (!id) { + return '' + label + ''; + } return ( - '' + - escapeHtml(tag.label || tag.id) + - '' + '' ); } @@ -172,6 +180,56 @@ bindRows(); } + function applyTagFilter(tagId) { + tag = tagId; + if (tagFilter) { + let found = false; + tagFilter.querySelectorAll('option').forEach((opt) => { + if (opt.value === tagId) found = true; + }); + if (!found) { + const opt = document.createElement('option'); + opt.value = tagId; + opt.textContent = tagId; + tagFilter.appendChild(opt); + } + tagFilter.value = tagId; + } + page = 1; + load(); + } + + function activateTag(tagId, ev) { + if (!tagId) return; + ev.stopPropagation(); + ev.preventDefault(); + const xhr = new XMLHttpRequest(); + xhr.open( + 'GET', + basePath() + + '/api/tickets.php?page=1&per_page=2&tag=' + + encodeURIComponent(tagId), + true + ); + xhr.onload = function () { + let data; + try { + data = JSON.parse(xhr.responseText); + } catch { + return; + } + if (!data.ok) return; + const total = Number(data.total || 0); + if (total === 1 && data.items && data.items[0] && data.items[0].id) { + window.location.href = + basePath() + '/?view=ticket&id=' + encodeURIComponent(String(data.items[0].id)); + return; + } + applyTagFilter(tagId); + }; + xhr.send(); + } + function bindRows() { tbody.querySelectorAll('.report-tree-toggle').forEach((btn) => { btn.addEventListener('click', (e) => { @@ -192,11 +250,11 @@ window.location.href = href; }; el.addEventListener('click', (e) => { - if (e.target.closest('.report-tree-toggle, .tag-edit-btn')) return; + if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return; go(); }); el.addEventListener('keydown', (e) => { - if (e.target.closest('.report-tree-toggle, .tag-edit-btn')) return; + if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); @@ -212,6 +270,11 @@ } }); }); + tbody.querySelectorAll('.report-tag--filter').forEach((btn) => { + btn.addEventListener('click', (e) => { + activateTag(btn.getAttribute('data-tag-id') || '', e); + }); + }); } function renderPagination(total, currentPage) { diff --git a/scripts/docker-build-runner.sh b/scripts/docker-build-runner.sh index 2894a2c..eb97f0f 100755 --- a/scripts/docker-build-runner.sh +++ b/scripts/docker-build-runner.sh @@ -20,6 +20,14 @@ mkdir -p "$(dirname "$LOG_FILE")" "$OUT_DIR" echo "[runner] image=${IMAGE} ci_version=${CI_VERSION} build_id=${BUILD_ID}" | tee -a "$LOG_FILE" echo "[runner] workdir=${WORK_DIR} out=${OUT_DIR}" | tee -a "$LOG_FILE" +if [[ -n "${GIT_REF:-}" ]] && [[ -d "${WORK_DIR}/.git" ]]; then + echo "[runner] syncing git ref=${GIT_REF}" | tee -a "$LOG_FILE" + git -C "${WORK_DIR}" fetch origin --prune 2>&1 | tee -a "$LOG_FILE" || true + git -C "${WORK_DIR}" checkout "${GIT_REF}" 2>&1 | tee -a "$LOG_FILE" || true + git -C "${WORK_DIR}" pull --rebase origin "${GIT_REF}" 2>&1 | tee -a "$LOG_FILE" || true + git -C "${WORK_DIR}" rev-parse HEAD 2>&1 | tee -a "$LOG_FILE" || true +fi + docker build \ --build-arg ANDROIDCAST_CI_VERSION="${CI_VERSION}" \ -t "$IMAGE" \